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:barback/barback.dart'; | 12 import 'package:barback/barback.dart'; |
12 import 'package:path/path.dart' as path; | 13 import 'package:path/path.dart' as path; |
13 import 'package:html5lib/dom.dart' show | 14 import 'package:html5lib/dom.dart' show |
14 Document, DocumentFragment, Element, Node; | 15 Document, DocumentFragment, Element, Node; |
15 import 'package:html5lib/dom_parsing.dart' show TreeVisitor; | 16 import 'package:html5lib/dom_parsing.dart' show TreeVisitor; |
17 import 'package:source_maps/refactor.dart' show TextEditTransaction; | |
16 import 'package:source_maps/span.dart'; | 18 import 'package:source_maps/span.dart'; |
17 | 19 |
18 import 'code_extractor.dart'; // import just for documentation. | |
19 import 'common.dart'; | 20 import 'common.dart'; |
20 | 21 |
21 class _HtmlInliner extends PolymerTransformer { | 22 class _HtmlInliner extends PolymerTransformer { |
22 final TransformOptions options; | 23 final TransformOptions options; |
23 final Transform transform; | 24 final Transform transform; |
24 final TransformLogger logger; | 25 final TransformLogger logger; |
25 final AssetId docId; | 26 final AssetId docId; |
26 final seen = new Set<AssetId>(); | 27 final seen = new Set<AssetId>(); |
27 final scriptIds = <AssetId>[]; | 28 final scriptIds = <AssetId>[]; |
28 | 29 |
29 static const TYPE_DART = 'application/dart'; | |
30 static const TYPE_JS = 'text/javascript'; | |
31 | |
32 _HtmlInliner(this.options, Transform transform) | 30 _HtmlInliner(this.options, Transform transform) |
33 : transform = transform, | 31 : transform = transform, |
34 logger = transform.logger, | 32 logger = transform.logger, |
35 docId = transform.primaryInput.id; | 33 docId = transform.primaryInput.id; |
36 | 34 |
37 Future apply() { | 35 Future apply() { |
38 seen.add(docId); | 36 seen.add(docId); |
39 | 37 |
40 Document document; | 38 Document document; |
39 bool changed; | |
41 | 40 |
42 return readPrimaryAsHtml(transform).then((document) => | 41 return readPrimaryAsHtml(transform).then((doc) { |
43 _visitImports(document, docId).then((importsFound) { | 42 document = doc; |
43 // Add the main script's ID, or null if none is present. | |
44 // This will be used by ScriptCompactor. | |
45 changed = _extractScripts(document); | |
46 return _visitImports(document, docId); | |
47 }).then((importsFound) { | |
48 changed = changed || importsFound; | |
44 | 49 |
45 var output = transform.primaryInput; | 50 var output = transform.primaryInput; |
46 if (importsFound) { | 51 if (changed) output = new Asset.fromString(docId, document.outerHtml); |
47 output = new Asset.fromString(docId, document.outerHtml); | |
48 } | |
49 transform.addOutput(output); | 52 transform.addOutput(output); |
50 | 53 |
51 // We produce a secondary asset with extra information for later phases. | 54 // We produce a secondary asset with extra information for later phases. |
52 transform.addOutput(new Asset.fromString( | 55 transform.addOutput(new Asset.fromString( |
53 docId.addExtension('.scriptUrls'), | 56 docId.addExtension('.scriptUrls'), |
54 JSON.encode(scriptIds, toEncodable: (id) => id.serialize()))); | 57 JSON.encode(scriptIds, toEncodable: (id) => id.serialize()))); |
55 })); | 58 }); |
56 } | 59 } |
57 | 60 |
58 /// Visits imports in [document] and add the imported documents to documents. | 61 /// Visits imports in [document] and add the imported documents to documents. |
59 /// Documents are added in the order they appear, transitive imports are added | 62 /// Documents are added in the order they appear, transitive imports are added |
60 /// first. | 63 /// first. |
61 /// | 64 /// |
62 /// Returns `true` if and only if the document was changed and should be | 65 /// Returns `true` if and only if the document was changed and should be |
63 /// written out. | 66 /// written out. |
64 Future<bool> _visitImports(Document document, AssetId sourceId) { | 67 Future<bool> _visitImports(Document document, AssetId sourceId) { |
65 bool changed = false; | 68 bool changed = false; |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
100 /// respect to eachother, because stylesheets can be pulled in transitively | 103 /// respect to eachother, because stylesheets can be pulled in transitively |
101 /// from imports. | 104 /// from imports. |
102 // TODO(jmesserly): vulcanizer doesn't need this because they inline JS | 105 // TODO(jmesserly): vulcanizer doesn't need this because they inline JS |
103 // scripts, causing them to be naturally moved as part of the inlining. | 106 // scripts, causing them to be naturally moved as part of the inlining. |
104 // Should we do the same? Alternatively could we inline head into head and | 107 // Should we do the same? Alternatively could we inline head into head and |
105 // body into body and avoid this whole thing? | 108 // body into body and avoid this whole thing? |
106 void _moveHeadToBody(Document doc) { | 109 void _moveHeadToBody(Document doc) { |
107 var insertionPoint = doc.body.firstChild; | 110 var insertionPoint = doc.body.firstChild; |
108 for (var node in doc.head.nodes.toList(growable: false)) { | 111 for (var node in doc.head.nodes.toList(growable: false)) { |
109 if (node is! Element) continue; | 112 if (node is! Element) continue; |
110 var tag = node.tagName; | 113 var tag = node.localName; |
111 var type = node.attributes['type']; | 114 var type = node.attributes['type']; |
112 var rel = node.attributes['rel']; | 115 var rel = node.attributes['rel']; |
113 if (tag == 'style' || tag == 'script' && | 116 if (tag == 'style' || tag == 'script' && |
114 (type == null || type == TYPE_JS || type == TYPE_DART) || | 117 (type == null || type == TYPE_JS || type == TYPE_DART) || |
115 tag == 'link' && (rel == 'stylesheet' || rel == 'import')) { | 118 tag == 'link' && (rel == 'stylesheet' || rel == 'import')) { |
116 // Move the node into the body, where its contents will be placed. | 119 // Move the node into the body, where its contents will be placed. |
117 doc.body.insertBefore(node, insertionPoint); | 120 doc.body.insertBefore(node, insertionPoint); |
118 } | 121 } |
119 } | 122 } |
120 } | 123 } |
121 | 124 |
122 // Loads an asset identified by [id], visits its imports and collects its | 125 // Loads an asset identified by [id], visits its imports and collects its |
123 // html imports. Then inlines it into the main document. | 126 // html imports. Then inlines it into the main document. |
124 Future _inlineImport(AssetId id, Element link) => | 127 Future _inlineImport(AssetId id, Element link) => |
125 readAsHtml(id, transform).then((doc) => _visitImports(doc, id).then((_) { | 128 readAsHtml(id, transform).then((doc) => _visitImports(doc, id).then((_) { |
126 | 129 |
127 new _UrlNormalizer(transform, id).visit(doc); | 130 new _UrlNormalizer(transform, id).visit(doc); |
128 _extractScripts(doc); | 131 _extractScripts(doc, htmlImport: true); |
129 | 132 |
130 // TODO(jmesserly): figure out how this is working in vulcanizer. | 133 // TODO(jmesserly): figure out how this is working in vulcanizer. |
131 // Do they produce a <body> tag with a <head> and <body> inside? | 134 // Do they produce a <body> tag with a <head> and <body> inside? |
132 var imported = new DocumentFragment(); | 135 var imported = new DocumentFragment(); |
133 imported.nodes..addAll(doc.head.nodes)..addAll(doc.body.nodes); | 136 imported.nodes..addAll(doc.head.nodes)..addAll(doc.body.nodes); |
134 link.replaceWith(imported); | 137 link.replaceWith(imported); |
135 })); | 138 })); |
136 | 139 |
137 Future _inlineStylesheet(AssetId id, Element link) { | 140 Future _inlineStylesheet(AssetId id, Element link) { |
138 return transform.readInputAsString(id).then((css) { | 141 return transform.readInputAsString(id).then((css) { |
139 var url = spanUrlFor(id, transform); | 142 css = new _UrlNormalizer(transform, id).visitCss(css); |
140 css = new _UrlNormalizer(transform, id).visitCss(css, url); | |
141 link.replaceWith(new Element.tag('style')..text = css); | 143 link.replaceWith(new Element.tag('style')..text = css); |
142 }); | 144 }); |
143 } | 145 } |
144 | 146 |
145 /// Split Dart script tags from all the other elements. Now that Dartium | 147 /// Split Dart script tags from all the other elements. |
146 /// only allows a single script tag per page, we can't inline script | 148 /// |
147 /// tags. Instead, we collect the urls of each script tag so we import | 149 /// For inline scripts, we extract into a .dart file |
148 /// them directly from the Dart bootstrap code. | 150 /// |
149 void _extractScripts(Document document) { | 151 /// Dartium only allows a single script tag per page, we can't inline script |
152 /// tags. In [htmlImport]ed documents, we remove the script tag entirely so | |
153 /// we can inline the HTML. | |
154 /// | |
155 /// However we leave in place the script tag in the entry point HTML file, | |
156 /// so we run it at the correct point in time. | |
157 bool _extractScripts(Document document, {bool htmlImport: false}) { | |
Jennifer Messerly
2014/02/26 02:43:02
Meshing together these two functions didn't work q
| |
158 bool changed = false; | |
150 bool first = true; | 159 bool first = true; |
160 int count = 0; | |
151 for (var script in document.querySelectorAll('script')) { | 161 for (var script in document.querySelectorAll('script')) { |
152 if (script.attributes['type'] == TYPE_DART) { | 162 if (script.attributes['type'] == TYPE_DART) { |
153 script.remove(); | 163 if (htmlImport) { |
164 script.remove(); | |
165 changed = true; | |
166 } | |
154 | 167 |
155 // only one Dart script per document is supported in Dartium. | 168 // only one Dart script per document is supported in Dartium. |
156 if (first) { | 169 if (first) { |
157 first = false; | 170 first = false; |
158 | 171 |
159 var src = script.attributes['src']; | 172 var src = script.attributes['src']; |
160 if (src == null) { | 173 if (src == null) { |
161 logger.warning('unexpected script without a src url. The ' | 174 var filename = path.url.basename(docId.path); |
162 'ImportInliner transformer should run after running the ' | 175 var code = script.text; |
163 'InlineCodeExtractor', span: script.sourceSpan); | 176 // TODO(sigmund): ensure this path is unique (dartbug.com/12618). |
164 continue; | 177 script.attributes['src'] = src = '$filename.$count.dart'; |
178 script.text = ''; | |
179 changed = true; | |
180 | |
181 var newId = docId.addExtension('.$count.dart'); | |
182 if (!_hasLibraryDirective(code)) { | |
183 var libname = path.withoutExtension(newId.path) | |
184 .replaceAll(new RegExp('[-./]'), '_'); | |
185 code = "library $libname;\n$code"; | |
186 } | |
187 transform.addOutput(new Asset.fromString(newId, code)); | |
165 } | 188 } |
166 scriptIds.add(resolve(docId, src, logger, script.sourceSpan)); | |
167 | 189 |
190 if (htmlImport) { | |
191 scriptIds.add(resolve(docId, src, logger, script.sourceSpan)); | |
192 } | |
168 } else { | 193 } else { |
194 // Remove the script (if we didn't already). | |
195 // It's invalid to have more than one in Dartium. | |
196 script.remove(); | |
197 changed = true; | |
198 | |
169 // TODO(jmesserly): remove this when we are running linter. | 199 // TODO(jmesserly): remove this when we are running linter. |
170 logger.warning('more than one Dart script per HTML ' | 200 logger.warning('more than one Dart script per HTML ' |
171 'document is not supported. Script will be ignored.', | 201 'document is not supported. Script will be ignored.', |
172 span: script.sourceSpan); | 202 span: script.sourceSpan); |
173 } | 203 } |
174 } | 204 } |
175 } | 205 } |
206 return changed; | |
176 } | 207 } |
177 } | 208 } |
178 | 209 |
210 /// Parse [code] and determine whether it has a library directive. | |
211 bool _hasLibraryDirective(String code) => | |
212 parseCompilationUnit(code).directives.any((d) => d is LibraryDirective); | |
213 | |
214 | |
179 /// Recursively inlines the contents of HTML imports. Produces as output a | 215 /// Recursively inlines the contents of HTML imports. Produces as output a |
180 /// single HTML file that inlines the polymer-element definitions, and a text | 216 /// single HTML file that inlines the polymer-element definitions, and a text |
181 /// file that contains, in order, the URIs to each library that sourced in a | 217 /// file that contains, in order, the URIs to each library that sourced in a |
182 /// script tag. | 218 /// script tag. |
183 /// | 219 /// |
184 /// This transformer assumes that all script tags point to external files. To | 220 /// This transformer assumes that all script tags point to external files. To |
185 /// support script tags with inlined code, use this transformer after running | 221 /// support script tags with inlined code, use this transformer after running |
186 /// [InlineCodeExtractor] on an earlier phase. | 222 /// [InlineCodeExtractor] on an earlier phase. |
187 class ImportInliner extends Transformer { | 223 class ImportInliner extends Transformer { |
188 final TransformOptions options; | 224 final TransformOptions options; |
189 | 225 |
190 ImportInliner(this.options); | 226 ImportInliner(this.options); |
191 | 227 |
192 /// Only run on entry point .html files. | 228 /// Only run on entry point .html files. |
193 Future<bool> isPrimary(Asset input) => | 229 Future<bool> isPrimary(Asset input) => |
194 new Future.value(options.isHtmlEntryPoint(input.id)); | 230 new Future.value(options.isHtmlEntryPoint(input.id)); |
195 | 231 |
196 Future apply(Transform transform) => | 232 Future apply(Transform transform) => |
197 new _HtmlInliner(options, transform).apply(); | 233 new _HtmlInliner(options, transform).apply(); |
198 } | 234 } |
199 | 235 |
236 const TYPE_DART = 'application/dart'; | |
237 const TYPE_JS = 'text/javascript'; | |
200 | 238 |
201 /// Internally adjusts urls in the html that we are about to inline. | 239 /// Internally adjusts urls in the html that we are about to inline. |
202 class _UrlNormalizer extends TreeVisitor { | 240 class _UrlNormalizer extends TreeVisitor { |
203 final Transform transform; | 241 final Transform transform; |
204 | 242 |
205 /// Asset where the original content (and original url) was found. | 243 /// Asset where the original content (and original url) was found. |
206 final AssetId sourceId; | 244 final AssetId sourceId; |
207 | 245 |
208 _UrlNormalizer(this.transform, this.sourceId); | 246 _UrlNormalizer(this.transform, this.sourceId); |
209 | 247 |
210 visitElement(Element node) { | 248 visitElement(Element node) { |
211 for (var key in node.attributes.keys) { | 249 node.attributes.forEach((name, value) { |
212 if (_urlAttributes.contains(key)) { | 250 if (_urlAttributes.contains(name)) { |
213 var url = node.attributes[key]; | 251 if (value != '' && !value.trim().startsWith('{{')) { |
214 if (url != null && url != '' && !url.startsWith('{{')) { | 252 node.attributes[name] = _newUrl(value, node.sourceSpan); |
215 node.attributes[key] = _newUrl(url, node.sourceSpan); | |
216 } | 253 } |
217 } | 254 } |
255 }); | |
256 if (node.localName == 'style') { | |
257 node.text = visitCss(node.text); | |
258 } else if (node.localName == 'script' && | |
259 node.attributes['type'] == TYPE_DART) { | |
260 // TODO(jmesserly): we might need to visit JS too to handle ES Harmony | |
261 // modules. | |
262 node.text = visitInlineDart(node.text); | |
218 } | 263 } |
219 super.visitElement(node); | 264 super.visitElement(node); |
220 } | 265 } |
221 | 266 |
222 static final _URL = new RegExp(r'url\(([^)]*)\)', multiLine: true); | 267 static final _URL = new RegExp(r'url\(([^)]*)\)', multiLine: true); |
223 static final _QUOTE = new RegExp('["\']', multiLine: true); | 268 static final _QUOTE = new RegExp('["\']', multiLine: true); |
224 | 269 |
225 /// Visit the CSS text and replace any relative URLs so we can inline it. | 270 /// Visit the CSS text and replace any relative URLs so we can inline it. |
226 // Ported from: | 271 // Ported from: |
227 // https://github.com/Polymer/vulcanize/blob/c14f63696797cda18dc3d372b78aa3378 acc691f/lib/vulcan.js#L149 | 272 // https://github.com/Polymer/vulcanize/blob/c14f63696797cda18dc3d372b78aa3378 acc691f/lib/vulcan.js#L149 |
228 // TODO(jmesserly): use csslib here instead? Parsing with RegEx is sadness. | 273 // TODO(jmesserly): use csslib here instead? Parsing with RegEx is sadness. |
229 // Maybe it's reliable enough for finding URLs in CSS? I'm not sure. | 274 // Maybe it's reliable enough for finding URLs in CSS? I'm not sure. |
230 String visitCss(String cssText, String url) { | 275 String visitCss(String cssText) { |
276 var url = spanUrlFor(sourceId, transform); | |
231 var src = new SourceFile.text(url, cssText); | 277 var src = new SourceFile.text(url, cssText); |
232 return cssText.replaceAllMapped(_URL, (match) { | 278 return cssText.replaceAllMapped(_URL, (match) { |
233 // Extract the URL, without any surrounding quotes. | 279 // Extract the URL, without any surrounding quotes. |
234 var span = src.span(match.start, match.end); | 280 var span = src.span(match.start, match.end); |
235 var href = match[1].replaceAll(_QUOTE, ''); | 281 var href = match[1].replaceAll(_QUOTE, ''); |
236 href = _newUrl(href, span); | 282 href = _newUrl(href, span); |
237 return 'url($href)'; | 283 return 'url($href)'; |
238 }); | 284 }); |
239 } | 285 } |
240 | 286 |
241 _newUrl(String href, Span span) { | 287 String visitInlineDart(String code) { |
288 var unit = parseCompilationUnit(code); | |
289 var file = new SourceFile.text(spanUrlFor(sourceId, transform), code); | |
290 var output = new TextEditTransaction(code, file); | |
291 | |
292 for (Directive directive in unit.directives) { | |
293 if (directive is UriBasedDirective) { | |
294 var uri = directive.uri.stringValue; | |
295 var span = _getSpan(file, directive.uri); | |
296 | |
297 // TODO(jmesserly): _newUrl supports some things we shouldn't allow | |
298 // in Dart imports, such as assets/. | |
299 var targetUri = _newUrl(uri, span); | |
300 if (targetUri != uri) { | |
301 output.edit(span.start.offset, span.end.offset, "'$targetUri'"); | |
302 } | |
303 } | |
304 } | |
305 | |
306 if (!output.hasEdits) return code; | |
307 | |
308 // TODO(sigmund): emit source maps when barback supports it (see | |
309 // dartbug.com/12340) | |
310 return (output.commit()..build(file.url)).text; | |
311 | |
312 } | |
313 | |
314 String _newUrl(String href, Span span) { | |
242 var uri = Uri.parse(href); | 315 var uri = Uri.parse(href); |
243 if (uri.isAbsolute) return href; | 316 if (uri.isAbsolute) return href; |
244 if (!uri.scheme.isEmpty) return href; | 317 if (!uri.scheme.isEmpty) return href; |
245 if (!uri.host.isEmpty) return href; | 318 if (!uri.host.isEmpty) return href; |
246 if (uri.path.isEmpty) return href; // Implies standalone ? or # in URI. | 319 if (uri.path.isEmpty) return href; // Implies standalone ? or # in URI. |
247 if (path.isAbsolute(href)) return href; | 320 if (path.isAbsolute(href)) return href; |
248 | 321 |
249 var id = resolve(sourceId, href, transform.logger, span); | 322 var id = resolve(sourceId, href, transform.logger, span); |
250 if (id == null) return href; | 323 if (id == null) return href; |
251 var primaryId = transform.primaryInput.id; | 324 var primaryId = transform.primaryInput.id; |
(...skipping 30 matching lines...) Expand all Loading... | |
282 'cite', // in blockquote, del, ins, q | 355 'cite', // in blockquote, del, ins, q |
283 'data', // in object | 356 'data', // in object |
284 'formaction', // in button, input | 357 'formaction', // in button, input |
285 'href', // in a, area, link, base, command | 358 'href', // in a, area, link, base, command |
286 'icon', // in command | 359 'icon', // in command |
287 'manifest', // in html | 360 'manifest', // in html |
288 'poster', // in video | 361 'poster', // in video |
289 'src', // in audio, embed, iframe, img, input, script, source, track, | 362 'src', // in audio, embed, iframe, img, input, script, source, track, |
290 // video | 363 // video |
291 ]; | 364 ]; |
365 | |
366 _getSpan(SourceFile file, ASTNode node) => file.span(node.offset, node.end); | |
OLD | NEW |