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