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