| 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 |