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/src/generated/ast.dart'; | 12 import 'package:analyzer/src/generated/ast.dart'; |
12 import 'package:barback/barback.dart'; | 13 import 'package:barback/barback.dart'; |
13 import 'package:code_transformers/assets.dart'; | 14 import 'package:code_transformers/assets.dart'; |
14 import 'package:path/path.dart' as path; | 15 import 'package:path/path.dart' as path; |
15 import 'package:html5lib/dom.dart' show | 16 import 'package:html5lib/dom.dart' show |
16 Document, DocumentFragment, Element, Node; | 17 Document, DocumentFragment, Element, Node; |
17 import 'package:html5lib/dom_parsing.dart' show TreeVisitor; | 18 import 'package:html5lib/dom_parsing.dart' show TreeVisitor; |
18 import 'package:source_maps/refactor.dart' show TextEditTransaction; | 19 import 'package:source_maps/refactor.dart' show TextEditTransaction; |
19 import 'package:source_maps/span.dart'; | 20 import 'package:source_maps/span.dart'; |
20 | 21 |
(...skipping 23 matching lines...) Expand all Loading... |
44 seen.add(docId); | 45 seen.add(docId); |
45 | 46 |
46 Document document; | 47 Document document; |
47 bool changed; | 48 bool changed; |
48 | 49 |
49 return readPrimaryAsHtml(transform).then((doc) { | 50 return readPrimaryAsHtml(transform).then((doc) { |
50 document = doc; | 51 document = doc; |
51 experimentalBootstrap = document.querySelectorAll('link').any((link) => | 52 experimentalBootstrap = document.querySelectorAll('link').any((link) => |
52 link.attributes['rel'] == 'import' && | 53 link.attributes['rel'] == 'import' && |
53 link.attributes['href'] == POLYMER_EXPERIMENTAL_HTML); | 54 link.attributes['href'] == POLYMER_EXPERIMENTAL_HTML); |
54 changed = _extractScripts(document, docId); | 55 changed = _extractScripts(document); |
55 return _visitImports(document); | 56 return _visitImports(document); |
56 }).then((importsFound) { | 57 }).then((importsFound) { |
57 changed = changed || importsFound; | 58 changed = changed || importsFound; |
58 return _removeScripts(document); | 59 return _removeScripts(document); |
59 }).then((scriptsRemoved) { | 60 }).then((scriptsRemoved) { |
60 changed = changed || scriptsRemoved; | 61 changed = changed || scriptsRemoved; |
61 | 62 |
62 var output = transform.primaryInput; | 63 var output = transform.primaryInput; |
63 if (changed) output = new Asset.fromString(docId, document.outerHtml); | 64 if (changed) output = new Asset.fromString(docId, document.outerHtml); |
64 transform.addOutput(output); | 65 transform.addOutput(output); |
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
137 } | 138 } |
138 } | 139 } |
139 } | 140 } |
140 | 141 |
141 /// Loads an asset identified by [id], visits its imports and collects its | 142 /// Loads an asset identified by [id], visits its imports and collects its |
142 /// html imports. Then inlines it into the main document. | 143 /// html imports. Then inlines it into the main document. |
143 Future _inlineImport(AssetId id, Element link) { | 144 Future _inlineImport(AssetId id, Element link) { |
144 return readAsHtml(id, transform).then((doc) { | 145 return readAsHtml(id, transform).then((doc) { |
145 new _UrlNormalizer(transform, id).visit(doc); | 146 new _UrlNormalizer(transform, id).visit(doc); |
146 return _visitImports(doc).then((_) { | 147 return _visitImports(doc).then((_) { |
147 _extractScripts(doc, id); | 148 // _UrlNormalizer already ensures there is a library name. |
| 149 _extractScripts(doc, injectLibraryName: false); |
148 | 150 |
149 // TODO(jmesserly): figure out how this is working in vulcanizer. | 151 // TODO(jmesserly): figure out how this is working in vulcanizer. |
150 // Do they produce a <body> tag with a <head> and <body> inside? | 152 // Do they produce a <body> tag with a <head> and <body> inside? |
151 var imported = new DocumentFragment(); | 153 var imported = new DocumentFragment(); |
152 imported.nodes..addAll(doc.head.nodes)..addAll(doc.body.nodes); | 154 imported.nodes..addAll(doc.head.nodes)..addAll(doc.body.nodes); |
153 link.replaceWith(imported); | 155 link.replaceWith(imported); |
154 }); | 156 }); |
155 }); | 157 }); |
156 } | 158 } |
157 | 159 |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
191 } | 193 } |
192 }); | 194 }); |
193 } | 195 } |
194 }).then((_) => changed); | 196 }).then((_) => changed); |
195 } | 197 } |
196 | 198 |
197 /// Split inline scripts into their own files. We need to do this for dart2js | 199 /// Split inline scripts into their own files. We need to do this for dart2js |
198 /// to be able to compile them. | 200 /// to be able to compile them. |
199 /// | 201 /// |
200 /// This also validates that there weren't any duplicate scripts. | 202 /// This also validates that there weren't any duplicate scripts. |
201 bool _extractScripts(Document doc, AssetId sourceId) { | 203 bool _extractScripts(Document doc, {bool injectLibraryName: true}) { |
202 bool changed = false; | 204 bool changed = false; |
203 for (var script in doc.querySelectorAll('script')) { | 205 for (var script in doc.querySelectorAll('script')) { |
204 if (script.attributes['type'] != TYPE_DART) continue; | 206 if (script.attributes['type'] != TYPE_DART) continue; |
205 | 207 |
206 var src = script.attributes['src']; | 208 var src = script.attributes['src']; |
207 if (src != null) continue; | 209 if (src != null) continue; |
208 | 210 |
209 final filename = path.url.basename(docId.path); | 211 final filename = path.url.basename(docId.path); |
210 final count = inlineScriptCounter++; | 212 final count = inlineScriptCounter++; |
211 var code = script.text; | 213 var code = script.text; |
212 // TODO(sigmund): ensure this path is unique (dartbug.com/12618). | 214 // TODO(sigmund): ensure this path is unique (dartbug.com/12618). |
213 script.attributes['src'] = src = '$filename.$count.dart'; | 215 script.attributes['src'] = src = '$filename.$count.dart'; |
214 script.text = ''; | 216 script.text = ''; |
215 changed = true; | 217 changed = true; |
216 | 218 |
217 var newId = docId.addExtension('.$count.dart'); | 219 var newId = docId.addExtension('.$count.dart'); |
218 // TODO(jmesserly): consolidate this check with our other parsing of the | 220 if (injectLibraryName && !_hasLibraryDirective(code)) { |
219 // Dart code, so we only parse it once. | 221 var libName = _libraryNameFor(docId, count); |
220 if (!_hasLibraryDirective(code)) { | |
221 // Inject a library tag with an appropriate library name. | |
222 | |
223 // Transform AssetId into a package name. For example: | |
224 // myPkgName|lib/foo/bar.html -> myPkgName.foo.bar_html | |
225 // myPkgName|web/foo/bar.html -> myPkgName.web.foo.bar_html | |
226 // This should roughly match the recommended library name conventions. | |
227 var libName = '${path.withoutExtension(sourceId.path)}_' | |
228 '${path.extension(sourceId.path).substring(1)}'; | |
229 if (libName.startsWith('lib/')) libName = libName.substring(4); | |
230 libName = libName.replaceAll('/', '.').replaceAll('-', '_'); | |
231 libName = '${sourceId.package}.${libName}_$count'; | |
232 | |
233 code = "library $libName;\n$code"; | 222 code = "library $libName;\n$code"; |
234 } | 223 } |
235 extractedFiles.add(newId); | 224 extractedFiles.add(newId); |
236 transform.addOutput(new Asset.fromString(newId, code)); | 225 transform.addOutput(new Asset.fromString(newId, code)); |
237 } | 226 } |
238 return changed; | 227 return changed; |
239 } | 228 } |
240 } | 229 } |
241 | 230 |
| 231 /// Transform AssetId into a library name. For example: |
| 232 /// |
| 233 /// myPkgName|lib/foo/bar.html -> myPkgName.foo.bar_html |
| 234 /// myPkgName|web/foo/bar.html -> myPkgName.web.foo.bar_html |
| 235 /// |
| 236 /// This should roughly match the recommended library name conventions. |
| 237 String _libraryNameFor(AssetId id, int suffix) { |
| 238 var name = '${path.withoutExtension(id.path)}_' |
| 239 '${path.extension(id.path).substring(1)}'; |
| 240 if (name.startsWith('lib/')) name = name.substring(4); |
| 241 name = name.replaceAll('/', '.').replaceAll('-', '_'); |
| 242 return '${id.package}.${name}_$suffix'; |
| 243 } |
| 244 |
242 /// Parse [code] and determine whether it has a library directive. | 245 /// Parse [code] and determine whether it has a library directive. |
243 bool _hasLibraryDirective(String code) => | 246 bool _hasLibraryDirective(String code) => |
244 parseCompilationUnit(code).directives.any((d) => d is LibraryDirective); | 247 parseDirectives(code, suppressErrors: true) |
| 248 .directives.any((d) => d is LibraryDirective); |
245 | 249 |
246 | 250 |
247 /// Recursively inlines the contents of HTML imports. Produces as output a | 251 /// Recursively inlines the contents of HTML imports. Produces as output a |
248 /// single HTML file that inlines the polymer-element definitions, and a text | 252 /// single HTML file that inlines the polymer-element definitions, and a text |
249 /// file that contains, in order, the URIs to each library that sourced in a | 253 /// file that contains, in order, the URIs to each library that sourced in a |
250 /// script tag. | 254 /// script tag. |
251 /// | 255 /// |
252 /// This transformer assumes that all script tags point to external files. To | 256 /// This transformer assumes that all script tags point to external files. To |
253 /// support script tags with inlined code, use this transformer after running | 257 /// support script tags with inlined code, use this transformer after running |
254 /// [InlineCodeExtractor] on an earlier phase. | 258 /// [InlineCodeExtractor] on an earlier phase. |
(...skipping 17 matching lines...) Expand all Loading... |
272 const TYPE_DART = 'application/dart'; | 276 const TYPE_DART = 'application/dart'; |
273 const TYPE_JS = 'text/javascript'; | 277 const TYPE_JS = 'text/javascript'; |
274 | 278 |
275 /// Internally adjusts urls in the html that we are about to inline. | 279 /// Internally adjusts urls in the html that we are about to inline. |
276 class _UrlNormalizer extends TreeVisitor { | 280 class _UrlNormalizer extends TreeVisitor { |
277 final Transform transform; | 281 final Transform transform; |
278 | 282 |
279 /// Asset where the original content (and original url) was found. | 283 /// Asset where the original content (and original url) was found. |
280 final AssetId sourceId; | 284 final AssetId sourceId; |
281 | 285 |
| 286 /// Counter used to ensure that every library name we inject is unique. |
| 287 int _count = 0; |
| 288 |
282 _UrlNormalizer(this.transform, this.sourceId); | 289 _UrlNormalizer(this.transform, this.sourceId); |
283 | 290 |
284 visitElement(Element node) { | 291 visitElement(Element node) { |
285 node.attributes.forEach((name, value) { | 292 node.attributes.forEach((name, value) { |
286 if (_urlAttributes.contains(name)) { | 293 if (_urlAttributes.contains(name)) { |
287 if (value != '' && !value.trim().startsWith('{{')) { | 294 if (value != '' && !value.trim().startsWith('{{')) { |
288 node.attributes[name] = _newUrl(value, node.sourceSpan); | 295 node.attributes[name] = _newUrl(value, node.sourceSpan); |
289 } | 296 } |
290 } | 297 } |
291 }); | 298 }); |
292 if (node.localName == 'style') { | 299 if (node.localName == 'style') { |
293 node.text = visitCss(node.text); | 300 node.text = visitCss(node.text); |
294 } else if (node.localName == 'script' && | 301 } else if (node.localName == 'script' && |
295 node.attributes['type'] == TYPE_DART) { | 302 node.attributes['type'] == TYPE_DART && |
| 303 !node.attributes.containsKey('src')) { |
296 // TODO(jmesserly): we might need to visit JS too to handle ES Harmony | 304 // TODO(jmesserly): we might need to visit JS too to handle ES Harmony |
297 // modules. | 305 // modules. |
298 node.text = visitInlineDart(node.text); | 306 node.text = visitInlineDart(node.text); |
299 } | 307 } |
300 super.visitElement(node); | 308 super.visitElement(node); |
301 } | 309 } |
302 | 310 |
303 static final _URL = new RegExp(r'url\(([^)]*)\)', multiLine: true); | 311 static final _URL = new RegExp(r'url\(([^)]*)\)', multiLine: true); |
304 static final _QUOTE = new RegExp('["\']', multiLine: true); | 312 static final _QUOTE = new RegExp('["\']', multiLine: true); |
305 | 313 |
306 /// Visit the CSS text and replace any relative URLs so we can inline it. | 314 /// Visit the CSS text and replace any relative URLs so we can inline it. |
307 // Ported from: | 315 // Ported from: |
308 // https://github.com/Polymer/vulcanize/blob/c14f63696797cda18dc3d372b78aa3378
acc691f/lib/vulcan.js#L149 | 316 // https://github.com/Polymer/vulcanize/blob/c14f63696797cda18dc3d372b78aa3378
acc691f/lib/vulcan.js#L149 |
309 // TODO(jmesserly): use csslib here instead? Parsing with RegEx is sadness. | 317 // TODO(jmesserly): use csslib here instead? Parsing with RegEx is sadness. |
310 // Maybe it's reliable enough for finding URLs in CSS? I'm not sure. | 318 // Maybe it's reliable enough for finding URLs in CSS? I'm not sure. |
311 String visitCss(String cssText) { | 319 String visitCss(String cssText) { |
312 var url = spanUrlFor(sourceId, transform); | 320 var url = spanUrlFor(sourceId, transform); |
313 var src = new SourceFile.text(url, cssText); | 321 var src = new SourceFile.text(url, cssText); |
314 return cssText.replaceAllMapped(_URL, (match) { | 322 return cssText.replaceAllMapped(_URL, (match) { |
315 // Extract the URL, without any surrounding quotes. | 323 // Extract the URL, without any surrounding quotes. |
316 var span = src.span(match.start, match.end); | 324 var span = src.span(match.start, match.end); |
317 var href = match[1].replaceAll(_QUOTE, ''); | 325 var href = match[1].replaceAll(_QUOTE, ''); |
318 href = _newUrl(href, span); | 326 href = _newUrl(href, span); |
319 return 'url($href)'; | 327 return 'url($href)'; |
320 }); | 328 }); |
321 } | 329 } |
322 | 330 |
323 String visitInlineDart(String code) { | 331 String visitInlineDart(String code) { |
324 var unit = parseCompilationUnit(code); | 332 var unit = parseDirectives(code, suppressErrors: true); |
325 var file = new SourceFile.text(spanUrlFor(sourceId, transform), code); | 333 var file = new SourceFile.text(spanUrlFor(sourceId, transform), code); |
326 var output = new TextEditTransaction(code, file); | 334 var output = new TextEditTransaction(code, file); |
327 | 335 var foundLibraryDirective = false; |
328 for (Directive directive in unit.directives) { | 336 for (Directive directive in unit.directives) { |
329 if (directive is UriBasedDirective) { | 337 if (directive is UriBasedDirective) { |
330 var uri = directive.uri.stringValue; | 338 var uri = directive.uri.stringValue; |
331 var span = _getSpan(file, directive.uri); | 339 var span = _getSpan(file, directive.uri); |
332 | 340 |
333 var id = uriToAssetId(sourceId, uri, transform.logger, span, | 341 var id = uriToAssetId(sourceId, uri, transform.logger, span, |
334 errorOnAbsolute: false); | 342 errorOnAbsolute: false); |
335 if (id == null) continue; | 343 if (id == null) continue; |
336 | 344 |
337 var primaryId = transform.primaryInput.id; | 345 var primaryId = transform.primaryInput.id; |
338 var newUri = assetUrlFor(id, primaryId, transform.logger); | 346 var newUri = assetUrlFor(id, primaryId, transform.logger); |
339 if (newUri != uri) { | 347 if (newUri != uri) { |
340 output.edit(span.start.offset, span.end.offset, "'$newUri'"); | 348 output.edit(span.start.offset, span.end.offset, "'$newUri'"); |
341 } | 349 } |
| 350 } else if (directive is LibraryDirective) { |
| 351 foundLibraryDirective = true; |
342 } | 352 } |
343 } | 353 } |
344 | 354 |
| 355 if (!foundLibraryDirective) { |
| 356 // Ensure all inline scripts also have a library name. |
| 357 var libName = _libraryNameFor(sourceId, _count++); |
| 358 output.edit(0, 0, "library $libName;\n"); |
| 359 } |
| 360 |
345 if (!output.hasEdits) return code; | 361 if (!output.hasEdits) return code; |
346 | 362 |
347 // TODO(sigmund): emit source maps when barback supports it (see | 363 // TODO(sigmund): emit source maps when barback supports it (see |
348 // dartbug.com/12340) | 364 // dartbug.com/12340) |
349 return (output.commit()..build(file.url)).text; | 365 return (output.commit()..build(file.url)).text; |
350 } | 366 } |
351 | 367 |
352 String _newUrl(String href, Span span) { | 368 String _newUrl(String href, Span span) { |
353 var uri = Uri.parse(href); | 369 var uri = Uri.parse(href); |
354 if (uri.isAbsolute) return href; | 370 if (uri.isAbsolute) return href; |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
395 'formaction', // in button, input | 411 'formaction', // in button, input |
396 'href', // in a, area, link, base, command | 412 'href', // in a, area, link, base, command |
397 'icon', // in command | 413 'icon', // in command |
398 'manifest', // in html | 414 'manifest', // in html |
399 'poster', // in video | 415 'poster', // in video |
400 'src', // in audio, embed, iframe, img, input, script, source, track, | 416 'src', // in audio, embed, iframe, img, input, script, source, track, |
401 // video | 417 // video |
402 ]; | 418 ]; |
403 | 419 |
404 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); | 420 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); |
OLD | NEW |