Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(116)

Side by Side Diff: observatory_pub_packages/polymer/src/build/import_inliner.dart

Issue 816693004: Add observatory_pub_packages snapshot to third_party (Closed) Base URL: http://dart.googlecode.com/svn/third_party/
Patch Set: Created 6 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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
3 // BSD-style license that can be found in the LICENSE file.
4
5 /// Transfomer that inlines polymer-element definitions from html imports.
6 library polymer.src.build.import_inliner;
7
8 import 'dart:async';
9 import 'dart:convert';
10 import 'dart:collection' show LinkedHashSet;
11
12 import 'package:analyzer/analyzer.dart';
13 import 'package:analyzer/src/generated/ast.dart';
14 import 'package:barback/barback.dart';
15 import 'package:code_transformers/assets.dart';
16 import 'package:code_transformers/messages/build_logger.dart';
17 import 'package:path/path.dart' as path;
18 import 'package:html5lib/dom.dart' show
19 Document, DocumentFragment, Element, Node;
20 import 'package:html5lib/dom_parsing.dart' show TreeVisitor;
21 import 'package:source_maps/refactor.dart' show TextEditTransaction;
22 import 'package:source_span/source_span.dart';
23
24 import 'common.dart';
25 import 'messages.dart';
26
27 // TODO(sigmund): move to web_components package (dartbug.com/18037).
28 class _HtmlInliner extends PolymerTransformer {
29 final TransformOptions options;
30 final Transform transform;
31 final BuildLogger logger;
32 final AssetId docId;
33 final seen = new Set<AssetId>();
34 final scriptIds = new LinkedHashSet<AssetId>();
35 final inlinedStylesheetIds = new Set<AssetId>();
36 final extractedFiles = new Set<AssetId>();
37 bool experimentalBootstrap = false;
38 final Element importsWrapper = new Element.html('<div hidden></div>');
39
40 /// The number of extracted inline Dart scripts. Used as a counter to give
41 /// unique-ish filenames.
42 int inlineScriptCounter = 0;
43
44 _HtmlInliner(TransformOptions options, Transform transform)
45 : options = options,
46 transform = transform,
47 logger = new BuildLogger(transform,
48 convertErrorsToWarnings: !options.releaseMode,
49 detailsUri: 'http://goo.gl/5HPeuP'),
50 docId = transform.primaryInput.id;
51
52 Future apply() {
53 seen.add(docId);
54
55 Document document;
56 bool changed = false;
57
58 return readPrimaryAsHtml(transform, logger).then((doc) {
59 document = doc;
60
61 // Insert our importsWrapper. This may be removed later if not needed, but
62 // it makes the logic simpler to have it in the document.
63 document.body.insertBefore(importsWrapper, document.body.firstChild);
64
65 changed = new _UrlNormalizer(transform, docId, logger).visit(document)
66 || changed;
67
68 experimentalBootstrap = document.querySelectorAll('link').any((link) =>
69 link.attributes['rel'] == 'import' &&
70 link.attributes['href'] == POLYMER_EXPERIMENTAL_HTML);
71 changed = _extractScripts(document) || changed;
72
73 // We only need to move the head into the body for the entry point.
74 _moveHeadToBody(document);
75
76 return _visitImports(document);
77 }).then((importsFound) {
78 changed = changed || importsFound;
79
80 return _removeScripts(document);
81 }).then((scriptsRemoved) {
82 changed = changed || scriptsRemoved;
83
84 // Remove the importsWrapper if it contains nothing. Wait until now to do
85 // this since it might have a script that got removed, and thus no longer
86 // have any children.
87 if (importsWrapper.children.isEmpty) importsWrapper.remove();
88
89 var output = transform.primaryInput;
90 if (changed) output = new Asset.fromString(docId, document.outerHtml);
91 transform.addOutput(output);
92
93 // We produce a secondary asset with extra information for later phases.
94 transform.addOutput(new Asset.fromString(
95 docId.addExtension('._data'),
96 JSON.encode({
97 'experimental_bootstrap': experimentalBootstrap,
98 'script_ids': scriptIds.toList(),
99 }, toEncodable: (id) => id.serialize())));
100
101 // Write out the logs collected by our [BuildLogger].
102 if (options.injectBuildLogsInOutput) {
103 return logger.writeOutput();
104 }
105 });
106 }
107
108 /// Visits imports in [document] and add the imported documents to documents.
109 /// Documents are added in the order they appear, transitive imports are added
110 /// first.
111 ///
112 /// Returns `true` if and only if the document was changed and should be
113 /// written out.
114 Future<bool> _visitImports(Document document) {
115 bool changed = false;
116
117 // Note: we need to preserve the import order in the generated output.
118 return Future.forEach(document.querySelectorAll('link'), (Element tag) {
119 var rel = tag.attributes['rel'];
120 if (rel != 'import' && rel != 'stylesheet') return null;
121
122 // Note: URL has already been normalized so use docId.
123 var href = tag.attributes['href'];
124 var id = uriToAssetId(docId, href, logger, tag.sourceSpan,
125 errorOnAbsolute: rel != 'stylesheet');
126
127 if (rel == 'import') {
128 changed = true;
129 if (id == null || !seen.add(id)) {
130 tag.remove();
131 return null;
132 }
133 return _inlineImport(id, tag);
134
135 } else if (rel == 'stylesheet') {
136 if (id == null) return null;
137 if (!options.shouldInlineStylesheet(id)) return null;
138
139 changed = true;
140 if (inlinedStylesheetIds.contains(id)
141 && !options.stylesheetInliningIsOverridden(id)) {
142 logger.warning(
143 CSS_FILE_INLINED_MULTIPLE_TIMES.create({'url': id.path}),
144 span: tag.sourceSpan);
145 }
146 inlinedStylesheetIds.add(id);
147 return _inlineStylesheet(id, tag);
148 }
149 }).then((_) => changed);
150 }
151
152 /// To preserve the order of scripts with respect to inlined
153 /// link rel=import, we move both of those into the body before we do any
154 /// inlining. We do not start doing this until the first import is found
155 /// however, as some scripts do need to be ran in the head to work
156 /// properly (webcomponents.js for instance).
157 ///
158 /// Note: we do this for stylesheets as well to preserve ordering with
159 /// respect to eachother, because stylesheets can be pulled in transitively
160 /// from imports.
161 // TODO(jmesserly): vulcanizer doesn't need this because they inline JS
162 // scripts, causing them to be naturally moved as part of the inlining.
163 // Should we do the same? Alternatively could we inline head into head and
164 // body into body and avoid this whole thing?
165 void _moveHeadToBody(Document doc) {
166 var foundImport = false;
167 for (var node in doc.head.nodes.toList(growable: false)) {
168 if (node is! Element) continue;
169 var tag = node.localName;
170 var type = node.attributes['type'];
171 var rel = node.attributes['rel'];
172 if (tag == 'link' && rel == 'import') foundImport = true;
173 if (!foundImport) continue;
174 if (tag == 'style' || tag == 'script' &&
175 (type == null || type == TYPE_JS || type == TYPE_DART) ||
176 tag == 'link' && (rel == 'stylesheet' || rel == 'import')) {
177 // Move the node into the importsWrapper, where its contents will be
178 // placed. This wrapper is a hidden div to prevent inlined html from
179 // causing a FOUC.
180 importsWrapper.append(node);
181 }
182 }
183 }
184
185 /// Loads an asset identified by [id], visits its imports and collects its
186 /// html imports. Then inlines it into the main document.
187 Future _inlineImport(AssetId id, Element link) {
188 return readAsHtml(id, transform, logger).catchError((error) {
189 logger.error(INLINE_IMPORT_FAIL.create({'error': error}),
190 span: link.sourceSpan);
191 }).then((doc) {
192 if (doc == null) return false;
193 new _UrlNormalizer(transform, id, logger).visit(doc);
194 return _visitImports(doc).then((_) {
195 // _UrlNormalizer already ensures there is a library name.
196 _extractScripts(doc, injectLibraryName: false);
197
198 // TODO(jmesserly): figure out how this is working in vulcanizer.
199 // Do they produce a <body> tag with a <head> and <body> inside?
200 var imported = new DocumentFragment();
201 imported.nodes..addAll(doc.head.nodes)..addAll(doc.body.nodes);
202 link.replaceWith(imported);
203
204 // Make sure to grab any logs from the inlined import.
205 return logger.addLogFilesFromAsset(id);
206 });
207 });
208 }
209
210 Future _inlineStylesheet(AssetId id, Element link) {
211 return transform.readInputAsString(id).catchError((error) {
212 // TODO(jakemac): Move this warning to the linter once we can make it run
213 // always (see http://dartbug.com/17199). Then hide this error and replace
214 // with a comment pointing to the linter error (so we don't double warn).
215 logger.warning(INLINE_STYLE_FAIL.create({'error': error}),
216 span: link.sourceSpan);
217 }).then((css) {
218 if (css == null) return null;
219 css = new _UrlNormalizer(transform, id, logger).visitCss(css);
220 var styleElement = new Element.tag('style')..text = css;
221 // Copy over the extra attributes from the link tag to the style tag.
222 // This adds support for no-shim, shim-shadowdom, etc.
223 link.attributes.forEach((key, value) {
224 if (!IGNORED_LINKED_STYLE_ATTRS.contains(key)) {
225 styleElement.attributes[key] = value;
226 }
227 });
228 link.replaceWith(styleElement);
229 });
230 }
231
232 /// Remove all Dart scripts and remember their [AssetId]s for later use.
233 ///
234 /// Dartium only allows a single script tag per page, so we can't inline
235 /// the script tags. Instead we remove them entirely.
236 Future<bool> _removeScripts(Document doc) {
237 bool changed = false;
238 return Future.forEach(doc.querySelectorAll('script'), (script) {
239 if (script.attributes['type'] == TYPE_DART) {
240 changed = true;
241 script.remove();
242 var src = script.attributes['src'];
243 var srcId = uriToAssetId(docId, src, logger, script.sourceSpan);
244
245 // No duplicates allowed!
246 if (scriptIds.contains(srcId)) {
247 logger.warning(SCRIPT_INCLUDED_MORE_THAN_ONCE.create({'url': src}),
248 span: script.sourceSpan);
249 return true;
250 }
251
252 // We check for extractedFiles because 'hasInput' below is only true for
253 // assets that existed before this transformer runs (hasInput is false
254 // for files created by [_extractScripts]).
255 if (extractedFiles.contains(srcId)) {
256 scriptIds.add(srcId);
257 return true;
258 }
259
260 return transform.hasInput(srcId).then((exists) {
261 if (!exists) {
262 logger.warning(SCRIPT_FILE_NOT_FOUND.create({'url': src}),
263 span: script.sourceSpan);
264 } else {
265 scriptIds.add(srcId);
266 }
267 });
268 }
269 }).then((_) => changed);
270 }
271
272 /// Split inline scripts into their own files. We need to do this for dart2js
273 /// to be able to compile them.
274 ///
275 /// This also validates that there weren't any duplicate scripts.
276 bool _extractScripts(Document doc, {bool injectLibraryName: true}) {
277 bool changed = false;
278 for (var script in doc.querySelectorAll('script')) {
279 var src = script.attributes['src'];
280 if (src != null) continue;
281
282 var type = script.attributes['type'];
283 var isDart = type == TYPE_DART;
284
285 var shouldExtract = isDart ||
286 (options.contentSecurityPolicy && (type == null || type == TYPE_JS));
287 if (!shouldExtract) continue;
288
289 var extension = isDart ? 'dart' : 'js';
290 final filename = path.url.basename(docId.path);
291 final count = inlineScriptCounter++;
292 var code = script.text;
293 // TODO(sigmund): ensure this path is unique (dartbug.com/12618).
294 script.attributes['src'] = src = '$filename.$count.$extension';
295 script.text = '';
296 changed = true;
297
298 var newId = docId.addExtension('.$count.$extension');
299 if (isDart && injectLibraryName && !_hasLibraryDirective(code)) {
300 var libName = _libraryNameFor(docId, count);
301 code = "library $libName;\n$code";
302 }
303 extractedFiles.add(newId);
304 transform.addOutput(new Asset.fromString(newId, code));
305 }
306 return changed;
307 }
308 }
309
310 /// Transform AssetId into a library name. For example:
311 ///
312 /// myPkgName|lib/foo/bar.html -> myPkgName.foo.bar_html
313 /// myPkgName|web/foo/bar.html -> myPkgName.web.foo.bar_html
314 ///
315 /// This should roughly match the recommended library name conventions.
316 String _libraryNameFor(AssetId id, int suffix) {
317 var name = '${path.withoutExtension(id.path)}_'
318 '${path.extension(id.path).substring(1)}';
319 if (name.startsWith('lib/')) name = name.substring(4);
320 name = name.split('/').map((part) {
321 part = part.replaceAll(_INVALID_LIB_CHARS_REGEX, '_');
322 if (part.startsWith(_NUM_REGEX)) part = '_${part}';
323 return part;
324 }).join(".");
325 return '${id.package}.${name}_$suffix';
326 }
327
328 /// Parse [code] and determine whether it has a library directive.
329 bool _hasLibraryDirective(String code) =>
330 parseDirectives(code, suppressErrors: true)
331 .directives.any((d) => d is LibraryDirective);
332
333
334 /// Recursively inlines the contents of HTML imports. Produces as output a
335 /// single HTML file that inlines the polymer-element definitions, and a text
336 /// file that contains, in order, the URIs to each library that sourced in a
337 /// script tag.
338 ///
339 /// This transformer assumes that all script tags point to external files. To
340 /// support script tags with inlined code, use this transformer after running
341 /// [InlineCodeExtractor] on an earlier phase.
342 class ImportInliner extends Transformer {
343 final TransformOptions options;
344
345 ImportInliner(this.options);
346
347 /// Only run on entry point .html files.
348 // TODO(nweiz): This should just take an AssetId when barback <0.13.0 support
349 // is dropped.
350 Future<bool> isPrimary(idOrAsset) {
351 var id = idOrAsset is AssetId ? idOrAsset : idOrAsset.id;
352 return new Future.value(options.isHtmlEntryPoint(id));
353 }
354
355 Future apply(Transform transform) =>
356 new _HtmlInliner(options, transform).apply();
357 }
358
359 const TYPE_DART = 'application/dart';
360 const TYPE_JS = 'text/javascript';
361
362 /// Internally adjusts urls in the html that we are about to inline.
363 class _UrlNormalizer extends TreeVisitor {
364 final Transform transform;
365
366 /// Asset where the original content (and original url) was found.
367 final AssetId sourceId;
368
369 /// Counter used to ensure that every library name we inject is unique.
370 int _count = 0;
371
372 /// Path to the top level folder relative to the transform primaryInput.
373 /// This should just be some arbitrary # of ../'s.
374 final String topLevelPath;
375
376 /// Whether or not the normalizer has changed something in the tree.
377 bool changed = false;
378
379 final BuildLogger logger;
380
381 _UrlNormalizer(transform, this.sourceId, this.logger)
382 : transform = transform,
383 topLevelPath =
384 '../' * (transform.primaryInput.id.path.split('/').length - 2);
385
386 visit(Node node) {
387 super.visit(node);
388 return changed;
389 }
390
391 visitElement(Element node) {
392 // TODO(jakemac): Support custom elements that extend html elements which
393 // have url-like attributes. This probably means keeping a list of which
394 // html elements support each url-like attribute.
395 if (!isCustomTagName(node.localName)) {
396 node.attributes.forEach((name, value) {
397 if (_urlAttributes.contains(name)) {
398 if (!name.startsWith('_') && value.contains(_BINDING_REGEX)) {
399 logger.warning(USE_UNDERSCORE_PREFIX.create({'name': name}),
400 span: node.sourceSpan, asset: sourceId);
401 } else if (name.startsWith('_') && !value.contains(_BINDING_REGEX)) {
402 logger.warning(DONT_USE_UNDERSCORE_PREFIX.create(
403 {'name': name.substring(1)}),
404 span: node.sourceSpan, asset: sourceId);
405 }
406 if (value != '' && !value.trim().startsWith(_BINDING_REGEX)) {
407 node.attributes[name] = _newUrl(value, node.sourceSpan);
408 changed = changed || value != node.attributes[name];
409 }
410 }
411 });
412 }
413 if (node.localName == 'style') {
414 node.text = visitCss(node.text);
415 changed = true;
416 } else if (node.localName == 'script' &&
417 node.attributes['type'] == TYPE_DART &&
418 !node.attributes.containsKey('src')) {
419 // TODO(jmesserly): we might need to visit JS too to handle ES Harmony
420 // modules.
421 node.text = visitInlineDart(node.text);
422 changed = true;
423 }
424 return super.visitElement(node);
425 }
426
427 static final _URL = new RegExp(r'url\(([^)]*)\)', multiLine: true);
428 static final _QUOTE = new RegExp('["\']', multiLine: true);
429
430 /// Visit the CSS text and replace any relative URLs so we can inline it.
431 // Ported from:
432 // https://github.com/Polymer/vulcanize/blob/c14f63696797cda18dc3d372b78aa3378 acc691f/lib/vulcan.js#L149
433 // TODO(jmesserly): use csslib here instead? Parsing with RegEx is sadness.
434 // Maybe it's reliable enough for finding URLs in CSS? I'm not sure.
435 String visitCss(String cssText) {
436 var url = spanUrlFor(sourceId, transform, logger);
437 var src = new SourceFile(cssText, url: url);
438 return cssText.replaceAllMapped(_URL, (match) {
439 // Extract the URL, without any surrounding quotes.
440 var span = src.span(match.start, match.end);
441 var href = match[1].replaceAll(_QUOTE, '');
442 href = _newUrl(href, span);
443 return 'url($href)';
444 });
445 }
446
447 String visitInlineDart(String code) {
448 var unit = parseDirectives(code, suppressErrors: true);
449 var file = new SourceFile(code,
450 url: spanUrlFor(sourceId, transform, logger));
451 var output = new TextEditTransaction(code, file);
452 var foundLibraryDirective = false;
453 for (Directive directive in unit.directives) {
454 if (directive is UriBasedDirective) {
455 var uri = directive.uri.stringValue;
456 var span = _getSpan(file, directive.uri);
457
458 var id = uriToAssetId(sourceId, uri, logger, span,
459 errorOnAbsolute: false);
460 if (id == null) continue;
461
462 var primaryId = transform.primaryInput.id;
463 var newUri = assetUrlFor(id, primaryId, logger);
464 if (newUri != uri) {
465 output.edit(span.start.offset, span.end.offset, "'$newUri'");
466 }
467 } else if (directive is LibraryDirective) {
468 foundLibraryDirective = true;
469 }
470 }
471
472 if (!foundLibraryDirective) {
473 // Ensure all inline scripts also have a library name.
474 var libName = _libraryNameFor(sourceId, _count++);
475 output.edit(0, 0, "library $libName;\n");
476 }
477
478 if (!output.hasEdits) return code;
479
480 // TODO(sigmund): emit source maps when barback supports it (see
481 // dartbug.com/12340)
482 return (output.commit()..build(file.url.toString())).text;
483 }
484
485 String _newUrl(String href, SourceSpan span) {
486 // Placeholder for everything past the start of the first binding.
487 const placeholder = '_';
488 // We only want to parse the part of the href leading up to the first
489 // binding, anything after that is not informative.
490 var hrefToParse;
491 var firstBinding = href.indexOf(_BINDING_REGEX);
492 if (firstBinding == -1) {
493 hrefToParse = href;
494 } else if (firstBinding == 0) {
495 return href;
496 } else {
497 hrefToParse = '${href.substring(0, firstBinding)}$placeholder';
498 }
499
500 var uri = Uri.parse(hrefToParse);
501 if (uri.isAbsolute) return href;
502 if (!uri.scheme.isEmpty) return href;
503 if (!uri.host.isEmpty) return href;
504 if (uri.path.isEmpty) return href; // Implies standalone ? or # in URI.
505 if (path.isAbsolute(href)) return href;
506
507 var id = uriToAssetId(sourceId, hrefToParse, logger, span);
508 if (id == null) return href;
509 var primaryId = transform.primaryInput.id;
510
511 // Build the new path, placing back any suffixes that we stripped earlier.
512 var prefix = (firstBinding == -1) ? id.path
513 : id.path.substring(0, id.path.length - placeholder.length);
514 var suffix = (firstBinding == -1) ? '' : href.substring(firstBinding);
515 var newPath = '$prefix$suffix';
516
517 if (newPath.startsWith('lib/')) {
518 return '${topLevelPath}packages/${id.package}/${newPath.substring(4)}';
519 }
520
521 if (newPath.startsWith('asset/')) {
522 return '${topLevelPath}assets/${id.package}/${newPath.substring(6)}';
523 }
524
525 if (primaryId.package != id.package) {
526 // Techincally we shouldn't get there
527 logger.error(INTERNAL_ERROR_DONT_KNOW_HOW_TO_IMPORT.create({
528 'target': id, 'source': primaryId, 'extra': ''}), span: span);
529 return href;
530 }
531
532 var builder = path.url;
533 return builder.relative(builder.join('/', newPath),
534 from: builder.join('/', builder.dirname(primaryId.path)));
535 }
536 }
537
538 /// HTML attributes that expect a URL value.
539 /// <http://dev.w3.org/html5/spec/section-index.html#attributes-1>
540 ///
541 /// Every one of these attributes is a URL in every context where it is used in
542 /// the DOM. The comments show every DOM element where an attribute can be used.
543 ///
544 /// The _* version of each attribute is also supported, see http://goo.gl/5av8cU
545 const _urlAttributes = const [
546 'action', '_action', // in form
547 'background', '_background', // in body
548 'cite', '_cite', // in blockquote, del, ins, q
549 'data', '_data', // in object
550 'formaction', '_formaction', // in button, input
551 'href', '_href', // in a, area, link, base, command
552 'icon', '_icon', // in command
553 'manifest', '_manifest', // in html
554 'poster', '_poster', // in video
555 'src', '_src', // in audio, embed, iframe, img, input, script,
556 // source, track,video
557 ];
558
559 /// When inlining <link rel="stylesheet"> tags copy over all attributes to the
560 /// style tag except these ones.
561 const IGNORED_LINKED_STYLE_ATTRS =
562 const ['charset', 'href', 'href-lang', 'rel', 'rev'];
563
564 /// Global RegExp objects.
565 final _INVALID_LIB_CHARS_REGEX = new RegExp('[^a-z0-9_]');
566 final _NUM_REGEX = new RegExp('[0-9]');
567 final _BINDING_REGEX = new RegExp(r'(({{.*}})|(\[\[.*\]\]))');
568
569 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end);
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698