Index: pkg/polymer/lib/src/css_analyzer.dart |
diff --git a/pkg/polymer/lib/src/css_analyzer.dart b/pkg/polymer/lib/src/css_analyzer.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..270af5d5ba0c6a4485298097a3e67c948cd70c94 |
--- /dev/null |
+++ b/pkg/polymer/lib/src/css_analyzer.dart |
@@ -0,0 +1,507 @@ |
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+/** Portion of the analyzer dealing with CSS sources. */ |
+library polymer.src.css_analyzer; |
+ |
+import 'package:csslib/parser.dart' as css; |
+import 'package:csslib/visitor.dart'; |
+import 'package:html5lib/dom.dart'; |
+import 'package:html5lib/dom_parsing.dart'; |
+ |
+import 'info.dart'; |
+import 'files.dart' show SourceFile; |
+import 'messages.dart'; |
+import 'compiler_options.dart'; |
+ |
+void analyzeCss(String packageRoot, List<SourceFile> files, |
+ Map<String, FileInfo> info, Map<String, String> pseudoElements, |
+ Messages messages, {warningsAsErrors: false}) { |
+ var analyzer = new _AnalyzerCss(packageRoot, info, pseudoElements, messages, |
+ warningsAsErrors); |
+ for (var file in files) analyzer.process(file); |
+ analyzer.normalize(); |
+} |
+ |
+class _AnalyzerCss { |
+ final String packageRoot; |
+ final Map<String, FileInfo> info; |
+ final Map<String, String> _pseudoElements; |
+ final Messages _messages; |
+ final bool _warningsAsErrors; |
+ |
+ Set<StyleSheet> allStyleSheets = new Set<StyleSheet>(); |
+ |
+ /** |
+ * [_pseudoElements] list of known pseudo attributes found in HTML, any |
+ * CSS pseudo-elements 'name::custom-element' is mapped to the manged name |
+ * associated with the pseudo-element key. |
+ */ |
+ _AnalyzerCss(this.packageRoot, this.info, this._pseudoElements, |
+ this._messages, this._warningsAsErrors); |
+ |
+ /** |
+ * Run the analyzer on every file that is a style sheet or any component that |
+ * has a style tag. |
+ */ |
+ void process(SourceFile file) { |
+ var fileInfo = info[file.path]; |
+ if (file.isStyleSheet || fileInfo.styleSheets.length > 0) { |
+ var styleSheets = processVars(fileInfo); |
+ |
+ // Add to list of all style sheets analyzed. |
+ allStyleSheets.addAll(styleSheets); |
+ } |
+ |
+ // Process any components. |
+ for (var component in fileInfo.declaredComponents) { |
+ var all = processVars(component); |
+ |
+ // Add to list of all style sheets analyzed. |
+ allStyleSheets.addAll(all); |
+ } |
+ |
+ processCustomPseudoElements(); |
+ } |
+ |
+ void normalize() { |
+ // Remove all var definitions for all style sheets analyzed. |
+ for (var tree in allStyleSheets) new _RemoveVarDefinitions().visitTree(tree); |
+ } |
+ |
+ List<StyleSheet> processVars(var libraryInfo) { |
+ // Get list of all stylesheet(s) dependencies referenced from this file. |
+ var styleSheets = _dependencies(libraryInfo).toList(); |
+ |
+ var errors = []; |
+ css.analyze(styleSheets, errors: errors, options: |
+ [_warningsAsErrors ? '--warnings_as_errors' : '', 'memory']); |
+ |
+ // Print errors as warnings. |
+ for (var e in errors) { |
+ _messages.warning(e.message, e.span); |
+ } |
+ |
+ // Build list of all var definitions. |
+ Map varDefs = new Map(); |
+ for (var tree in styleSheets) { |
+ var allDefs = (new _VarDefinitions()..visitTree(tree)).found; |
+ allDefs.forEach((key, value) { |
+ varDefs[key] = value; |
+ }); |
+ } |
+ |
+ // Resolve all definitions to a non-VarUsage (terminal expression). |
+ varDefs.forEach((key, value) { |
+ for (var expr in (value.expression as Expressions).expressions) { |
+ var def = _findTerminalVarDefinition(varDefs, value); |
+ varDefs[key] = def; |
+ } |
+ }); |
+ |
+ // Resolve all var usages. |
+ for (var tree in styleSheets) new _ResolveVarUsages(varDefs).visitTree(tree); |
+ |
+ return styleSheets; |
+ } |
+ |
+ processCustomPseudoElements() { |
+ var polyFiller = new _PseudoElementExpander(_pseudoElements); |
+ for (var tree in allStyleSheets) { |
+ polyFiller.visitTree(tree); |
+ } |
+ } |
+ |
+ /** |
+ * Given a component or file check if any stylesheets referenced. If so then |
+ * return a list of all referenced stylesheet dependencies (@imports or <link |
+ * rel="stylesheet" ..>). |
+ */ |
+ Set<StyleSheet> _dependencies(var libraryInfo, {Set<StyleSheet> seen}) { |
+ if (seen == null) seen = new Set(); |
+ |
+ // Used to resolve all pathing information. |
+ var inputUrl = libraryInfo is FileInfo |
+ ? libraryInfo.inputUrl |
+ : (libraryInfo as ComponentInfo).declaringFile.inputUrl; |
+ |
+ for (var styleSheet in libraryInfo.styleSheets) { |
+ if (!seen.contains(styleSheet)) { |
+ // TODO(terry): VM uses expandos to implement hashes. Currently, it's a |
+ // linear (not constant) time cost (see dartbug.com/5746). |
+ // If this bug isn't fixed and performance show's this a |
+ // a problem we'll need to implement our own hashCode or |
+ // use a different key for better perf. |
+ // Add the stylesheet. |
+ seen.add(styleSheet); |
+ |
+ // Any other imports in this stylesheet? |
+ var urlInfos = findImportsInStyleSheet(styleSheet, packageRoot, |
+ inputUrl, _messages); |
+ |
+ // Process other imports in this stylesheets. |
+ for (var importSS in urlInfos) { |
+ var importInfo = info[importSS.resolvedPath]; |
+ if (importInfo != null) { |
+ // Add all known stylesheets processed. |
+ seen.addAll(importInfo.styleSheets); |
+ // Find dependencies for stylesheet referenced with a |
+ // @import |
+ for (var ss in importInfo.styleSheets) { |
+ var urls = findImportsInStyleSheet(ss, packageRoot, inputUrl, |
+ _messages); |
+ for (var url in urls) { |
+ _dependencies(info[url.resolvedPath], seen: seen); |
+ } |
+ } |
+ } |
+ } |
+ } |
+ } |
+ |
+ return seen; |
+ } |
+} |
+ |
+/** |
+ * Find var- definitions in a style sheet. |
+ * [found] list of known definitions. |
+ */ |
+class _VarDefinitions extends Visitor { |
+ final Map<String, VarDefinition> found = new Map(); |
+ |
+ void visitTree(StyleSheet tree) { |
+ visitStyleSheet(tree); |
+ } |
+ |
+ visitVarDefinition(VarDefinition node) { |
+ // Replace with latest variable definition. |
+ found[node.definedName] = node; |
+ super.visitVarDefinition(node); |
+ } |
+ |
+ void visitVarDefinitionDirective(VarDefinitionDirective node) { |
+ visitVarDefinition(node.def); |
+ } |
+} |
+ |
+/** |
+ * Resolve any CSS expression which contains a var() usage to the ultimate real |
+ * CSS expression value e.g., |
+ * |
+ * var-one: var(two); |
+ * var-two: #ff00ff; |
+ * |
+ * .test { |
+ * color: var(one); |
+ * } |
+ * |
+ * then .test's color would be #ff00ff |
+ */ |
+class _ResolveVarUsages extends Visitor { |
+ final Map<String, VarDefinition> varDefs; |
+ bool inVarDefinition = false; |
+ bool inUsage = false; |
+ Expressions currentExpressions; |
+ |
+ _ResolveVarUsages(this.varDefs); |
+ |
+ void visitTree(StyleSheet tree) { |
+ visitStyleSheet(tree); |
+ } |
+ |
+ void visitVarDefinition(VarDefinition varDef) { |
+ inVarDefinition = true; |
+ super.visitVarDefinition(varDef); |
+ inVarDefinition = false; |
+ } |
+ |
+ void visitExpressions(Expressions node) { |
+ currentExpressions = node; |
+ super.visitExpressions(node); |
+ currentExpressions = null; |
+ } |
+ |
+ void visitVarUsage(VarUsage node) { |
+ // Don't process other var() inside of a varUsage. That implies that the |
+ // default is a var() too. Also, don't process any var() inside of a |
+ // varDefinition (they're just place holders until we've resolved all real |
+ // usages. |
+ if (!inUsage && !inVarDefinition && currentExpressions != null) { |
+ var expressions = currentExpressions.expressions; |
+ var index = expressions.indexOf(node); |
+ assert(index >= 0); |
+ var def = varDefs[node.name]; |
+ if (def != null) { |
+ // Found a VarDefinition use it. |
+ _resolveVarUsage(currentExpressions.expressions, index, def); |
+ } else if (node.defaultValues.any((e) => e is VarUsage)) { |
+ // Don't have a VarDefinition need to use default values resolve all |
+ // default values. |
+ var terminalDefaults = []; |
+ for (var defaultValue in node.defaultValues) { |
+ terminalDefaults.addAll(resolveUsageTerminal(defaultValue)); |
+ } |
+ expressions.replaceRange(index, index + 1, terminalDefaults); |
+ } else { |
+ // No VarDefinition but default value is a terminal expression; use it. |
+ expressions.replaceRange(index, index + 1, node.defaultValues); |
+ } |
+ } |
+ |
+ inUsage = true; |
+ super.visitVarUsage(node); |
+ inUsage = false; |
+ } |
+ |
+ List<Expression> resolveUsageTerminal(VarUsage usage) { |
+ var result = []; |
+ |
+ var varDef = varDefs[usage.name]; |
+ var expressions; |
+ if (varDef == null) { |
+ // VarDefinition not found try the defaultValues. |
+ expressions = usage.defaultValues; |
+ } else { |
+ // Use the VarDefinition found. |
+ expressions = (varDef.expression as Expressions).expressions; |
+ } |
+ |
+ for (var expr in expressions) { |
+ if (expr is VarUsage) { |
+ // Get terminal value. |
+ result.addAll(resolveUsageTerminal(expr)); |
+ } |
+ } |
+ |
+ // We're at a terminal just return the VarDefinition expression. |
+ if (result.isEmpty && varDef != null) { |
+ result = (varDef.expression as Expressions).expressions; |
+ } |
+ |
+ return result; |
+ } |
+ |
+ _resolveVarUsage(List<Expressions> expressions, int index, |
+ VarDefinition def) { |
+ var defExpressions = (def.expression as Expressions).expressions; |
+ expressions.replaceRange(index, index + 1, defExpressions); |
+ } |
+} |
+ |
+/** Remove all var definitions. */ |
+class _RemoveVarDefinitions extends Visitor { |
+ void visitTree(StyleSheet tree) { |
+ visitStyleSheet(tree); |
+ } |
+ |
+ void visitStyleSheet(StyleSheet ss) { |
+ ss.topLevels.removeWhere((e) => e is VarDefinitionDirective); |
+ super.visitStyleSheet(ss); |
+ } |
+ |
+ void visitDeclarationGroup(DeclarationGroup node) { |
+ node.declarations.removeWhere((e) => e is VarDefinition); |
+ super.visitDeclarationGroup(node); |
+ } |
+} |
+ |
+/** |
+ * Process all selectors looking for a pseudo-element in a selector. If the |
+ * name is found in our list of known pseudo-elements. Known pseudo-elements |
+ * are built when parsing a component looking for an attribute named "pseudo". |
+ * The value of the pseudo attribute is the name of the custom pseudo-element. |
+ * The name is mangled so Dart/JS can't directly access the pseudo-element only |
+ * CSS can access a custom pseudo-element (and see issue #510, querying needs |
+ * access to custom pseudo-elements). |
+ * |
+ * Change the custom pseudo-element to be a child of the pseudo attribute's |
+ * mangled custom pseudo element name. e.g, |
+ * |
+ * .test::x-box |
+ * |
+ * would become: |
+ * |
+ * .test > *[pseudo="x-box_2"] |
+ */ |
+class _PseudoElementExpander extends Visitor { |
+ final Map<String, String> _pseudoElements; |
+ |
+ _PseudoElementExpander(this._pseudoElements); |
+ |
+ void visitTree(StyleSheet tree) => visitStyleSheet(tree); |
+ |
+ visitSelector(Selector node) { |
+ var selectors = node.simpleSelectorSequences; |
+ for (var index = 0; index < selectors.length; index++) { |
+ var selector = selectors[index].simpleSelector; |
+ if (selector is PseudoElementSelector) { |
+ if (_pseudoElements.containsKey(selector.name)) { |
+ // Pseudo Element is a custom element. |
+ var mangledName = _pseudoElements[selector.name]; |
+ |
+ var span = selectors[index].span; |
+ |
+ var attrSelector = new AttributeSelector( |
+ new Identifier('pseudo', span), css.TokenKind.EQUALS, |
+ mangledName, span); |
+ // The wildcard * namespace selector. |
+ var wildCard = new ElementSelector(new Wildcard(span), span); |
+ selectors[index] = new SimpleSelectorSequence(wildCard, span, |
+ css.TokenKind.COMBINATOR_GREATER); |
+ selectors.insert(++index, |
+ new SimpleSelectorSequence(attrSelector, span)); |
+ } |
+ } |
+ } |
+ } |
+} |
+ |
+List<UrlInfo> findImportsInStyleSheet(StyleSheet styleSheet, |
+ String packageRoot, UrlInfo inputUrl, Messages messages) { |
+ var visitor = new _CssImports(packageRoot, inputUrl, messages); |
+ visitor.visitTree(styleSheet); |
+ return visitor.urlInfos; |
+} |
+ |
+/** |
+ * Find any imports in the style sheet; normalize the style sheet href and |
+ * return a list of all fully qualified CSS files. |
+ */ |
+class _CssImports extends Visitor { |
+ final String packageRoot; |
+ |
+ /** Input url of the css file, used to normalize relative import urls. */ |
+ final UrlInfo inputUrl; |
+ |
+ /** List of all imported style sheets. */ |
+ final List<UrlInfo> urlInfos = []; |
+ |
+ final Messages _messages; |
+ |
+ _CssImports(this.packageRoot, this.inputUrl, this._messages); |
+ |
+ void visitTree(StyleSheet tree) { |
+ visitStyleSheet(tree); |
+ } |
+ |
+ void visitImportDirective(ImportDirective node) { |
+ var urlInfo = UrlInfo.resolve(node.import, inputUrl, |
+ node.span, packageRoot, _messages, ignoreAbsolute: true); |
+ if (urlInfo == null) return; |
+ urlInfos.add(urlInfo); |
+ } |
+} |
+ |
+StyleSheet parseCss(String content, Messages messages, |
+ CompilerOptions options) { |
+ if (content.trim().isEmpty) return null; |
+ |
+ var errors = []; |
+ |
+ // TODO(terry): Add --checked when fully implemented and error handling. |
+ var stylesheet = css.parse(content, errors: errors, options: |
+ [options.warningsAsErrors ? '--warnings_as_errors' : '', 'memory']); |
+ |
+ // Note: errors aren't fatal in HTML (unless strict mode is on). |
+ // So just print them as warnings. |
+ for (var e in errors) { |
+ messages.warning(e.message, e.span); |
+ } |
+ |
+ return stylesheet; |
+} |
+ |
+/** Find terminal definition (non VarUsage implies real CSS value). */ |
+VarDefinition _findTerminalVarDefinition(Map<String, VarDefinition> varDefs, |
+ VarDefinition varDef) { |
+ var expressions = varDef.expression as Expressions; |
+ for (var expr in expressions.expressions) { |
+ if (expr is VarUsage) { |
+ var usageName = (expr as VarUsage).name; |
+ var foundDef = varDefs[usageName]; |
+ |
+ // If foundDef is unknown check if defaultValues; if it exist then resolve |
+ // to terminal value. |
+ if (foundDef == null) { |
+ // We're either a VarUsage or terminal definition if in varDefs; |
+ // either way replace VarUsage with it's default value because the |
+ // VarDefinition isn't found. |
+ var defaultValues = (expr as VarUsage).defaultValues; |
+ var replaceExprs = expressions.expressions; |
+ assert(replaceExprs.length == 1); |
+ replaceExprs.replaceRange(0, 1, defaultValues); |
+ return varDef; |
+ } |
+ if (foundDef is VarDefinition) { |
+ return _findTerminalVarDefinition(varDefs, foundDef); |
+ } |
+ } else { |
+ // Return real CSS property. |
+ return varDef; |
+ } |
+ } |
+ |
+ // Didn't point to a var definition that existed. |
+ return varDef; |
+} |
+ |
+/** |
+ * Find urls imported inside style tags under [info]. If [info] is a FileInfo |
+ * then process only style tags in the body (don't process any style tags in a |
+ * component). If [info] is a ComponentInfo only process style tags inside of |
+ * the element are processed. For an [info] of type FileInfo [node] is the |
+ * file's document and for an [info] of type ComponentInfo then [node] is the |
+ * component's element tag. |
+ */ |
+List<UrlInfo> findUrlsImported(LibraryInfo info, UrlInfo inputUrl, |
+ String packageRoot, Node node, Messages messages, CompilerOptions options) { |
+ // Process any @imports inside of the <style> tag. |
+ var styleProcessor = |
+ new _CssStyleTag(packageRoot, info, inputUrl, messages, options); |
+ styleProcessor.visit(node); |
+ return styleProcessor.imports; |
+} |
+ |
+/* Process CSS inside of a style tag. */ |
+class _CssStyleTag extends TreeVisitor { |
+ final String _packageRoot; |
+ |
+ /** Either a FileInfo or ComponentInfo. */ |
+ final LibraryInfo _info; |
+ final Messages _messages; |
+ final CompilerOptions _options; |
+ |
+ /** |
+ * Path of the declaring file, for a [_info] of type FileInfo it's the file's |
+ * path for a type ComponentInfo it's the declaring file path. |
+ */ |
+ final UrlInfo _inputUrl; |
+ |
+ /** List of @imports found. */ |
+ List<UrlInfo> imports = []; |
+ |
+ _CssStyleTag(this._packageRoot, this._info, this._inputUrl, this._messages, |
+ this._options); |
+ |
+ void visitElement(Element node) { |
+ // Don't process any style tags inside of element if we're processing a |
+ // FileInfo. The style tags inside of a component defintion will be |
+ // processed when _info is a ComponentInfo. |
+ if (node.tagName == 'polymer-element' && _info is FileInfo) return; |
+ if (node.tagName == 'style') { |
+ // Parse the contents of the scoped style tag. |
+ var styleSheet = parseCss(node.nodes.single.value, _messages, _options); |
+ if (styleSheet != null) { |
+ _info.styleSheets.add(styleSheet); |
+ |
+ // Find all imports return list of @imports in this style tag. |
+ var urlInfos = findImportsInStyleSheet(styleSheet, _packageRoot, |
+ _inputUrl, _messages); |
+ imports.addAll(urlInfos); |
+ } |
+ } |
+ super.visitElement(node); |
+ } |
+} |