| 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 /** |  | 
| 6  * Part of the template compilation that concerns with extracting information |  | 
| 7  * from the HTML parse tree. |  | 
| 8  */ |  | 
| 9 library analyzer; |  | 
| 10 |  | 
| 11 import 'package:html5lib/dom.dart'; |  | 
| 12 import 'package:html5lib/dom_parsing.dart'; |  | 
| 13 |  | 
| 14 import 'custom_tag_name.dart'; |  | 
| 15 import 'files.dart'; |  | 
| 16 import 'info.dart'; |  | 
| 17 import 'messages.dart'; |  | 
| 18 |  | 
| 19 /** |  | 
| 20  * Finds custom elements in this file and the list of referenced files with |  | 
| 21  * component declarations. This is the first pass of analysis on a file. |  | 
| 22  * |  | 
| 23  * Adds emitted error/warning messages to [messages], if [messages] is |  | 
| 24  * supplied. |  | 
| 25  */ |  | 
| 26 FileInfo analyzeDefinitions(GlobalInfo global, UrlInfo inputUrl, |  | 
| 27     Document document, String packageRoot, Messages messages) { |  | 
| 28   var result = new FileInfo(inputUrl); |  | 
| 29   var loader = new _ElementLoader(global, result, packageRoot, messages); |  | 
| 30   loader.visit(document); |  | 
| 31   return result; |  | 
| 32 } |  | 
| 33 |  | 
| 34 /** |  | 
| 35  *  Extract relevant information from all files found from the root document. |  | 
| 36  * |  | 
| 37  *  Adds emitted error/warning messages to [messages], if [messages] is |  | 
| 38  *  supplied. |  | 
| 39  */ |  | 
| 40 void analyzeFile(SourceFile file, Map<String, FileInfo> info, |  | 
| 41                  Iterator<int> uniqueIds, GlobalInfo global, |  | 
| 42                  Messages messages, emulateScopedCss) { |  | 
| 43   var fileInfo = info[file.path]; |  | 
| 44   var analyzer = new _Analyzer(fileInfo, uniqueIds, global, messages, |  | 
| 45       emulateScopedCss); |  | 
| 46   analyzer._normalize(fileInfo, info); |  | 
| 47   analyzer.visit(file.document); |  | 
| 48 } |  | 
| 49 |  | 
| 50 |  | 
| 51 /** A visitor that walks the HTML to extract all the relevant information. */ |  | 
| 52 class _Analyzer extends TreeVisitor { |  | 
| 53   final FileInfo _fileInfo; |  | 
| 54   LibraryInfo _currentInfo; |  | 
| 55   Iterator<int> _uniqueIds; |  | 
| 56   GlobalInfo _global; |  | 
| 57   Messages _messages; |  | 
| 58 |  | 
| 59   int _generatedClassNumber = 0; |  | 
| 60 |  | 
| 61   /** |  | 
| 62    * Whether to keep indentation spaces. Break lines and indentation spaces |  | 
| 63    * within templates are preserved in HTML. When users specify the attribute |  | 
| 64    * 'indentation="remove"' on a template tag, we'll trim those indentation |  | 
| 65    * spaces that occur within that tag and its decendants. If any decendant |  | 
| 66    * specifies 'indentation="preserve"', then we'll switch back to the normal |  | 
| 67    * behavior. |  | 
| 68    */ |  | 
| 69   bool _keepIndentationSpaces = true; |  | 
| 70 |  | 
| 71   final bool _emulateScopedCss; |  | 
| 72 |  | 
| 73   _Analyzer(this._fileInfo, this._uniqueIds, this._global, this._messages, |  | 
| 74       this._emulateScopedCss) { |  | 
| 75     _currentInfo = _fileInfo; |  | 
| 76   } |  | 
| 77 |  | 
| 78   void visitElement(Element node) { |  | 
| 79     if (node.tagName == 'script') { |  | 
| 80       // We already extracted script tags in previous phase. |  | 
| 81       return; |  | 
| 82     } |  | 
| 83 |  | 
| 84     if (node.tagName == 'style') { |  | 
| 85       // We've already parsed the CSS. |  | 
| 86       // If this is a component remove the style node. |  | 
| 87       if (_currentInfo is ComponentInfo && _emulateScopedCss) node.remove(); |  | 
| 88       return; |  | 
| 89     } |  | 
| 90 |  | 
| 91     _bindCustomElement(node); |  | 
| 92 |  | 
| 93     var lastInfo = _currentInfo; |  | 
| 94     if (node.tagName == 'polymer-element') { |  | 
| 95       // If element is invalid _ElementLoader already reported an error, but |  | 
| 96       // we skip the body of the element here. |  | 
| 97       var name = node.attributes['name']; |  | 
| 98       if (name == null) return; |  | 
| 99 |  | 
| 100       ComponentInfo component = _fileInfo.components[name]; |  | 
| 101       if (component == null) return; |  | 
| 102 |  | 
| 103       _analyzeComponent(component); |  | 
| 104 |  | 
| 105       _currentInfo = component; |  | 
| 106 |  | 
| 107       // Remove the <element> tag from the tree |  | 
| 108       node.remove(); |  | 
| 109     } |  | 
| 110 |  | 
| 111     node.attributes.forEach((name, value) { |  | 
| 112       if (name.startsWith('on')) { |  | 
| 113         _validateEventHandler(node, name, value); |  | 
| 114       } else  if (name == 'pseudo' && _currentInfo is ComponentInfo) { |  | 
| 115         // Any component's custom pseudo-element(s) defined? |  | 
| 116         _processPseudoAttribute(node, value.split(' ')); |  | 
| 117       } |  | 
| 118     }); |  | 
| 119 |  | 
| 120     var keepSpaces = _keepIndentationSpaces; |  | 
| 121     if (node.tagName == 'template' && |  | 
| 122         node.attributes.containsKey('indentation')) { |  | 
| 123       var value = node.attributes['indentation']; |  | 
| 124       if (value != 'remove' && value != 'preserve') { |  | 
| 125         _messages.warning( |  | 
| 126             "Invalid value for 'indentation' ($value). By default we preserve " |  | 
| 127             "the indentation. Valid values are either 'remove' or 'preserve'.", |  | 
| 128             node.sourceSpan); |  | 
| 129       } |  | 
| 130       _keepIndentationSpaces = value != 'remove'; |  | 
| 131     } |  | 
| 132 |  | 
| 133     // Invoke super to visit children. |  | 
| 134     super.visitElement(node); |  | 
| 135 |  | 
| 136     _keepIndentationSpaces = keepSpaces; |  | 
| 137     _currentInfo = lastInfo; |  | 
| 138   } |  | 
| 139 |  | 
| 140   void _analyzeComponent(ComponentInfo component) { |  | 
| 141     var baseTag = component.extendsTag; |  | 
| 142     component.extendsComponent = baseTag == null ? null |  | 
| 143         : _fileInfo.components[baseTag]; |  | 
| 144     if (component.extendsComponent == null && isCustomTag(baseTag)) { |  | 
| 145       _messages.warning( |  | 
| 146           'custom element with tag name ${component.extendsTag} not found.', |  | 
| 147           component.element.sourceSpan); |  | 
| 148     } |  | 
| 149   } |  | 
| 150 |  | 
| 151   void _bindCustomElement(Element node) { |  | 
| 152     // <fancy-button> |  | 
| 153     var component = _fileInfo.components[node.tagName]; |  | 
| 154     if (component == null) { |  | 
| 155       // TODO(jmesserly): warn for unknown element tags? |  | 
| 156 |  | 
| 157       // <button is="fancy-button"> |  | 
| 158       var componentName = node.attributes['is']; |  | 
| 159       if (componentName != null) { |  | 
| 160         component = _fileInfo.components[componentName]; |  | 
| 161       } else if (isCustomTag(node.tagName)) { |  | 
| 162         componentName = node.tagName; |  | 
| 163       } |  | 
| 164       if (component == null && componentName != null && |  | 
| 165           componentName != 'polymer-element') { |  | 
| 166         _messages.warning( |  | 
| 167             'custom element with tag name $componentName not found.', |  | 
| 168             node.sourceSpan); |  | 
| 169       } |  | 
| 170     } |  | 
| 171 |  | 
| 172     if (component != null) { |  | 
| 173       var baseTag = component.baseExtendsTag; |  | 
| 174       var nodeTag = node.tagName; |  | 
| 175       var hasIsAttribute = node.attributes.containsKey('is'); |  | 
| 176 |  | 
| 177       if (baseTag != null && !hasIsAttribute) { |  | 
| 178         _messages.warning( |  | 
| 179             'custom element "${component.tagName}" extends from "$baseTag", but' |  | 
| 180             ' this tag will not include the default properties of "$baseTag". ' |  | 
| 181             'To fix this, either write this tag as <$baseTag ' |  | 
| 182             'is="${component.tagName}"> or remove the "extends" attribute from ' |  | 
| 183             'the custom element declaration.', node.sourceSpan); |  | 
| 184       } else if (hasIsAttribute) { |  | 
| 185         if (baseTag == null) { |  | 
| 186           _messages.warning( |  | 
| 187               'custom element "${component.tagName}" doesn\'t declare any type ' |  | 
| 188               'extensions. To fix this, either rewrite this tag as ' |  | 
| 189               '<${component.tagName}> or add \'extends="$nodeTag"\' to ' |  | 
| 190               'the custom element declaration.', node.sourceSpan); |  | 
| 191         } else if (baseTag != nodeTag) { |  | 
| 192           _messages.warning( |  | 
| 193               'custom element "${component.tagName}" extends from "$baseTag". ' |  | 
| 194               'Did you mean to write <$baseTag is="${component.tagName}">?', |  | 
| 195               node.sourceSpan); |  | 
| 196         } |  | 
| 197       } |  | 
| 198     } |  | 
| 199   } |  | 
| 200 |  | 
| 201   void _processPseudoAttribute(Node node, List<String> values) { |  | 
| 202     List mangledValues = []; |  | 
| 203     for (var pseudoElement in values) { |  | 
| 204       if (_global.pseudoElements.containsKey(pseudoElement)) continue; |  | 
| 205 |  | 
| 206       _uniqueIds.moveNext(); |  | 
| 207       var newValue = "${pseudoElement}_${_uniqueIds.current}"; |  | 
| 208       _global.pseudoElements[pseudoElement] = newValue; |  | 
| 209       // Mangled name of pseudo-element. |  | 
| 210       mangledValues.add(newValue); |  | 
| 211 |  | 
| 212       if (!pseudoElement.startsWith('x-')) { |  | 
| 213         // TODO(terry): The name must start with x- otherwise it's not a custom |  | 
| 214         //              pseudo-element.  May want to relax since components no |  | 
| 215         //              longer need to start with x-.  See isse #509 on |  | 
| 216         //              pseudo-element prefix. |  | 
| 217         _messages.warning("Custom pseudo-element must be prefixed with 'x-'.", |  | 
| 218             node.sourceSpan); |  | 
| 219       } |  | 
| 220     } |  | 
| 221 |  | 
| 222     // Update the pseudo attribute with the new mangled names. |  | 
| 223     node.attributes['pseudo'] = mangledValues.join(' '); |  | 
| 224   } |  | 
| 225 |  | 
| 226   /** |  | 
| 227    * Support for inline event handlers that take expressions. |  | 
| 228    * For example: `on-double-click=myHandler($event, todo)`. |  | 
| 229    */ |  | 
| 230   void _validateEventHandler(Element node, String name, String value) { |  | 
| 231     if (!name.startsWith('on-')) { |  | 
| 232       // TODO(jmesserly): do we need an option to suppress this warning? |  | 
| 233       _messages.warning('Event handler $name will be interpreted as an inline ' |  | 
| 234           'JavaScript event handler. Use the form ' |  | 
| 235           'on-event-name="handlerName" if you want a Dart handler ' |  | 
| 236           'that will automatically update the UI based on model changes.', |  | 
| 237           node.sourceSpan); |  | 
| 238     } |  | 
| 239 |  | 
| 240     if (value.contains('.') || value.contains('(')) { |  | 
| 241       // TODO(sigmund): should we allow more if we use fancy-syntax? |  | 
| 242       _messages.warning('Invalid event handler body "$value". Declare a method ' |  | 
| 243           'in your custom element "void handlerName(event, detail, target)" ' |  | 
| 244           'and use the form on-event-name="handlerName".', |  | 
| 245           node.sourceSpan); |  | 
| 246     } |  | 
| 247   } |  | 
| 248 |  | 
| 249   /** |  | 
| 250    * Normalizes references in [info]. On the [analyzeDefinitions] phase, the |  | 
| 251    * analyzer extracted names of files and components. Here we link those names |  | 
| 252    * to actual info classes. In particular: |  | 
| 253    *   * we initialize the [FileInfo.components] map in [info] by importing all |  | 
| 254    *     [declaredComponents], |  | 
| 255    *   * we scan all [info.componentLinks] and import their |  | 
| 256    *     [info.declaredComponents], using [files] to map the href to the file |  | 
| 257    *     info. Names in [info] will shadow names from imported files. |  | 
| 258    */ |  | 
| 259   void _normalize(FileInfo info, Map<String, FileInfo> files) { |  | 
| 260     for (var component in info.declaredComponents) { |  | 
| 261       _addComponent(info, component); |  | 
| 262     } |  | 
| 263 |  | 
| 264     for (var link in info.componentLinks) { |  | 
| 265       var file = files[link.resolvedPath]; |  | 
| 266       // We already issued an error for missing files. |  | 
| 267       if (file == null) continue; |  | 
| 268       file.declaredComponents.forEach((c) => _addComponent(info, c)); |  | 
| 269     } |  | 
| 270   } |  | 
| 271 |  | 
| 272   /** Adds a component's tag name to the names in scope for [fileInfo]. */ |  | 
| 273   void _addComponent(FileInfo fileInfo, ComponentInfo component) { |  | 
| 274     var existing = fileInfo.components[component.tagName]; |  | 
| 275     if (existing != null) { |  | 
| 276       if (existing == component) { |  | 
| 277         // This is the same exact component as the existing one. |  | 
| 278         return; |  | 
| 279       } |  | 
| 280 |  | 
| 281       if (existing is ComponentInfo && component is! ComponentInfo) { |  | 
| 282         // Components declared in [fileInfo] shadow component names declared in |  | 
| 283         // imported files. |  | 
| 284         return; |  | 
| 285       } |  | 
| 286 |  | 
| 287       if (existing.hasConflict) { |  | 
| 288         // No need to report a second error for the same name. |  | 
| 289         return; |  | 
| 290       } |  | 
| 291 |  | 
| 292       existing.hasConflict = true; |  | 
| 293 |  | 
| 294       if (component is ComponentInfo) { |  | 
| 295         _messages.error('duplicate custom element definition for ' |  | 
| 296             '"${component.tagName}".', existing.sourceSpan); |  | 
| 297         _messages.error('duplicate custom element definition for ' |  | 
| 298             '"${component.tagName}" (second location).', component.sourceSpan); |  | 
| 299       } else { |  | 
| 300         _messages.error('imported duplicate custom element definitions ' |  | 
| 301             'for "${component.tagName}".', existing.sourceSpan); |  | 
| 302         _messages.error('imported duplicate custom element definitions ' |  | 
| 303             'for "${component.tagName}" (second location).', |  | 
| 304             component.sourceSpan); |  | 
| 305       } |  | 
| 306     } else { |  | 
| 307       fileInfo.components[component.tagName] = component; |  | 
| 308     } |  | 
| 309   } |  | 
| 310 } |  | 
| 311 |  | 
| 312 /** A visitor that finds `<link rel="import">` and `<element>` tags.  */ |  | 
| 313 class _ElementLoader extends TreeVisitor { |  | 
| 314   final GlobalInfo _global; |  | 
| 315   final FileInfo _fileInfo; |  | 
| 316   LibraryInfo _currentInfo; |  | 
| 317   String _packageRoot; |  | 
| 318   bool _inHead = false; |  | 
| 319   Messages _messages; |  | 
| 320 |  | 
| 321   /** |  | 
| 322    * Adds emitted warning/error messages to [_messages]. [_messages] |  | 
| 323    * must not be null. |  | 
| 324    */ |  | 
| 325   _ElementLoader(this._global, this._fileInfo, this._packageRoot, |  | 
| 326       this._messages) { |  | 
| 327     _currentInfo = _fileInfo; |  | 
| 328   } |  | 
| 329 |  | 
| 330   void visitElement(Element node) { |  | 
| 331     switch (node.tagName) { |  | 
| 332       case 'link': visitLinkElement(node); break; |  | 
| 333       case 'element': |  | 
| 334         _messages.warning('<element> elements are not supported, use' |  | 
| 335             ' <polymer-element> instead', node.sourceSpan); |  | 
| 336         break; |  | 
| 337       case 'polymer-element': |  | 
| 338          visitElementElement(node); |  | 
| 339          break; |  | 
| 340       case 'script': visitScriptElement(node); break; |  | 
| 341       case 'head': |  | 
| 342         var savedInHead = _inHead; |  | 
| 343         _inHead = true; |  | 
| 344         super.visitElement(node); |  | 
| 345         _inHead = savedInHead; |  | 
| 346         break; |  | 
| 347       default: super.visitElement(node); break; |  | 
| 348     } |  | 
| 349   } |  | 
| 350 |  | 
| 351   /** |  | 
| 352    * Process `link rel="import"` as specified in: |  | 
| 353    * <https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/components/index.ht
     ml#link-type-component> |  | 
| 354    */ |  | 
| 355   void visitLinkElement(Element node) { |  | 
| 356     var rel = node.attributes['rel']; |  | 
| 357     if (rel != 'component' && rel != 'components' && |  | 
| 358         rel != 'import' && rel != 'stylesheet') return; |  | 
| 359 |  | 
| 360     if (!_inHead) { |  | 
| 361       _messages.warning('link rel="$rel" only valid in ' |  | 
| 362           'head.', node.sourceSpan); |  | 
| 363       return; |  | 
| 364     } |  | 
| 365 |  | 
| 366     if (rel == 'component' || rel == 'components') { |  | 
| 367       _messages.warning('import syntax is changing, use ' |  | 
| 368           'rel="import" instead of rel="$rel".', node.sourceSpan); |  | 
| 369     } |  | 
| 370 |  | 
| 371     var href = node.attributes['href']; |  | 
| 372     if (href == null || href == '') { |  | 
| 373       _messages.warning('link rel="$rel" missing href.', |  | 
| 374           node.sourceSpan); |  | 
| 375       return; |  | 
| 376     } |  | 
| 377 |  | 
| 378     bool isStyleSheet = rel == 'stylesheet'; |  | 
| 379     var urlInfo = UrlInfo.resolve(href, _fileInfo.inputUrl, node.sourceSpan, |  | 
| 380         _packageRoot, _messages, ignoreAbsolute: isStyleSheet); |  | 
| 381     if (urlInfo == null) return; |  | 
| 382     if (isStyleSheet) { |  | 
| 383       _fileInfo.styleSheetHrefs.add(urlInfo); |  | 
| 384     } else { |  | 
| 385       _fileInfo.componentLinks.add(urlInfo); |  | 
| 386     } |  | 
| 387   } |  | 
| 388 |  | 
| 389   void visitElementElement(Element node) { |  | 
| 390     // TODO(jmesserly): what do we do in this case? It seems like an <element> |  | 
| 391     // inside a Shadow DOM should be scoped to that <template> tag, and not |  | 
| 392     // visible from the outside. |  | 
| 393     if (_currentInfo is ComponentInfo) { |  | 
| 394       _messages.error('Nested component definitions are not yet supported.', |  | 
| 395           node.sourceSpan); |  | 
| 396       return; |  | 
| 397     } |  | 
| 398 |  | 
| 399     var tagName = node.attributes['name']; |  | 
| 400     var extendsTag = node.attributes['extends']; |  | 
| 401 |  | 
| 402     if (tagName == null) { |  | 
| 403       _messages.error('Missing tag name of the component. Please include an ' |  | 
| 404           'attribute like \'name="your-tag-name"\'.', |  | 
| 405           node.sourceSpan); |  | 
| 406       return; |  | 
| 407     } |  | 
| 408 |  | 
| 409     var component = new ComponentInfo(node, tagName, extendsTag); |  | 
| 410     _fileInfo.declaredComponents.add(component); |  | 
| 411     _addComponent(component); |  | 
| 412 |  | 
| 413     var lastInfo = _currentInfo; |  | 
| 414     _currentInfo = component; |  | 
| 415     super.visitElement(node); |  | 
| 416     _currentInfo = lastInfo; |  | 
| 417   } |  | 
| 418 |  | 
| 419   /** Adds a component's tag name to the global list. */ |  | 
| 420   void _addComponent(ComponentInfo component) { |  | 
| 421     var existing = _global.components[component.tagName]; |  | 
| 422     if (existing != null) { |  | 
| 423       if (existing.hasConflict) { |  | 
| 424         // No need to report a second error for the same name. |  | 
| 425         return; |  | 
| 426       } |  | 
| 427 |  | 
| 428       existing.hasConflict = true; |  | 
| 429 |  | 
| 430       _messages.error('duplicate custom element definition for ' |  | 
| 431           '"${component.tagName}".', existing.sourceSpan); |  | 
| 432       _messages.error('duplicate custom element definition for ' |  | 
| 433           '"${component.tagName}" (second location).', component.sourceSpan); |  | 
| 434     } else { |  | 
| 435       _global.components[component.tagName] = component; |  | 
| 436     } |  | 
| 437   } |  | 
| 438 |  | 
| 439   void visitScriptElement(Element node) { |  | 
| 440     var scriptType = node.attributes['type']; |  | 
| 441     var src = node.attributes["src"]; |  | 
| 442 |  | 
| 443     if (scriptType == null) { |  | 
| 444       // Note: in html5 leaving off type= is fine, but it defaults to |  | 
| 445       // text/javascript. Because this might be a common error, we warn about it |  | 
| 446       // in two cases: |  | 
| 447       //   * an inline script tag in a web component |  | 
| 448       //   * a script src= if the src file ends in .dart (component or not) |  | 
| 449       // |  | 
| 450       // The hope is that neither of these cases should break existing valid |  | 
| 451       // code, but that they'll help component authors avoid having their Dart |  | 
| 452       // code accidentally interpreted as JavaScript by the browser. |  | 
| 453       if (src == null && _currentInfo is ComponentInfo) { |  | 
| 454         _messages.warning('script tag in component with no type will ' |  | 
| 455             'be treated as JavaScript. Did you forget type="application/dart"?', |  | 
| 456             node.sourceSpan); |  | 
| 457       } |  | 
| 458       if (src != null && src.endsWith('.dart')) { |  | 
| 459         _messages.warning('script tag with .dart source file but no type will ' |  | 
| 460             'be treated as JavaScript. Did you forget type="application/dart"?', |  | 
| 461             node.sourceSpan); |  | 
| 462       } |  | 
| 463       return; |  | 
| 464     } |  | 
| 465 |  | 
| 466     if (scriptType != 'application/dart') return; |  | 
| 467 |  | 
| 468     if (src != null) { |  | 
| 469       if (!src.endsWith('.dart')) { |  | 
| 470         _messages.warning('"application/dart" scripts should ' |  | 
| 471             'use the .dart file extension.', |  | 
| 472             node.sourceSpan); |  | 
| 473       } |  | 
| 474 |  | 
| 475       if (node.innerHtml.trim() != '') { |  | 
| 476         _messages.error('script tag has "src" attribute and also has script ' |  | 
| 477             'text.', node.sourceSpan); |  | 
| 478       } |  | 
| 479     } |  | 
| 480   } |  | 
| 481 } |  | 
| OLD | NEW | 
|---|