OLD | NEW |
---|---|
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 /// Transfomer that inlines polymer-element definitions from html imports. | 5 /// Transfomer that inlines polymer-element definitions from html imports. |
6 library polymer.src.build.import_inliner; | 6 library polymer.src.build.import_inliner; |
7 | 7 |
8 import 'dart:async'; | 8 import 'dart:async'; |
9 import 'dart:convert'; | 9 import 'dart:convert'; |
10 | 10 |
11 import 'package:analyzer/analyzer.dart'; | 11 import 'package:analyzer/analyzer.dart'; |
12 import 'package:analyzer/src/generated/ast.dart'; | 12 import 'package:analyzer/src/generated/ast.dart'; |
13 import 'package:barback/barback.dart'; | 13 import 'package:barback/barback.dart'; |
14 import 'package:code_transformers/assets.dart'; | 14 import 'package:code_transformers/assets.dart'; |
15 import 'package:path/path.dart' as path; | 15 import 'package:path/path.dart' as path; |
16 import 'package:html5lib/dom.dart' show | 16 import 'package:html5lib/dom.dart' show |
17 Document, DocumentFragment, Element, Node; | 17 Document, DocumentFragment, Element, Node; |
18 import 'package:html5lib/dom_parsing.dart' show TreeVisitor; | 18 import 'package:html5lib/dom_parsing.dart' show TreeVisitor; |
19 import 'package:source_maps/refactor.dart' show TextEditTransaction; | 19 import 'package:source_maps/refactor.dart' show TextEditTransaction; |
20 import 'package:source_span/source_span.dart'; | 20 import 'package:source_span/source_span.dart'; |
21 | 21 |
22 import 'common.dart'; | 22 import 'common.dart'; |
23 import 'wrapped_logger.dart'; | |
23 | 24 |
24 // TODO(sigmund): move to web_components package (dartbug.com/18037). | 25 // TODO(sigmund): move to web_components package (dartbug.com/18037). |
25 class _HtmlInliner extends PolymerTransformer { | 26 class _HtmlInliner extends PolymerTransformer { |
26 final TransformOptions options; | 27 final TransformOptions options; |
27 final Transform transform; | 28 final Transform transform; |
28 final TransformLogger logger; | 29 final TransformLogger logger; |
29 final AssetId docId; | 30 final AssetId docId; |
30 final seen = new Set<AssetId>(); | 31 final seen = new Set<AssetId>(); |
31 final scriptIds = <AssetId>[]; | 32 final scriptIds = <AssetId>[]; |
32 final extractedFiles = new Set<AssetId>(); | 33 final extractedFiles = new Set<AssetId>(); |
33 bool experimentalBootstrap = false; | 34 bool experimentalBootstrap = false; |
34 | 35 |
35 /// The number of extracted inline Dart scripts. Used as a counter to give | 36 /// The number of extracted inline Dart scripts. Used as a counter to give |
36 /// unique-ish filenames. | 37 /// unique-ish filenames. |
37 int inlineScriptCounter = 0; | 38 int inlineScriptCounter = 0; |
38 | 39 |
39 _HtmlInliner(this.options, Transform transform) | 40 _HtmlInliner(TransformOptions options, Transform transform) |
40 : transform = transform, | 41 : options = options, |
41 logger = transform.logger, | 42 transform = transform, |
43 logger = new WrappedLogger(transform, | |
44 convertErrorsToWarnings: !options.releaseMode), | |
Siggi Cherem (dart-lang)
2014/08/05 23:35:25
alternatively, we could skip the wrapping in relea
jakemac
2014/08/06 14:11:34
Ya, more efficient that way
| |
42 docId = transform.primaryInput.id; | 45 docId = transform.primaryInput.id; |
43 | 46 |
44 Future apply() { | 47 Future apply() { |
45 seen.add(docId); | 48 seen.add(docId); |
46 | 49 |
47 Document document; | 50 Document document; |
48 bool changed = false; | 51 bool changed = false; |
49 | 52 |
50 return readPrimaryAsHtml(transform).then((doc) { | 53 return readPrimaryAsHtml(transform).then((doc) { |
51 document = doc; | 54 document = doc; |
52 changed = new _UrlNormalizer(transform, docId).visit(document) || changed; | 55 changed = new _UrlNormalizer(transform, docId, logger).visit(document) |
56 || changed; | |
53 | 57 |
54 experimentalBootstrap = document.querySelectorAll('link').any((link) => | 58 experimentalBootstrap = document.querySelectorAll('link').any((link) => |
55 link.attributes['rel'] == 'import' && | 59 link.attributes['rel'] == 'import' && |
56 link.attributes['href'] == POLYMER_EXPERIMENTAL_HTML); | 60 link.attributes['href'] == POLYMER_EXPERIMENTAL_HTML); |
57 changed = _extractScripts(document) || changed; | 61 changed = _extractScripts(document) || changed; |
58 return _visitImports(document); | 62 return _visitImports(document); |
59 }).then((importsFound) { | 63 }).then((importsFound) { |
60 changed = changed || importsFound; | 64 changed = changed || importsFound; |
61 return _removeScripts(document); | 65 return _removeScripts(document); |
62 }).then((scriptsRemoved) { | 66 }).then((scriptsRemoved) { |
63 changed = changed || scriptsRemoved; | 67 changed = changed || scriptsRemoved; |
64 | 68 |
65 var output = transform.primaryInput; | 69 var output = transform.primaryInput; |
66 if (changed) output = new Asset.fromString(docId, document.outerHtml); | 70 if (changed) output = new Asset.fromString(docId, document.outerHtml); |
67 transform.addOutput(output); | 71 transform.addOutput(output); |
68 | 72 |
69 // We produce a secondary asset with extra information for later phases. | 73 // We produce a secondary asset with extra information for later phases. |
70 transform.addOutput(new Asset.fromString( | 74 transform.addOutput(new Asset.fromString( |
71 docId.addExtension('._data'), | 75 docId.addExtension('._data'), |
72 JSON.encode({ | 76 JSON.encode({ |
73 'experimental_bootstrap': experimentalBootstrap, | 77 'experimental_bootstrap': experimentalBootstrap, |
74 'script_ids': scriptIds, | 78 'script_ids': scriptIds, |
75 }, toEncodable: (id) => id.serialize()))); | 79 }, toEncodable: (id) => id.serialize()))); |
80 | |
81 // Write out the logs collected by our [WrappedLogger]. | |
82 if (options.injectBuildLogsInOutput) return logger.writeOutput(); | |
76 }); | 83 }); |
77 } | 84 } |
78 | 85 |
79 /// Visits imports in [document] and add the imported documents to documents. | 86 /// Visits imports in [document] and add the imported documents to documents. |
80 /// Documents are added in the order they appear, transitive imports are added | 87 /// Documents are added in the order they appear, transitive imports are added |
81 /// first. | 88 /// first. |
82 /// | 89 /// |
83 /// Returns `true` if and only if the document was changed and should be | 90 /// Returns `true` if and only if the document was changed and should be |
84 /// written out. | 91 /// written out. |
85 Future<bool> _visitImports(Document document) { | 92 Future<bool> _visitImports(Document document) { |
86 bool changed = false; | 93 bool changed = false; |
87 | 94 |
88 _moveHeadToBody(document); | 95 _moveHeadToBody(document); |
89 | 96 |
90 // Note: we need to preserve the import order in the generated output. | 97 // Note: we need to preserve the import order in the generated output. |
91 return Future.forEach(document.querySelectorAll('link'), (Element tag) { | 98 return Future.forEach(document.querySelectorAll('link'), (Element tag) { |
92 var rel = tag.attributes['rel']; | 99 var rel = tag.attributes['rel']; |
93 if (rel != 'import' && rel != 'stylesheet') return null; | 100 if (rel != 'import' && rel != 'stylesheet') return null; |
94 | 101 |
95 // Note: URL has already been normalized so use docId. | 102 // Note: URL has already been normalized so use docId. |
96 var href = tag.attributes['href']; | 103 var href = tag.attributes['href']; |
97 var id = uriToAssetId(docId, href, transform.logger, tag.sourceSpan, | 104 var id = uriToAssetId(docId, href, logger, tag.sourceSpan, |
98 errorOnAbsolute: rel != 'stylesheet'); | 105 errorOnAbsolute: rel != 'stylesheet'); |
99 | 106 |
100 if (rel == 'import') { | 107 if (rel == 'import') { |
101 changed = true; | 108 changed = true; |
102 if (id == null || !seen.add(id)) { | 109 if (id == null || !seen.add(id)) { |
103 tag.remove(); | 110 tag.remove(); |
104 return null; | 111 return null; |
105 } | 112 } |
106 return _inlineImport(id, tag); | 113 return _inlineImport(id, tag); |
107 | 114 |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
139 // Move the node into the body, where its contents will be placed. | 146 // Move the node into the body, where its contents will be placed. |
140 doc.body.insertBefore(node, insertionPoint); | 147 doc.body.insertBefore(node, insertionPoint); |
141 } | 148 } |
142 } | 149 } |
143 } | 150 } |
144 | 151 |
145 /// Loads an asset identified by [id], visits its imports and collects its | 152 /// Loads an asset identified by [id], visits its imports and collects its |
146 /// html imports. Then inlines it into the main document. | 153 /// html imports. Then inlines it into the main document. |
147 Future _inlineImport(AssetId id, Element link) { | 154 Future _inlineImport(AssetId id, Element link) { |
148 return readAsHtml(id, transform).catchError((error) { | 155 return readAsHtml(id, transform).catchError((error) { |
149 transform.logger.error( | 156 logger.error( |
150 "Failed to inline html import: $error", asset: id, | 157 "Failed to inline html import: $error", asset: id, |
151 span: link.sourceSpan); | 158 span: link.sourceSpan); |
152 }).then((doc) { | 159 }).then((doc) { |
153 if (doc == null) return false; | 160 if (doc == null) return false; |
154 new _UrlNormalizer(transform, id).visit(doc); | 161 new _UrlNormalizer(transform, id, logger).visit(doc); |
155 return _visitImports(doc).then((_) { | 162 return _visitImports(doc).then((_) { |
156 // _UrlNormalizer already ensures there is a library name. | 163 // _UrlNormalizer already ensures there is a library name. |
157 _extractScripts(doc, injectLibraryName: false); | 164 _extractScripts(doc, injectLibraryName: false); |
158 | 165 |
159 // TODO(jmesserly): figure out how this is working in vulcanizer. | 166 // TODO(jmesserly): figure out how this is working in vulcanizer. |
160 // Do they produce a <body> tag with a <head> and <body> inside? | 167 // Do they produce a <body> tag with a <head> and <body> inside? |
161 var imported = new DocumentFragment(); | 168 var imported = new DocumentFragment(); |
162 imported.nodes..addAll(doc.head.nodes)..addAll(doc.body.nodes); | 169 imported.nodes..addAll(doc.head.nodes)..addAll(doc.body.nodes); |
163 link.replaceWith(imported); | 170 link.replaceWith(imported); |
164 }); | 171 }); |
165 }); | 172 }); |
166 } | 173 } |
167 | 174 |
168 Future _inlineStylesheet(AssetId id, Element link) { | 175 Future _inlineStylesheet(AssetId id, Element link) { |
169 return transform.readInputAsString(id).catchError((error) { | 176 return transform.readInputAsString(id).catchError((error) { |
170 // TODO(jakemac): Move this warning to the linter once we can make it run | 177 // TODO(jakemac): Move this warning to the linter once we can make it run |
171 // always (see http://dartbug.com/17199). Then hide this error and replace | 178 // always (see http://dartbug.com/17199). Then hide this error and replace |
172 // with a comment pointing to the linter error (so we don't double warn). | 179 // with a comment pointing to the linter error (so we don't double warn). |
173 transform.logger.warning( | 180 logger.warning( |
174 "Failed to inline stylesheet: $error", asset: id, | 181 "Failed to inline stylesheet: $error", asset: id, |
175 span: link.sourceSpan); | 182 span: link.sourceSpan); |
176 }).then((css) { | 183 }).then((css) { |
177 if (css == null) return; | 184 if (css == null) return; |
178 css = new _UrlNormalizer(transform, id).visitCss(css); | 185 css = new _UrlNormalizer(transform, id, logger).visitCss(css); |
179 var styleElement = new Element.tag('style')..text = css; | 186 var styleElement = new Element.tag('style')..text = css; |
180 // Copy over the extra attributes from the link tag to the style tag. | 187 // Copy over the extra attributes from the link tag to the style tag. |
181 // This adds support for no-shim, shim-shadowdom, etc. | 188 // This adds support for no-shim, shim-shadowdom, etc. |
182 link.attributes.forEach((key, value) { | 189 link.attributes.forEach((key, value) { |
183 if (!IGNORED_LINKED_STYLE_ATTRS.contains(key)) { | 190 if (!IGNORED_LINKED_STYLE_ATTRS.contains(key)) { |
184 styleElement.attributes[key] = value; | 191 styleElement.attributes[key] = value; |
185 } | 192 } |
186 }); | 193 }); |
187 link.replaceWith(styleElement); | 194 link.replaceWith(styleElement); |
188 }); | 195 }); |
(...skipping 131 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
320 /// Counter used to ensure that every library name we inject is unique. | 327 /// Counter used to ensure that every library name we inject is unique. |
321 int _count = 0; | 328 int _count = 0; |
322 | 329 |
323 /// Path to the top level folder relative to the transform primaryInput. | 330 /// Path to the top level folder relative to the transform primaryInput. |
324 /// This should just be some arbitrary # of ../'s. | 331 /// This should just be some arbitrary # of ../'s. |
325 final String topLevelPath; | 332 final String topLevelPath; |
326 | 333 |
327 /// Whether or not the normalizer has changed something in the tree. | 334 /// Whether or not the normalizer has changed something in the tree. |
328 bool changed = false; | 335 bool changed = false; |
329 | 336 |
330 _UrlNormalizer(transform, this.sourceId) | 337 final TransformLogger logger; |
338 | |
339 _UrlNormalizer(transform, this.sourceId, this.logger) | |
331 : transform = transform, | 340 : transform = transform, |
332 topLevelPath = | 341 topLevelPath = |
333 '../' * (transform.primaryInput.id.path.split('/').length - 2); | 342 '../' * (transform.primaryInput.id.path.split('/').length - 2); |
334 | 343 |
335 visit(Node node) { | 344 visit(Node node) { |
336 super.visit(node); | 345 super.visit(node); |
337 return changed; | 346 return changed; |
338 } | 347 } |
339 | 348 |
340 visitElement(Element node) { | 349 visitElement(Element node) { |
341 // TODO(jakemac): Support custom elements that extend html elements which | 350 // TODO(jakemac): Support custom elements that extend html elements which |
342 // have url-like attributes. This probably means keeping a list of which | 351 // have url-like attributes. This probably means keeping a list of which |
343 // html elements support each url-like attribute. | 352 // html elements support each url-like attribute. |
344 if (!isCustomTagName(node.localName)) { | 353 if (!isCustomTagName(node.localName)) { |
345 node.attributes.forEach((name, value) { | 354 node.attributes.forEach((name, value) { |
346 if (_urlAttributes.contains(name)) { | 355 if (_urlAttributes.contains(name)) { |
347 if (!name.startsWith('_') && value.contains(_BINDING_REGEX)) { | 356 if (!name.startsWith('_') && value.contains(_BINDING_REGEX)) { |
348 transform.logger.warning( | 357 logger.warning( |
349 'When using bindings with the "$name" attribute you may ' | 358 'When using bindings with the "$name" attribute you may ' |
350 'experience errors in certain browsers. Please use the ' | 359 'experience errors in certain browsers. Please use the ' |
351 '"_$name" attribute instead. For more information, see ' | 360 '"_$name" attribute instead. For more information, see ' |
352 'http://goo.gl/5av8cU', span: node.sourceSpan, asset: sourceId); | 361 'http://goo.gl/5av8cU', span: node.sourceSpan, asset: sourceId); |
353 } else if (name.startsWith('_') && !value.contains(_BINDING_REGEX)) { | 362 } else if (name.startsWith('_') && !value.contains(_BINDING_REGEX)) { |
354 transform.logger.warning( | 363 logger.warning( |
355 'The "$name" attribute is only supported when using bindings. ' | 364 'The "$name" attribute is only supported when using bindings. ' |
356 'Please change to the "${name.substring(1)}" attribute.', | 365 'Please change to the "${name.substring(1)}" attribute.', |
357 span: node.sourceSpan, asset: sourceId); | 366 span: node.sourceSpan, asset: sourceId); |
358 } | 367 } |
359 if (value != '' && !value.trim().startsWith(_BINDING_REGEX)) { | 368 if (value != '' && !value.trim().startsWith(_BINDING_REGEX)) { |
360 node.attributes[name] = _newUrl(value, node.sourceSpan); | 369 node.attributes[name] = _newUrl(value, node.sourceSpan); |
361 changed = changed || value != node.attributes[name]; | 370 changed = changed || value != node.attributes[name]; |
362 } | 371 } |
363 } | 372 } |
364 }); | 373 }); |
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
400 String visitInlineDart(String code) { | 409 String visitInlineDart(String code) { |
401 var unit = parseDirectives(code, suppressErrors: true); | 410 var unit = parseDirectives(code, suppressErrors: true); |
402 var file = new SourceFile(code, url: spanUrlFor(sourceId, transform)); | 411 var file = new SourceFile(code, url: spanUrlFor(sourceId, transform)); |
403 var output = new TextEditTransaction(code, file); | 412 var output = new TextEditTransaction(code, file); |
404 var foundLibraryDirective = false; | 413 var foundLibraryDirective = false; |
405 for (Directive directive in unit.directives) { | 414 for (Directive directive in unit.directives) { |
406 if (directive is UriBasedDirective) { | 415 if (directive is UriBasedDirective) { |
407 var uri = directive.uri.stringValue; | 416 var uri = directive.uri.stringValue; |
408 var span = _getSpan(file, directive.uri); | 417 var span = _getSpan(file, directive.uri); |
409 | 418 |
410 var id = uriToAssetId(sourceId, uri, transform.logger, span, | 419 var id = uriToAssetId(sourceId, uri, logger, span, |
411 errorOnAbsolute: false); | 420 errorOnAbsolute: false); |
412 if (id == null) continue; | 421 if (id == null) continue; |
413 | 422 |
414 var primaryId = transform.primaryInput.id; | 423 var primaryId = transform.primaryInput.id; |
415 var newUri = assetUrlFor(id, primaryId, transform.logger); | 424 var newUri = assetUrlFor(id, primaryId, logger); |
416 if (newUri != uri) { | 425 if (newUri != uri) { |
417 output.edit(span.start.offset, span.end.offset, "'$newUri'"); | 426 output.edit(span.start.offset, span.end.offset, "'$newUri'"); |
418 } | 427 } |
419 } else if (directive is LibraryDirective) { | 428 } else if (directive is LibraryDirective) { |
420 foundLibraryDirective = true; | 429 foundLibraryDirective = true; |
421 } | 430 } |
422 } | 431 } |
423 | 432 |
424 if (!foundLibraryDirective) { | 433 if (!foundLibraryDirective) { |
425 // Ensure all inline scripts also have a library name. | 434 // Ensure all inline scripts also have a library name. |
(...skipping 23 matching lines...) Expand all Loading... | |
449 hrefToParse = '${href.substring(0, firstBinding)}$placeholder'; | 458 hrefToParse = '${href.substring(0, firstBinding)}$placeholder'; |
450 } | 459 } |
451 | 460 |
452 var uri = Uri.parse(hrefToParse); | 461 var uri = Uri.parse(hrefToParse); |
453 if (uri.isAbsolute) return href; | 462 if (uri.isAbsolute) return href; |
454 if (!uri.scheme.isEmpty) return href; | 463 if (!uri.scheme.isEmpty) return href; |
455 if (!uri.host.isEmpty) return href; | 464 if (!uri.host.isEmpty) return href; |
456 if (uri.path.isEmpty) return href; // Implies standalone ? or # in URI. | 465 if (uri.path.isEmpty) return href; // Implies standalone ? or # in URI. |
457 if (path.isAbsolute(href)) return href; | 466 if (path.isAbsolute(href)) return href; |
458 | 467 |
459 var id = uriToAssetId(sourceId, hrefToParse, transform.logger, span); | 468 var id = uriToAssetId(sourceId, hrefToParse, logger, span); |
460 if (id == null) return href; | 469 if (id == null) return href; |
461 var primaryId = transform.primaryInput.id; | 470 var primaryId = transform.primaryInput.id; |
462 | 471 |
463 // Build the new path, placing back any suffixes that we stripped earlier. | 472 // Build the new path, placing back any suffixes that we stripped earlier. |
464 var prefix = (firstBinding == -1) ? id.path | 473 var prefix = (firstBinding == -1) ? id.path |
465 : id.path.substring(0, id.path.length - placeholder.length); | 474 : id.path.substring(0, id.path.length - placeholder.length); |
466 var suffix = (firstBinding == -1) ? '' : href.substring(firstBinding); | 475 var suffix = (firstBinding == -1) ? '' : href.substring(firstBinding); |
467 var newPath = '$prefix$suffix'; | 476 var newPath = '$prefix$suffix'; |
468 | 477 |
469 if (newPath.startsWith('lib/')) { | 478 if (newPath.startsWith('lib/')) { |
470 return '${topLevelPath}packages/${id.package}/${newPath.substring(4)}'; | 479 return '${topLevelPath}packages/${id.package}/${newPath.substring(4)}'; |
471 } | 480 } |
472 | 481 |
473 if (newPath.startsWith('asset/')) { | 482 if (newPath.startsWith('asset/')) { |
474 return '${topLevelPath}assets/${id.package}/${newPath.substring(6)}'; | 483 return '${topLevelPath}assets/${id.package}/${newPath.substring(6)}'; |
475 } | 484 } |
476 | 485 |
477 if (primaryId.package != id.package) { | 486 if (primaryId.package != id.package) { |
478 // Techincally we shouldn't get there | 487 // Techincally we shouldn't get there |
479 transform.logger.error("don't know how to include $id from $primaryId", | 488 logger.error("don't know how to include $id from $primaryId", span: span); |
480 span: span); | |
481 return href; | 489 return href; |
482 } | 490 } |
483 | 491 |
484 var builder = path.url; | 492 var builder = path.url; |
485 return builder.relative(builder.join('/', newPath), | 493 return builder.relative(builder.join('/', newPath), |
486 from: builder.join('/', builder.dirname(primaryId.path))); | 494 from: builder.join('/', builder.dirname(primaryId.path))); |
487 } | 495 } |
488 } | 496 } |
489 | 497 |
490 /// HTML attributes that expect a URL value. | 498 /// HTML attributes that expect a URL value. |
(...skipping 21 matching lines...) Expand all Loading... | |
512 /// style tag except these ones. | 520 /// style tag except these ones. |
513 const IGNORED_LINKED_STYLE_ATTRS = | 521 const IGNORED_LINKED_STYLE_ATTRS = |
514 const ['charset', 'href', 'href-lang', 'rel', 'rev']; | 522 const ['charset', 'href', 'href-lang', 'rel', 'rev']; |
515 | 523 |
516 /// Global RegExp objects. | 524 /// Global RegExp objects. |
517 final _INVALID_LIB_CHARS_REGEX = new RegExp('[^a-z0-9_]'); | 525 final _INVALID_LIB_CHARS_REGEX = new RegExp('[^a-z0-9_]'); |
518 final _NUM_REGEX = new RegExp('[0-9]'); | 526 final _NUM_REGEX = new RegExp('[0-9]'); |
519 final _BINDING_REGEX = new RegExp(r'(({{.*}})|(\[\[.*\]\]))'); | 527 final _BINDING_REGEX = new RegExp(r'(({{.*}})|(\[\[.*\]\]))'); |
520 | 528 |
521 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); | 529 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); |
OLD | NEW |