| 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/src/generated/ast.dart'; | 11 import 'package:analyzer/src/generated/ast.dart'; |
| 12 import 'package:barback/barback.dart'; | 12 import 'package:barback/barback.dart'; |
| 13 import 'package:code_transformers/assets.dart'; | 13 import 'package:code_transformers/assets.dart'; |
| 14 import 'package:path/path.dart' as path; | 14 import 'package:path/path.dart' as path; |
| 15 import 'package:html5lib/dom.dart' show | 15 import 'package:html5lib/dom.dart' show |
| 16 Document, DocumentFragment, Element, Node; | 16 Document, DocumentFragment, Element, Node; |
| 17 import 'package:html5lib/dom_parsing.dart' show TreeVisitor; | 17 import 'package:html5lib/dom_parsing.dart' show TreeVisitor; |
| 18 import 'package:source_maps/refactor.dart' show TextEditTransaction; | 18 import 'package:source_maps/refactor.dart' show TextEditTransaction; |
| 19 import 'package:source_maps/span.dart'; | 19 import 'package:source_maps/span.dart'; |
| 20 | 20 |
| 21 import 'common.dart'; | 21 import 'common.dart'; |
| 22 | 22 |
| 23 // TODO(sigmund): move to web_components package (dartbug.com/18037). |
| 23 class _HtmlInliner extends PolymerTransformer { | 24 class _HtmlInliner extends PolymerTransformer { |
| 24 final TransformOptions options; | 25 final TransformOptions options; |
| 25 final Transform transform; | 26 final Transform transform; |
| 26 final TransformLogger logger; | 27 final TransformLogger logger; |
| 27 final AssetId docId; | 28 final AssetId docId; |
| 28 final seen = new Set<AssetId>(); | 29 final seen = new Set<AssetId>(); |
| 29 final scriptIds = <AssetId>[]; | 30 final scriptIds = <AssetId>[]; |
| 30 | 31 |
| 31 /// The number of extracted inline Dart scripts. Used as a counter to give | 32 /// The number of extracted inline Dart scripts. Used as a counter to give |
| 32 /// unique-ish filenames. | 33 /// unique-ish filenames. |
| (...skipping 10 matching lines...) Expand all Loading... |
| 43 Document document; | 44 Document document; |
| 44 bool changed; | 45 bool changed; |
| 45 | 46 |
| 46 return readPrimaryAsHtml(transform).then((doc) { | 47 return readPrimaryAsHtml(transform).then((doc) { |
| 47 document = doc; | 48 document = doc; |
| 48 // Add the main script's ID, or null if none is present. | 49 // Add the main script's ID, or null if none is present. |
| 49 // This will be used by ScriptCompactor. | 50 // This will be used by ScriptCompactor. |
| 50 changed = _extractScripts(document, docId); | 51 changed = _extractScripts(document, docId); |
| 51 return _visitImports(document); | 52 return _visitImports(document); |
| 52 }).then((importsFound) { | 53 }).then((importsFound) { |
| 53 changed = changed || importsFound; | 54 bool scriptsRemoved = _removeScripts(document); |
| 55 changed = changed || importsFound || scriptsRemoved; |
| 54 | 56 |
| 55 var output = transform.primaryInput; | 57 var output = transform.primaryInput; |
| 56 if (changed) output = new Asset.fromString(docId, document.outerHtml); | 58 if (changed) output = new Asset.fromString(docId, document.outerHtml); |
| 57 transform.addOutput(output); | 59 transform.addOutput(output); |
| 58 | 60 |
| 59 // We produce a secondary asset with extra information for later phases. | 61 // We produce a secondary asset with extra information for later phases. |
| 60 transform.addOutput(new Asset.fromString( | 62 transform.addOutput(new Asset.fromString( |
| 61 docId.addExtension('.scriptUrls'), | 63 docId.addExtension('.scriptUrls'), |
| 62 JSON.encode(scriptIds, toEncodable: (id) => id.serialize()))); | 64 JSON.encode(scriptIds, toEncodable: (id) => id.serialize()))); |
| 63 }); | 65 }); |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 113 // Should we do the same? Alternatively could we inline head into head and | 115 // Should we do the same? Alternatively could we inline head into head and |
| 114 // body into body and avoid this whole thing? | 116 // body into body and avoid this whole thing? |
| 115 void _moveHeadToBody(Document doc) { | 117 void _moveHeadToBody(Document doc) { |
| 116 var insertionPoint = doc.body.firstChild; | 118 var insertionPoint = doc.body.firstChild; |
| 117 for (var node in doc.head.nodes.toList(growable: false)) { | 119 for (var node in doc.head.nodes.toList(growable: false)) { |
| 118 if (node is! Element) continue; | 120 if (node is! Element) continue; |
| 119 var tag = node.localName; | 121 var tag = node.localName; |
| 120 var type = node.attributes['type']; | 122 var type = node.attributes['type']; |
| 121 var rel = node.attributes['rel']; | 123 var rel = node.attributes['rel']; |
| 122 if (tag == 'style' || tag == 'script' && | 124 if (tag == 'style' || tag == 'script' && |
| 123 (type == null || type == TYPE_JS || type == TYPE_DART) || | 125 (type == null || type == TYPE_JS || type == TYPE_DART_APP || |
| 126 type == TYPE_DART_COMPONENT) || |
| 124 tag == 'link' && (rel == 'stylesheet' || rel == 'import')) { | 127 tag == 'link' && (rel == 'stylesheet' || rel == 'import')) { |
| 125 // Move the node into the body, where its contents will be placed. | 128 // Move the node into the body, where its contents will be placed. |
| 126 doc.body.insertBefore(node, insertionPoint); | 129 doc.body.insertBefore(node, insertionPoint); |
| 127 } | 130 } |
| 128 } | 131 } |
| 129 } | 132 } |
| 130 | 133 |
| 131 /// Loads an asset identified by [id], visits its imports and collects its | 134 /// Loads an asset identified by [id], visits its imports and collects its |
| 132 /// html imports. Then inlines it into the main document. | 135 /// html imports. Then inlines it into the main document. |
| 133 Future _inlineImport(AssetId id, Element link) { | 136 Future _inlineImport(AssetId id, Element link) { |
| 134 return readAsHtml(id, transform).then((doc) { | 137 return readAsHtml(id, transform).then((doc) { |
| 135 new _UrlNormalizer(transform, id).visit(doc); | 138 new _UrlNormalizer(transform, id).visit(doc); |
| 136 return _visitImports(doc).then((_) { | 139 return _visitImports(doc).then((_) { |
| 137 _extractScripts(doc, id); | 140 _extractScripts(doc, id); |
| 138 _removeScripts(doc); | |
| 139 | 141 |
| 140 // TODO(jmesserly): figure out how this is working in vulcanizer. | 142 // TODO(jmesserly): figure out how this is working in vulcanizer. |
| 141 // Do they produce a <body> tag with a <head> and <body> inside? | 143 // Do they produce a <body> tag with a <head> and <body> inside? |
| 142 var imported = new DocumentFragment(); | 144 var imported = new DocumentFragment(); |
| 143 imported.nodes..addAll(doc.head.nodes)..addAll(doc.body.nodes); | 145 imported.nodes..addAll(doc.head.nodes)..addAll(doc.body.nodes); |
| 144 link.replaceWith(imported); | 146 link.replaceWith(imported); |
| 145 }); | 147 }); |
| 146 }); | 148 }); |
| 147 } | 149 } |
| 148 | 150 |
| 149 Future _inlineStylesheet(AssetId id, Element link) { | 151 Future _inlineStylesheet(AssetId id, Element link) { |
| 150 return transform.readInputAsString(id).then((css) { | 152 return transform.readInputAsString(id).then((css) { |
| 151 css = new _UrlNormalizer(transform, id).visitCss(css); | 153 css = new _UrlNormalizer(transform, id).visitCss(css); |
| 152 link.replaceWith(new Element.tag('style')..text = css); | 154 link.replaceWith(new Element.tag('style')..text = css); |
| 153 }); | 155 }); |
| 154 } | 156 } |
| 155 | 157 |
| 156 /// Remove scripts from HTML imports, and remember their [AssetId]s for later | 158 /// Remove "application/dart;component=1" scripts and remember their |
| 157 /// use. | 159 /// [AssetId]s for later use. |
| 158 /// | 160 /// |
| 159 /// Dartium only allows a single script tag per page, so we can't inline | 161 /// Dartium only allows a single script tag per page, so we can't inline |
| 160 /// the script tags. Instead we remove them entirely. | 162 /// the script tags. Instead we remove them entirely. |
| 161 void _removeScripts(Document doc) { | 163 bool _removeScripts(Document doc) { |
| 164 bool changed = false; |
| 162 for (var script in doc.querySelectorAll('script')) { | 165 for (var script in doc.querySelectorAll('script')) { |
| 163 if (script.attributes['type'] == TYPE_DART) { | 166 if (script.attributes['type'] == TYPE_DART_COMPONENT) { |
| 167 changed = true; |
| 164 script.remove(); | 168 script.remove(); |
| 165 var src = script.attributes['src']; | 169 var src = script.attributes['src']; |
| 166 scriptIds.add(uriToAssetId(docId, src, logger, script.sourceSpan)); | 170 scriptIds.add(uriToAssetId(docId, src, logger, script.sourceSpan)); |
| 167 | |
| 168 // only the first script needs to be added. | |
| 169 // others are already removed by _extractScripts | |
| 170 return; | |
| 171 } | 171 } |
| 172 } | 172 } |
| 173 return changed; |
| 173 } | 174 } |
| 174 | 175 |
| 175 /// Split inline scripts into their own files. We need to do this for dart2js | 176 /// Split inline scripts into their own files. We need to do this for dart2js |
| 176 /// to be able to compile them. | 177 /// to be able to compile them. |
| 177 /// | 178 /// |
| 178 /// This also validates that there weren't any duplicate scripts. | 179 /// This also validates that there weren't any duplicate scripts. |
| 179 bool _extractScripts(Document doc, AssetId sourceId) { | 180 bool _extractScripts(Document doc, AssetId sourceId) { |
| 180 bool changed = false; | 181 bool changed = false; |
| 181 bool first = true; | 182 bool first = true; |
| 182 for (var script in doc.querySelectorAll('script')) { | 183 for (var script in doc.querySelectorAll('script')) { |
| 183 if (script.attributes['type'] != TYPE_DART) continue; | 184 var type = script.attributes['type']; |
| 185 if (type != TYPE_DART_COMPONENT && type != TYPE_DART_APP) continue; |
| 184 | 186 |
| 185 // only one Dart script per document is supported in Dartium. | 187 // only one Dart script per document is supported in Dartium. |
| 186 if (!first) { | 188 if (type == TYPE_DART_APP) { |
| 187 // Remove the script. It's invalid to have more than one in Dartium. | 189 if (!first) logger.warning(COMPONENT_WARNING, span: script.sourceSpan); |
| 188 script.remove(); | 190 first = false; |
| 189 changed = true; | |
| 190 | |
| 191 // TODO(jmesserly): remove this when we are running linter. | |
| 192 logger.warning('more than one Dart script per HTML ' | |
| 193 'document is not supported. Script will be ignored.', | |
| 194 span: script.sourceSpan); | |
| 195 continue; | |
| 196 } | 191 } |
| 197 | 192 |
| 198 first = false; | |
| 199 | |
| 200 var src = script.attributes['src']; | 193 var src = script.attributes['src']; |
| 201 if (src != null) continue; | 194 if (src != null) continue; |
| 202 | 195 |
| 203 final filename = path.url.basename(docId.path); | 196 final filename = path.url.basename(docId.path); |
| 204 final count = inlineScriptCounter++; | 197 final count = inlineScriptCounter++; |
| 205 var code = script.text; | 198 var code = script.text; |
| 206 // TODO(sigmund): ensure this path is unique (dartbug.com/12618). | 199 // TODO(sigmund): ensure this path is unique (dartbug.com/12618). |
| 207 script.attributes['src'] = src = '$filename.$count.dart'; | 200 script.attributes['src'] = src = '$filename.$count.dart'; |
| 208 script.text = ''; | 201 script.text = ''; |
| 209 changed = true; | 202 changed = true; |
| 210 | 203 |
| 211 var newId = docId.addExtension('.$count.dart'); | 204 var newId = docId.addExtension('.$count.dart'); |
| 212 // TODO(jmesserly): consolidate this check with our other parsing of the | 205 // TODO(jmesserly): consolidate this check with our other parsing of the |
| 213 // Dart code, so we only parse it once. | 206 // Dart code, so we only parse it once. |
| 214 if (!_hasLibraryDirective(code)) { | 207 if (!_hasLibraryDirective(code)) { |
| 215 // Inject a library tag with an appropriate library name. | 208 // Inject a library tag with an appropriate library name. |
| 216 | 209 |
| 217 // Transform AssetId into a package name. For example: | 210 // Transform AssetId into a package name. For example: |
| 218 // myPkgName|lib/foo/bar.html -> myPkgName.foo.bar_html | 211 // myPkgName|lib/foo/bar.html -> myPkgName.foo.bar_html |
| 219 // myPkgName|web/foo/bar.html -> myPkgName.web.foo.bar_html | 212 // myPkgName|web/foo/bar.html -> myPkgName.web.foo.bar_html |
| 220 // This should roughly match the recommended library name conventions. | 213 // This should roughly match the recommended library name conventions. |
| 221 var libName = '${path.withoutExtension(sourceId.path)}_' | 214 var libName = '${path.withoutExtension(sourceId.path)}_' |
| 222 '${path.extension(sourceId.path).substring(1)}'; | 215 '${path.extension(sourceId.path).substring(1)}'; |
| 223 if (libName.startsWith('lib/')) libName = libName.substring(4); | 216 if (libName.startsWith('lib/')) libName = libName.substring(4); |
| 224 libName = libName.replaceAll('/', '.').replaceAll('-', '_'); | 217 libName = libName.replaceAll('/', '.').replaceAll('-', '_'); |
| 225 libName = '${sourceId.package}.$libName'; | 218 libName = '${sourceId.package}.${libName}_$count'; |
| 226 | 219 |
| 227 code = "library $libName;\n$code"; | 220 code = "library $libName;\n$code"; |
| 228 } | 221 } |
| 229 transform.addOutput(new Asset.fromString(newId, code)); | 222 transform.addOutput(new Asset.fromString(newId, code)); |
| 230 } | 223 } |
| 231 return changed; | 224 return changed; |
| 232 } | 225 } |
| 233 } | 226 } |
| 234 | 227 |
| 235 /// Parse [code] and determine whether it has a library directive. | 228 /// Parse [code] and determine whether it has a library directive. |
| (...skipping 15 matching lines...) Expand all Loading... |
| 251 ImportInliner(this.options); | 244 ImportInliner(this.options); |
| 252 | 245 |
| 253 /// Only run on entry point .html files. | 246 /// Only run on entry point .html files. |
| 254 Future<bool> isPrimary(Asset input) => | 247 Future<bool> isPrimary(Asset input) => |
| 255 new Future.value(options.isHtmlEntryPoint(input.id)); | 248 new Future.value(options.isHtmlEntryPoint(input.id)); |
| 256 | 249 |
| 257 Future apply(Transform transform) => | 250 Future apply(Transform transform) => |
| 258 new _HtmlInliner(options, transform).apply(); | 251 new _HtmlInliner(options, transform).apply(); |
| 259 } | 252 } |
| 260 | 253 |
| 261 const TYPE_DART = 'application/dart'; | 254 const TYPE_DART_APP = 'application/dart'; |
| 255 const TYPE_DART_COMPONENT = 'application/dart;component=1'; |
| 262 const TYPE_JS = 'text/javascript'; | 256 const TYPE_JS = 'text/javascript'; |
| 263 | 257 |
| 264 /// Internally adjusts urls in the html that we are about to inline. | 258 /// Internally adjusts urls in the html that we are about to inline. |
| 265 class _UrlNormalizer extends TreeVisitor { | 259 class _UrlNormalizer extends TreeVisitor { |
| 266 final Transform transform; | 260 final Transform transform; |
| 267 | 261 |
| 268 /// Asset where the original content (and original url) was found. | 262 /// Asset where the original content (and original url) was found. |
| 269 final AssetId sourceId; | 263 final AssetId sourceId; |
| 270 | 264 |
| 271 _UrlNormalizer(this.transform, this.sourceId); | 265 _UrlNormalizer(this.transform, this.sourceId); |
| 272 | 266 |
| 273 visitElement(Element node) { | 267 visitElement(Element node) { |
| 274 node.attributes.forEach((name, value) { | 268 node.attributes.forEach((name, value) { |
| 275 if (_urlAttributes.contains(name)) { | 269 if (_urlAttributes.contains(name)) { |
| 276 if (value != '' && !value.trim().startsWith('{{')) { | 270 if (value != '' && !value.trim().startsWith('{{')) { |
| 277 node.attributes[name] = _newUrl(value, node.sourceSpan); | 271 node.attributes[name] = _newUrl(value, node.sourceSpan); |
| 278 } | 272 } |
| 279 } | 273 } |
| 280 }); | 274 }); |
| 281 if (node.localName == 'style') { | 275 if (node.localName == 'style') { |
| 282 node.text = visitCss(node.text); | 276 node.text = visitCss(node.text); |
| 283 } else if (node.localName == 'script' && | 277 } else if (node.localName == 'script') { |
| 284 node.attributes['type'] == TYPE_DART) { | 278 var type = node.attributes['type']; |
| 285 // TODO(jmesserly): we might need to visit JS too to handle ES Harmony | 279 // TODO(jmesserly): we might need to visit JS too to handle ES Harmony |
| 286 // modules. | 280 // modules. |
| 287 node.text = visitInlineDart(node.text); | 281 if (type == TYPE_DART_APP || type == TYPE_DART_COMPONENT) { |
| 282 node.text = visitInlineDart(node.text); |
| 283 } |
| 288 } | 284 } |
| 289 super.visitElement(node); | 285 super.visitElement(node); |
| 290 } | 286 } |
| 291 | 287 |
| 292 static final _URL = new RegExp(r'url\(([^)]*)\)', multiLine: true); | 288 static final _URL = new RegExp(r'url\(([^)]*)\)', multiLine: true); |
| 293 static final _QUOTE = new RegExp('["\']', multiLine: true); | 289 static final _QUOTE = new RegExp('["\']', multiLine: true); |
| 294 | 290 |
| 295 /// Visit the CSS text and replace any relative URLs so we can inline it. | 291 /// Visit the CSS text and replace any relative URLs so we can inline it. |
| 296 // Ported from: | 292 // Ported from: |
| 297 // https://github.com/Polymer/vulcanize/blob/c14f63696797cda18dc3d372b78aa3378
acc691f/lib/vulcan.js#L149 | 293 // https://github.com/Polymer/vulcanize/blob/c14f63696797cda18dc3d372b78aa3378
acc691f/lib/vulcan.js#L149 |
| (...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 384 'formaction', // in button, input | 380 'formaction', // in button, input |
| 385 'href', // in a, area, link, base, command | 381 'href', // in a, area, link, base, command |
| 386 'icon', // in command | 382 'icon', // in command |
| 387 'manifest', // in html | 383 'manifest', // in html |
| 388 'poster', // in video | 384 'poster', // in video |
| 389 'src', // in audio, embed, iframe, img, input, script, source, track, | 385 'src', // in audio, embed, iframe, img, input, script, source, track, |
| 390 // video | 386 // video |
| 391 ]; | 387 ]; |
| 392 | 388 |
| 393 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); | 389 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); |
| 390 |
| 391 const COMPONENT_WARNING = |
| 392 'More than one Dart script per HTML document is not supported, but in the ' |
| 393 'near future Dartium will execute each tag as a separate isolate. If this ' |
| 394 'code is meant to load definitions that are part of the same application ' |
| 395 'you should switch it to use the "application/dart;component=1" mime-type ' |
| 396 'instead.'; |
| OLD | NEW |