Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(26)

Side by Side Diff: pkg/polymer/lib/src/analyzer.dart

Issue 23224003: move polymer.dart into dart svn (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: add --deploy to todomvc sample Created 7 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « pkg/polymer/lib/safe_html.dart ('k') | pkg/polymer/lib/src/compiler.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 import 'package:source_maps/span.dart' hide SourceFile;
14
15 import 'custom_tag_name.dart';
16 import 'dart_parser.dart' show parseDartCode;
17 import 'files.dart';
18 import 'info.dart';
19 import 'messages.dart';
20 import 'summary.dart';
21
22 /**
23 * Finds custom elements in this file and the list of referenced files with
24 * component declarations. This is the first pass of analysis on a file.
25 *
26 * Adds emitted error/warning messages to [messages], if [messages] is
27 * supplied.
28 */
29 FileInfo analyzeDefinitions(GlobalInfo global, UrlInfo inputUrl,
30 Document document, String packageRoot,
31 Messages messages, {bool isEntryPoint: false}) {
32 var result = new FileInfo(inputUrl, isEntryPoint);
33 var loader = new _ElementLoader(global, result, packageRoot, messages);
34 loader.visit(document);
35 return result;
36 }
37
38 /**
39 * Extract relevant information from all files found from the root document.
40 *
41 * Adds emitted error/warning messages to [messages], if [messages] is
42 * supplied.
43 */
44 void analyzeFile(SourceFile file, Map<String, FileInfo> info,
45 Iterator<int> uniqueIds, GlobalInfo global,
46 Messages messages, emulateScopedCss) {
47 var fileInfo = info[file.path];
48 var analyzer = new _Analyzer(fileInfo, uniqueIds, global, messages,
49 emulateScopedCss);
50 analyzer._normalize(fileInfo, info);
51 analyzer.visit(file.document);
52 }
53
54
55 /** A visitor that walks the HTML to extract all the relevant information. */
56 class _Analyzer extends TreeVisitor {
57 final FileInfo _fileInfo;
58 LibraryInfo _currentInfo;
59 Iterator<int> _uniqueIds;
60 GlobalInfo _global;
61 Messages _messages;
62
63 int _generatedClassNumber = 0;
64
65 /**
66 * Whether to keep indentation spaces. Break lines and indentation spaces
67 * within templates are preserved in HTML. When users specify the attribute
68 * 'indentation="remove"' on a template tag, we'll trim those indentation
69 * spaces that occur within that tag and its decendants. If any decendant
70 * specifies 'indentation="preserve"', then we'll switch back to the normal
71 * behavior.
72 */
73 bool _keepIndentationSpaces = true;
74
75 final bool _emulateScopedCss;
76
77 _Analyzer(this._fileInfo, this._uniqueIds, this._global, this._messages,
78 this._emulateScopedCss) {
79 _currentInfo = _fileInfo;
80 }
81
82 void visitElement(Element node) {
83 if (node.tagName == 'script') {
84 // We already extracted script tags in previous phase.
85 return;
86 }
87
88 if (node.tagName == 'style') {
89 // We've already parsed the CSS.
90 // If this is a component remove the style node.
91 if (_currentInfo is ComponentInfo && _emulateScopedCss) node.remove();
92 return;
93 }
94
95 _bindCustomElement(node);
96
97 var lastInfo = _currentInfo;
98 if (node.tagName == 'polymer-element') {
99 // If element is invalid _ElementLoader already reported an error, but
100 // we skip the body of the element here.
101 var name = node.attributes['name'];
102 if (name == null) return;
103
104 ComponentInfo component = _fileInfo.components[name];
105 if (component == null) return;
106
107 _analyzeComponent(component);
108
109 _currentInfo = component;
110
111 // Remove the <element> tag from the tree
112 node.remove();
113 }
114
115 node.attributes.forEach((name, value) {
116 if (name.startsWith('on')) {
117 _validateEventHandler(node, name, value);
118 } else if (name == 'pseudo' && _currentInfo is ComponentInfo) {
119 // Any component's custom pseudo-element(s) defined?
120 _processPseudoAttribute(node, value.split(' '));
121 }
122 });
123
124 var keepSpaces = _keepIndentationSpaces;
125 if (node.tagName == 'template' &&
126 node.attributes.containsKey('indentation')) {
127 var value = node.attributes['indentation'];
128 if (value != 'remove' && value != 'preserve') {
129 _messages.warning(
130 "Invalid value for 'indentation' ($value). By default we preserve "
131 "the indentation. Valid values are either 'remove' or 'preserve'.",
132 node.sourceSpan);
133 }
134 _keepIndentationSpaces = value != 'remove';
135 }
136
137 // Invoke super to visit children.
138 super.visitElement(node);
139
140 _keepIndentationSpaces = keepSpaces;
141 _currentInfo = lastInfo;
142
143 if (node.tagName == 'body' || node.parent == null) {
144 _fileInfo.body = node;
145 }
146 }
147
148 void _analyzeComponent(ComponentInfo component) {
149 var baseTag = component.extendsTag;
150 component.extendsComponent = baseTag == null ? null
151 : _fileInfo.components[baseTag];
152 if (component.extendsComponent == null && isCustomTag(baseTag)) {
153 _messages.warning(
154 'custom element with tag name ${component.extendsTag} not found.',
155 component.element.sourceSpan);
156 }
157
158 // Now that the component's code has been loaded, we can validate that the
159 // class exists.
160 component.findClassDeclaration(_messages);
161 }
162
163 void _bindCustomElement(Element node) {
164 // <fancy-button>
165 var component = _fileInfo.components[node.tagName];
166 if (component == null) {
167 // TODO(jmesserly): warn for unknown element tags?
168
169 // <button is="fancy-button">
170 var componentName = node.attributes['is'];
171 if (componentName != null) {
172 component = _fileInfo.components[componentName];
173 } else if (isCustomTag(node.tagName)) {
174 componentName = node.tagName;
175 }
176 if (component == null && componentName != null &&
177 componentName != 'polymer-element') {
178 _messages.warning(
179 'custom element with tag name $componentName not found.',
180 node.sourceSpan);
181 }
182 }
183
184 if (component != null) {
185 if (!component.hasConflict) {
186 _currentInfo.usedComponents[component] = true;
187 }
188
189 var baseTag = component.baseExtendsTag;
190 var nodeTag = node.tagName;
191 var hasIsAttribute = node.attributes.containsKey('is');
192
193 if (baseTag != null && !hasIsAttribute) {
194 _messages.warning(
195 'custom element "${component.tagName}" extends from "$baseTag", but'
196 ' this tag will not include the default properties of "$baseTag". '
197 'To fix this, either write this tag as <$baseTag '
198 'is="${component.tagName}"> or remove the "extends" attribute from '
199 'the custom element declaration.', node.sourceSpan);
200 } else if (hasIsAttribute) {
201 if (baseTag == null) {
202 _messages.warning(
203 'custom element "${component.tagName}" doesn\'t declare any type '
204 'extensions. To fix this, either rewrite this tag as '
205 '<${component.tagName}> or add \'extends="$nodeTag"\' to '
206 'the custom element declaration.', node.sourceSpan);
207 } else if (baseTag != nodeTag) {
208 _messages.warning(
209 'custom element "${component.tagName}" extends from "$baseTag". '
210 'Did you mean to write <$baseTag is="${component.tagName}">?',
211 node.sourceSpan);
212 }
213 }
214 }
215 }
216
217 void _processPseudoAttribute(Node node, List<String> values) {
218 List mangledValues = [];
219 for (var pseudoElement in values) {
220 if (_global.pseudoElements.containsKey(pseudoElement)) continue;
221
222 _uniqueIds.moveNext();
223 var newValue = "${pseudoElement}_${_uniqueIds.current}";
224 _global.pseudoElements[pseudoElement] = newValue;
225 // Mangled name of pseudo-element.
226 mangledValues.add(newValue);
227
228 if (!pseudoElement.startsWith('x-')) {
229 // TODO(terry): The name must start with x- otherwise it's not a custom
230 // pseudo-element. May want to relax since components no
231 // longer need to start with x-. See isse #509 on
232 // pseudo-element prefix.
233 _messages.warning("Custom pseudo-element must be prefixed with 'x-'.",
234 node.sourceSpan);
235 }
236 }
237
238 // Update the pseudo attribute with the new mangled names.
239 node.attributes['pseudo'] = mangledValues.join(' ');
240 }
241
242 /**
243 * Support for inline event handlers that take expressions.
244 * For example: `on-double-click=myHandler($event, todo)`.
245 */
246 void _validateEventHandler(Element node, String name, String value) {
247 if (!name.startsWith('on-')) {
248 // TODO(jmesserly): do we need an option to suppress this warning?
249 _messages.warning('Event handler $name will be interpreted as an inline '
250 'JavaScript event handler. Use the form '
251 'on-event-name="handlerName" if you want a Dart handler '
252 'that will automatically update the UI based on model changes.',
253 node.sourceSpan);
254 }
255
256 if (value.contains('.') || value.contains('(')) {
257 // TODO(sigmund): should we allow more if we use fancy-syntax?
258 _messages.warning('Invalid event handler body "$value". Declare a method '
259 'in your custom element "void handlerName(event, detail, target)" '
260 'and use the form on-event-name="handlerName".',
261 node.sourceSpan);
262 }
263 }
264
265 /**
266 * Normalizes references in [info]. On the [analyzeDefinitions] phase, the
267 * analyzer extracted names of files and components. Here we link those names
268 * to actual info classes. In particular:
269 * * we initialize the [FileInfo.components] map in [info] by importing all
270 * [declaredComponents],
271 * * we scan all [info.componentLinks] and import their
272 * [info.declaredComponents], using [files] to map the href to the file
273 * info. Names in [info] will shadow names from imported files.
274 * * we fill [LibraryInfo.externalCode] on each component declared in
275 * [info].
276 */
277 void _normalize(FileInfo info, Map<String, FileInfo> files) {
278 _attachExtenalScript(info, files);
279
280 for (var component in info.declaredComponents) {
281 _addComponent(info, component);
282 _attachExtenalScript(component, files);
283 }
284
285 for (var link in info.componentLinks) {
286 var file = files[link.resolvedPath];
287 // We already issued an error for missing files.
288 if (file == null) continue;
289 file.declaredComponents.forEach((c) => _addComponent(info, c));
290 }
291 }
292
293 /**
294 * Stores a direct reference in [info] to a dart source file that was loaded
295 * in a script tag with the 'src' attribute.
296 */
297 void _attachExtenalScript(LibraryInfo info, Map<String, FileInfo> files) {
298 var externalFile = info.externalFile;
299 if (externalFile != null) {
300 info.externalCode = files[externalFile.resolvedPath];
301 if (info.externalCode != null) info.externalCode.htmlFile = info;
302 }
303 }
304
305 /** Adds a component's tag name to the names in scope for [fileInfo]. */
306 void _addComponent(FileInfo fileInfo, ComponentSummary component) {
307 var existing = fileInfo.components[component.tagName];
308 if (existing != null) {
309 if (existing == component) {
310 // This is the same exact component as the existing one.
311 return;
312 }
313
314 if (existing is ComponentInfo && component is! ComponentInfo) {
315 // Components declared in [fileInfo] shadow component names declared in
316 // imported files.
317 return;
318 }
319
320 if (existing.hasConflict) {
321 // No need to report a second error for the same name.
322 return;
323 }
324
325 existing.hasConflict = true;
326
327 if (component is ComponentInfo) {
328 _messages.error('duplicate custom element definition for '
329 '"${component.tagName}".', existing.sourceSpan);
330 _messages.error('duplicate custom element definition for '
331 '"${component.tagName}" (second location).', component.sourceSpan);
332 } else {
333 _messages.error('imported duplicate custom element definitions '
334 'for "${component.tagName}".', existing.sourceSpan);
335 _messages.error('imported duplicate custom element definitions '
336 'for "${component.tagName}" (second location).',
337 component.sourceSpan);
338 }
339 } else {
340 fileInfo.components[component.tagName] = component;
341 }
342 }
343 }
344
345 /** A visitor that finds `<link rel="import">` and `<element>` tags. */
346 class _ElementLoader extends TreeVisitor {
347 final GlobalInfo _global;
348 final FileInfo _fileInfo;
349 LibraryInfo _currentInfo;
350 String _packageRoot;
351 bool _inHead = false;
352 Messages _messages;
353
354 /**
355 * Adds emitted warning/error messages to [_messages]. [_messages]
356 * must not be null.
357 */
358 _ElementLoader(this._global, this._fileInfo, this._packageRoot,
359 this._messages) {
360 _currentInfo = _fileInfo;
361 }
362
363 void visitElement(Element node) {
364 switch (node.tagName) {
365 case 'link': visitLinkElement(node); break;
366 case 'element':
367 _messages.warning('<element> elements are not supported, use'
368 ' <polymer-element> instead', node.sourceSpan);
369 break;
370 case 'polymer-element':
371 visitElementElement(node);
372 break;
373 case 'script': visitScriptElement(node); break;
374 case 'head':
375 var savedInHead = _inHead;
376 _inHead = true;
377 super.visitElement(node);
378 _inHead = savedInHead;
379 break;
380 default: super.visitElement(node); break;
381 }
382 }
383
384 /**
385 * Process `link rel="import"` as specified in:
386 * <https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/components/index.ht ml#link-type-component>
387 */
388 void visitLinkElement(Element node) {
389 var rel = node.attributes['rel'];
390 if (rel != 'component' && rel != 'components' &&
391 rel != 'import' && rel != 'stylesheet') return;
392
393 if (!_inHead) {
394 _messages.warning('link rel="$rel" only valid in '
395 'head.', node.sourceSpan);
396 return;
397 }
398
399 if (rel == 'component' || rel == 'components') {
400 _messages.warning('import syntax is changing, use '
401 'rel="import" instead of rel="$rel".', node.sourceSpan);
402 }
403
404 var href = node.attributes['href'];
405 if (href == null || href == '') {
406 _messages.warning('link rel="$rel" missing href.',
407 node.sourceSpan);
408 return;
409 }
410
411 bool isStyleSheet = rel == 'stylesheet';
412 var urlInfo = UrlInfo.resolve(href, _fileInfo.inputUrl, node.sourceSpan,
413 _packageRoot, _messages, ignoreAbsolute: isStyleSheet);
414 if (urlInfo == null) return;
415 if (isStyleSheet) {
416 _fileInfo.styleSheetHrefs.add(urlInfo);
417 } else {
418 _fileInfo.componentLinks.add(urlInfo);
419 }
420 }
421
422 void visitElementElement(Element node) {
423 // TODO(jmesserly): what do we do in this case? It seems like an <element>
424 // inside a Shadow DOM should be scoped to that <template> tag, and not
425 // visible from the outside.
426 if (_currentInfo is ComponentInfo) {
427 _messages.error('Nested component definitions are not yet supported.',
428 node.sourceSpan);
429 return;
430 }
431
432 var tagName = node.attributes['name'];
433 var extendsTag = node.attributes['extends'];
434
435 if (tagName == null) {
436 _messages.error('Missing tag name of the component. Please include an '
437 'attribute like \'name="your-tag-name"\'.',
438 node.sourceSpan);
439 return;
440 }
441
442 var component = new ComponentInfo(node, _fileInfo, tagName, extendsTag);
443 _fileInfo.declaredComponents.add(component);
444 _addComponent(component);
445
446 var lastInfo = _currentInfo;
447 _currentInfo = component;
448 super.visitElement(node);
449 _currentInfo = lastInfo;
450 }
451
452 /** Adds a component's tag name to the global list. */
453 void _addComponent(ComponentInfo component) {
454 var existing = _global.components[component.tagName];
455 if (existing != null) {
456 if (existing.hasConflict) {
457 // No need to report a second error for the same name.
458 return;
459 }
460
461 existing.hasConflict = true;
462
463 _messages.error('duplicate custom element definition for '
464 '"${component.tagName}".', existing.sourceSpan);
465 _messages.error('duplicate custom element definition for '
466 '"${component.tagName}" (second location).', component.sourceSpan);
467 } else {
468 _global.components[component.tagName] = component;
469 }
470 }
471
472 void visitScriptElement(Element node) {
473 var scriptType = node.attributes['type'];
474 var src = node.attributes["src"];
475
476 if (scriptType == null) {
477 // Note: in html5 leaving off type= is fine, but it defaults to
478 // text/javascript. Because this might be a common error, we warn about it
479 // in two cases:
480 // * an inline script tag in a web component
481 // * a script src= if the src file ends in .dart (component or not)
482 //
483 // The hope is that neither of these cases should break existing valid
484 // code, but that they'll help component authors avoid having their Dart
485 // code accidentally interpreted as JavaScript by the browser.
486 if (src == null && _currentInfo is ComponentInfo) {
487 _messages.warning('script tag in component with no type will '
488 'be treated as JavaScript. Did you forget type="application/dart"?',
489 node.sourceSpan);
490 }
491 if (src != null && src.endsWith('.dart')) {
492 _messages.warning('script tag with .dart source file but no type will '
493 'be treated as JavaScript. Did you forget type="application/dart"?',
494 node.sourceSpan);
495 }
496 return;
497 }
498
499 if (scriptType != 'application/dart') {
500 if (_currentInfo is ComponentInfo) {
501 // TODO(jmesserly): this warning should not be here, but our compiler
502 // does the wrong thing and it could cause surprising behavior, so let
503 // the user know! See issue #340 for more info.
504 // What we should be doing: leave JS component untouched by compiler.
505 _messages.warning('our custom element implementation does not support '
506 'JavaScript components yet. If this is affecting you please let us '
507 'know at https://github.com/dart-lang/web-ui/issues/340.',
508 node.sourceSpan);
509 }
510
511 return;
512 }
513
514 if (src != null) {
515 if (!src.endsWith('.dart')) {
516 _messages.warning('"application/dart" scripts should '
517 'use the .dart file extension.',
518 node.sourceSpan);
519 }
520
521 if (node.innerHtml.trim() != '') {
522 _messages.error('script tag has "src" attribute and also has script '
523 'text.', node.sourceSpan);
524 }
525
526 if (_currentInfo.codeAttached) {
527 _tooManyScriptsError(node);
528 } else {
529 _currentInfo.externalFile = UrlInfo.resolve(src, _fileInfo.inputUrl,
530 node.sourceSpan, _packageRoot, _messages);
531 }
532 return;
533 }
534
535 if (node.nodes.length == 0) return;
536
537 // I don't think the html5 parser will emit a tree with more than
538 // one child of <script>
539 assert(node.nodes.length == 1);
540 Text text = node.nodes[0];
541
542 if (_currentInfo.codeAttached) {
543 _tooManyScriptsError(node);
544 } else if (_currentInfo == _fileInfo && !_fileInfo.isEntryPoint) {
545 _messages.warning('top-level dart code is ignored on '
546 ' HTML pages that define components, but are not the entry HTML '
547 'file.', node.sourceSpan);
548 } else {
549 _currentInfo.inlinedCode = parseDartCode(
550 _currentInfo.dartCodeUrl.resolvedPath, text.value,
551 text.sourceSpan.start);
552 if (_currentInfo.userCode.partOf != null) {
553 _messages.error('expected a library, not a part.',
554 node.sourceSpan);
555 }
556 }
557 }
558
559 void _tooManyScriptsError(Node node) {
560 var location = _currentInfo is ComponentInfo ?
561 'a custom element declaration' : 'the top-level HTML page';
562
563 _messages.error('there should be only one dart script tag in $location.',
564 node.sourceSpan);
565 }
566 }
OLDNEW
« no previous file with comments | « pkg/polymer/lib/safe_html.dart ('k') | pkg/polymer/lib/src/compiler.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698