OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /** Portion of the analyzer dealing with CSS sources. */ |
| 6 library polymer.src.css_analyzer; |
| 7 |
| 8 import 'package:csslib/parser.dart' as css; |
| 9 import 'package:csslib/visitor.dart'; |
| 10 import 'package:html5lib/dom.dart'; |
| 11 import 'package:html5lib/dom_parsing.dart'; |
| 12 |
| 13 import 'info.dart'; |
| 14 import 'files.dart' show SourceFile; |
| 15 import 'messages.dart'; |
| 16 import 'compiler_options.dart'; |
| 17 |
| 18 void analyzeCss(String packageRoot, List<SourceFile> files, |
| 19 Map<String, FileInfo> info, Map<String, String> pseudoElements, |
| 20 Messages messages, {warningsAsErrors: false}) { |
| 21 var analyzer = new _AnalyzerCss(packageRoot, info, pseudoElements, messages, |
| 22 warningsAsErrors); |
| 23 for (var file in files) analyzer.process(file); |
| 24 analyzer.normalize(); |
| 25 } |
| 26 |
| 27 class _AnalyzerCss { |
| 28 final String packageRoot; |
| 29 final Map<String, FileInfo> info; |
| 30 final Map<String, String> _pseudoElements; |
| 31 final Messages _messages; |
| 32 final bool _warningsAsErrors; |
| 33 |
| 34 Set<StyleSheet> allStyleSheets = new Set<StyleSheet>(); |
| 35 |
| 36 /** |
| 37 * [_pseudoElements] list of known pseudo attributes found in HTML, any |
| 38 * CSS pseudo-elements 'name::custom-element' is mapped to the manged name |
| 39 * associated with the pseudo-element key. |
| 40 */ |
| 41 _AnalyzerCss(this.packageRoot, this.info, this._pseudoElements, |
| 42 this._messages, this._warningsAsErrors); |
| 43 |
| 44 /** |
| 45 * Run the analyzer on every file that is a style sheet or any component that |
| 46 * has a style tag. |
| 47 */ |
| 48 void process(SourceFile file) { |
| 49 var fileInfo = info[file.path]; |
| 50 if (file.isStyleSheet || fileInfo.styleSheets.length > 0) { |
| 51 var styleSheets = processVars(fileInfo); |
| 52 |
| 53 // Add to list of all style sheets analyzed. |
| 54 allStyleSheets.addAll(styleSheets); |
| 55 } |
| 56 |
| 57 // Process any components. |
| 58 for (var component in fileInfo.declaredComponents) { |
| 59 var all = processVars(component); |
| 60 |
| 61 // Add to list of all style sheets analyzed. |
| 62 allStyleSheets.addAll(all); |
| 63 } |
| 64 |
| 65 processCustomPseudoElements(); |
| 66 } |
| 67 |
| 68 void normalize() { |
| 69 // Remove all var definitions for all style sheets analyzed. |
| 70 for (var tree in allStyleSheets) new _RemoveVarDefinitions().visitTree(tree)
; |
| 71 } |
| 72 |
| 73 List<StyleSheet> processVars(var libraryInfo) { |
| 74 // Get list of all stylesheet(s) dependencies referenced from this file. |
| 75 var styleSheets = _dependencies(libraryInfo).toList(); |
| 76 |
| 77 var errors = []; |
| 78 css.analyze(styleSheets, errors: errors, options: |
| 79 [_warningsAsErrors ? '--warnings_as_errors' : '', 'memory']); |
| 80 |
| 81 // Print errors as warnings. |
| 82 for (var e in errors) { |
| 83 _messages.warning(e.message, e.span); |
| 84 } |
| 85 |
| 86 // Build list of all var definitions. |
| 87 Map varDefs = new Map(); |
| 88 for (var tree in styleSheets) { |
| 89 var allDefs = (new _VarDefinitions()..visitTree(tree)).found; |
| 90 allDefs.forEach((key, value) { |
| 91 varDefs[key] = value; |
| 92 }); |
| 93 } |
| 94 |
| 95 // Resolve all definitions to a non-VarUsage (terminal expression). |
| 96 varDefs.forEach((key, value) { |
| 97 for (var expr in (value.expression as Expressions).expressions) { |
| 98 var def = _findTerminalVarDefinition(varDefs, value); |
| 99 varDefs[key] = def; |
| 100 } |
| 101 }); |
| 102 |
| 103 // Resolve all var usages. |
| 104 for (var tree in styleSheets) new _ResolveVarUsages(varDefs).visitTree(tree)
; |
| 105 |
| 106 return styleSheets; |
| 107 } |
| 108 |
| 109 processCustomPseudoElements() { |
| 110 var polyFiller = new _PseudoElementExpander(_pseudoElements); |
| 111 for (var tree in allStyleSheets) { |
| 112 polyFiller.visitTree(tree); |
| 113 } |
| 114 } |
| 115 |
| 116 /** |
| 117 * Given a component or file check if any stylesheets referenced. If so then |
| 118 * return a list of all referenced stylesheet dependencies (@imports or <link |
| 119 * rel="stylesheet" ..>). |
| 120 */ |
| 121 Set<StyleSheet> _dependencies(var libraryInfo, {Set<StyleSheet> seen}) { |
| 122 if (seen == null) seen = new Set(); |
| 123 |
| 124 // Used to resolve all pathing information. |
| 125 var inputUrl = libraryInfo is FileInfo |
| 126 ? libraryInfo.inputUrl |
| 127 : (libraryInfo as ComponentInfo).declaringFile.inputUrl; |
| 128 |
| 129 for (var styleSheet in libraryInfo.styleSheets) { |
| 130 if (!seen.contains(styleSheet)) { |
| 131 // TODO(terry): VM uses expandos to implement hashes. Currently, it's a |
| 132 // linear (not constant) time cost (see dartbug.com/5746). |
| 133 // If this bug isn't fixed and performance show's this a |
| 134 // a problem we'll need to implement our own hashCode or |
| 135 // use a different key for better perf. |
| 136 // Add the stylesheet. |
| 137 seen.add(styleSheet); |
| 138 |
| 139 // Any other imports in this stylesheet? |
| 140 var urlInfos = findImportsInStyleSheet(styleSheet, packageRoot, |
| 141 inputUrl, _messages); |
| 142 |
| 143 // Process other imports in this stylesheets. |
| 144 for (var importSS in urlInfos) { |
| 145 var importInfo = info[importSS.resolvedPath]; |
| 146 if (importInfo != null) { |
| 147 // Add all known stylesheets processed. |
| 148 seen.addAll(importInfo.styleSheets); |
| 149 // Find dependencies for stylesheet referenced with a |
| 150 // @import |
| 151 for (var ss in importInfo.styleSheets) { |
| 152 var urls = findImportsInStyleSheet(ss, packageRoot, inputUrl, |
| 153 _messages); |
| 154 for (var url in urls) { |
| 155 _dependencies(info[url.resolvedPath], seen: seen); |
| 156 } |
| 157 } |
| 158 } |
| 159 } |
| 160 } |
| 161 } |
| 162 |
| 163 return seen; |
| 164 } |
| 165 } |
| 166 |
| 167 /** |
| 168 * Find var- definitions in a style sheet. |
| 169 * [found] list of known definitions. |
| 170 */ |
| 171 class _VarDefinitions extends Visitor { |
| 172 final Map<String, VarDefinition> found = new Map(); |
| 173 |
| 174 void visitTree(StyleSheet tree) { |
| 175 visitStyleSheet(tree); |
| 176 } |
| 177 |
| 178 visitVarDefinition(VarDefinition node) { |
| 179 // Replace with latest variable definition. |
| 180 found[node.definedName] = node; |
| 181 super.visitVarDefinition(node); |
| 182 } |
| 183 |
| 184 void visitVarDefinitionDirective(VarDefinitionDirective node) { |
| 185 visitVarDefinition(node.def); |
| 186 } |
| 187 } |
| 188 |
| 189 /** |
| 190 * Resolve any CSS expression which contains a var() usage to the ultimate real |
| 191 * CSS expression value e.g., |
| 192 * |
| 193 * var-one: var(two); |
| 194 * var-two: #ff00ff; |
| 195 * |
| 196 * .test { |
| 197 * color: var(one); |
| 198 * } |
| 199 * |
| 200 * then .test's color would be #ff00ff |
| 201 */ |
| 202 class _ResolveVarUsages extends Visitor { |
| 203 final Map<String, VarDefinition> varDefs; |
| 204 bool inVarDefinition = false; |
| 205 bool inUsage = false; |
| 206 Expressions currentExpressions; |
| 207 |
| 208 _ResolveVarUsages(this.varDefs); |
| 209 |
| 210 void visitTree(StyleSheet tree) { |
| 211 visitStyleSheet(tree); |
| 212 } |
| 213 |
| 214 void visitVarDefinition(VarDefinition varDef) { |
| 215 inVarDefinition = true; |
| 216 super.visitVarDefinition(varDef); |
| 217 inVarDefinition = false; |
| 218 } |
| 219 |
| 220 void visitExpressions(Expressions node) { |
| 221 currentExpressions = node; |
| 222 super.visitExpressions(node); |
| 223 currentExpressions = null; |
| 224 } |
| 225 |
| 226 void visitVarUsage(VarUsage node) { |
| 227 // Don't process other var() inside of a varUsage. That implies that the |
| 228 // default is a var() too. Also, don't process any var() inside of a |
| 229 // varDefinition (they're just place holders until we've resolved all real |
| 230 // usages. |
| 231 if (!inUsage && !inVarDefinition && currentExpressions != null) { |
| 232 var expressions = currentExpressions.expressions; |
| 233 var index = expressions.indexOf(node); |
| 234 assert(index >= 0); |
| 235 var def = varDefs[node.name]; |
| 236 if (def != null) { |
| 237 // Found a VarDefinition use it. |
| 238 _resolveVarUsage(currentExpressions.expressions, index, def); |
| 239 } else if (node.defaultValues.any((e) => e is VarUsage)) { |
| 240 // Don't have a VarDefinition need to use default values resolve all |
| 241 // default values. |
| 242 var terminalDefaults = []; |
| 243 for (var defaultValue in node.defaultValues) { |
| 244 terminalDefaults.addAll(resolveUsageTerminal(defaultValue)); |
| 245 } |
| 246 expressions.replaceRange(index, index + 1, terminalDefaults); |
| 247 } else { |
| 248 // No VarDefinition but default value is a terminal expression; use it. |
| 249 expressions.replaceRange(index, index + 1, node.defaultValues); |
| 250 } |
| 251 } |
| 252 |
| 253 inUsage = true; |
| 254 super.visitVarUsage(node); |
| 255 inUsage = false; |
| 256 } |
| 257 |
| 258 List<Expression> resolveUsageTerminal(VarUsage usage) { |
| 259 var result = []; |
| 260 |
| 261 var varDef = varDefs[usage.name]; |
| 262 var expressions; |
| 263 if (varDef == null) { |
| 264 // VarDefinition not found try the defaultValues. |
| 265 expressions = usage.defaultValues; |
| 266 } else { |
| 267 // Use the VarDefinition found. |
| 268 expressions = (varDef.expression as Expressions).expressions; |
| 269 } |
| 270 |
| 271 for (var expr in expressions) { |
| 272 if (expr is VarUsage) { |
| 273 // Get terminal value. |
| 274 result.addAll(resolveUsageTerminal(expr)); |
| 275 } |
| 276 } |
| 277 |
| 278 // We're at a terminal just return the VarDefinition expression. |
| 279 if (result.isEmpty && varDef != null) { |
| 280 result = (varDef.expression as Expressions).expressions; |
| 281 } |
| 282 |
| 283 return result; |
| 284 } |
| 285 |
| 286 _resolveVarUsage(List<Expressions> expressions, int index, |
| 287 VarDefinition def) { |
| 288 var defExpressions = (def.expression as Expressions).expressions; |
| 289 expressions.replaceRange(index, index + 1, defExpressions); |
| 290 } |
| 291 } |
| 292 |
| 293 /** Remove all var definitions. */ |
| 294 class _RemoveVarDefinitions extends Visitor { |
| 295 void visitTree(StyleSheet tree) { |
| 296 visitStyleSheet(tree); |
| 297 } |
| 298 |
| 299 void visitStyleSheet(StyleSheet ss) { |
| 300 ss.topLevels.removeWhere((e) => e is VarDefinitionDirective); |
| 301 super.visitStyleSheet(ss); |
| 302 } |
| 303 |
| 304 void visitDeclarationGroup(DeclarationGroup node) { |
| 305 node.declarations.removeWhere((e) => e is VarDefinition); |
| 306 super.visitDeclarationGroup(node); |
| 307 } |
| 308 } |
| 309 |
| 310 /** |
| 311 * Process all selectors looking for a pseudo-element in a selector. If the |
| 312 * name is found in our list of known pseudo-elements. Known pseudo-elements |
| 313 * are built when parsing a component looking for an attribute named "pseudo". |
| 314 * The value of the pseudo attribute is the name of the custom pseudo-element. |
| 315 * The name is mangled so Dart/JS can't directly access the pseudo-element only |
| 316 * CSS can access a custom pseudo-element (and see issue #510, querying needs |
| 317 * access to custom pseudo-elements). |
| 318 * |
| 319 * Change the custom pseudo-element to be a child of the pseudo attribute's |
| 320 * mangled custom pseudo element name. e.g, |
| 321 * |
| 322 * .test::x-box |
| 323 * |
| 324 * would become: |
| 325 * |
| 326 * .test > *[pseudo="x-box_2"] |
| 327 */ |
| 328 class _PseudoElementExpander extends Visitor { |
| 329 final Map<String, String> _pseudoElements; |
| 330 |
| 331 _PseudoElementExpander(this._pseudoElements); |
| 332 |
| 333 void visitTree(StyleSheet tree) => visitStyleSheet(tree); |
| 334 |
| 335 visitSelector(Selector node) { |
| 336 var selectors = node.simpleSelectorSequences; |
| 337 for (var index = 0; index < selectors.length; index++) { |
| 338 var selector = selectors[index].simpleSelector; |
| 339 if (selector is PseudoElementSelector) { |
| 340 if (_pseudoElements.containsKey(selector.name)) { |
| 341 // Pseudo Element is a custom element. |
| 342 var mangledName = _pseudoElements[selector.name]; |
| 343 |
| 344 var span = selectors[index].span; |
| 345 |
| 346 var attrSelector = new AttributeSelector( |
| 347 new Identifier('pseudo', span), css.TokenKind.EQUALS, |
| 348 mangledName, span); |
| 349 // The wildcard * namespace selector. |
| 350 var wildCard = new ElementSelector(new Wildcard(span), span); |
| 351 selectors[index] = new SimpleSelectorSequence(wildCard, span, |
| 352 css.TokenKind.COMBINATOR_GREATER); |
| 353 selectors.insert(++index, |
| 354 new SimpleSelectorSequence(attrSelector, span)); |
| 355 } |
| 356 } |
| 357 } |
| 358 } |
| 359 } |
| 360 |
| 361 List<UrlInfo> findImportsInStyleSheet(StyleSheet styleSheet, |
| 362 String packageRoot, UrlInfo inputUrl, Messages messages) { |
| 363 var visitor = new _CssImports(packageRoot, inputUrl, messages); |
| 364 visitor.visitTree(styleSheet); |
| 365 return visitor.urlInfos; |
| 366 } |
| 367 |
| 368 /** |
| 369 * Find any imports in the style sheet; normalize the style sheet href and |
| 370 * return a list of all fully qualified CSS files. |
| 371 */ |
| 372 class _CssImports extends Visitor { |
| 373 final String packageRoot; |
| 374 |
| 375 /** Input url of the css file, used to normalize relative import urls. */ |
| 376 final UrlInfo inputUrl; |
| 377 |
| 378 /** List of all imported style sheets. */ |
| 379 final List<UrlInfo> urlInfos = []; |
| 380 |
| 381 final Messages _messages; |
| 382 |
| 383 _CssImports(this.packageRoot, this.inputUrl, this._messages); |
| 384 |
| 385 void visitTree(StyleSheet tree) { |
| 386 visitStyleSheet(tree); |
| 387 } |
| 388 |
| 389 void visitImportDirective(ImportDirective node) { |
| 390 var urlInfo = UrlInfo.resolve(node.import, inputUrl, |
| 391 node.span, packageRoot, _messages, ignoreAbsolute: true); |
| 392 if (urlInfo == null) return; |
| 393 urlInfos.add(urlInfo); |
| 394 } |
| 395 } |
| 396 |
| 397 StyleSheet parseCss(String content, Messages messages, |
| 398 CompilerOptions options) { |
| 399 if (content.trim().isEmpty) return null; |
| 400 |
| 401 var errors = []; |
| 402 |
| 403 // TODO(terry): Add --checked when fully implemented and error handling. |
| 404 var stylesheet = css.parse(content, errors: errors, options: |
| 405 [options.warningsAsErrors ? '--warnings_as_errors' : '', 'memory']); |
| 406 |
| 407 // Note: errors aren't fatal in HTML (unless strict mode is on). |
| 408 // So just print them as warnings. |
| 409 for (var e in errors) { |
| 410 messages.warning(e.message, e.span); |
| 411 } |
| 412 |
| 413 return stylesheet; |
| 414 } |
| 415 |
| 416 /** Find terminal definition (non VarUsage implies real CSS value). */ |
| 417 VarDefinition _findTerminalVarDefinition(Map<String, VarDefinition> varDefs, |
| 418 VarDefinition varDef) { |
| 419 var expressions = varDef.expression as Expressions; |
| 420 for (var expr in expressions.expressions) { |
| 421 if (expr is VarUsage) { |
| 422 var usageName = (expr as VarUsage).name; |
| 423 var foundDef = varDefs[usageName]; |
| 424 |
| 425 // If foundDef is unknown check if defaultValues; if it exist then resolve |
| 426 // to terminal value. |
| 427 if (foundDef == null) { |
| 428 // We're either a VarUsage or terminal definition if in varDefs; |
| 429 // either way replace VarUsage with it's default value because the |
| 430 // VarDefinition isn't found. |
| 431 var defaultValues = (expr as VarUsage).defaultValues; |
| 432 var replaceExprs = expressions.expressions; |
| 433 assert(replaceExprs.length == 1); |
| 434 replaceExprs.replaceRange(0, 1, defaultValues); |
| 435 return varDef; |
| 436 } |
| 437 if (foundDef is VarDefinition) { |
| 438 return _findTerminalVarDefinition(varDefs, foundDef); |
| 439 } |
| 440 } else { |
| 441 // Return real CSS property. |
| 442 return varDef; |
| 443 } |
| 444 } |
| 445 |
| 446 // Didn't point to a var definition that existed. |
| 447 return varDef; |
| 448 } |
| 449 |
| 450 /** |
| 451 * Find urls imported inside style tags under [info]. If [info] is a FileInfo |
| 452 * then process only style tags in the body (don't process any style tags in a |
| 453 * component). If [info] is a ComponentInfo only process style tags inside of |
| 454 * the element are processed. For an [info] of type FileInfo [node] is the |
| 455 * file's document and for an [info] of type ComponentInfo then [node] is the |
| 456 * component's element tag. |
| 457 */ |
| 458 List<UrlInfo> findUrlsImported(LibraryInfo info, UrlInfo inputUrl, |
| 459 String packageRoot, Node node, Messages messages, CompilerOptions options) { |
| 460 // Process any @imports inside of the <style> tag. |
| 461 var styleProcessor = |
| 462 new _CssStyleTag(packageRoot, info, inputUrl, messages, options); |
| 463 styleProcessor.visit(node); |
| 464 return styleProcessor.imports; |
| 465 } |
| 466 |
| 467 /* Process CSS inside of a style tag. */ |
| 468 class _CssStyleTag extends TreeVisitor { |
| 469 final String _packageRoot; |
| 470 |
| 471 /** Either a FileInfo or ComponentInfo. */ |
| 472 final LibraryInfo _info; |
| 473 final Messages _messages; |
| 474 final CompilerOptions _options; |
| 475 |
| 476 /** |
| 477 * Path of the declaring file, for a [_info] of type FileInfo it's the file's |
| 478 * path for a type ComponentInfo it's the declaring file path. |
| 479 */ |
| 480 final UrlInfo _inputUrl; |
| 481 |
| 482 /** List of @imports found. */ |
| 483 List<UrlInfo> imports = []; |
| 484 |
| 485 _CssStyleTag(this._packageRoot, this._info, this._inputUrl, this._messages, |
| 486 this._options); |
| 487 |
| 488 void visitElement(Element node) { |
| 489 // Don't process any style tags inside of element if we're processing a |
| 490 // FileInfo. The style tags inside of a component defintion will be |
| 491 // processed when _info is a ComponentInfo. |
| 492 if (node.tagName == 'polymer-element' && _info is FileInfo) return; |
| 493 if (node.tagName == 'style') { |
| 494 // Parse the contents of the scoped style tag. |
| 495 var styleSheet = parseCss(node.nodes.single.value, _messages, _options); |
| 496 if (styleSheet != null) { |
| 497 _info.styleSheets.add(styleSheet); |
| 498 |
| 499 // Find all imports return list of @imports in this style tag. |
| 500 var urlInfos = findImportsInStyleSheet(styleSheet, _packageRoot, |
| 501 _inputUrl, _messages); |
| 502 imports.addAll(urlInfos); |
| 503 } |
| 504 } |
| 505 super.visitElement(node); |
| 506 } |
| 507 } |
OLD | NEW |