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

Side by Side Diff: web_components/lib/build/import_inliner.dart

Issue 1400473008: Roll Observatory packages and add a roll script (Closed) Base URL: git@github.com:dart-lang/observatory_pub_packages.git@master
Patch Set: Created 5 years, 2 months 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
« no previous file with comments | « web_components/lib/build/import_crawler.dart ('k') | web_components/lib/build/messages.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2015, 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 library web_components.build.import_inliner;
5
6 import 'dart:async';
7 import 'dart:collection' show LinkedHashMap;
8 import 'package:barback/barback.dart';
9 import 'package:code_transformers/assets.dart';
10 import 'package:code_transformers/messages/build_logger.dart';
11 import 'package:html/dom.dart';
12 import 'package:html/dom_parsing.dart' show TreeVisitor;
13 import 'package:path/path.dart' as path;
14 import 'package:source_span/source_span.dart';
15 import 'common.dart';
16 import 'import_crawler.dart';
17 import 'messages.dart';
18
19 /// Transformer which inlines all html imports found from the entry points. This
20 /// deletes all dart scripts found during the inlining, so the
21 /// [ScriptCompactorTransformer] should be ran first if there are any dart files
22 /// in html imports.
23 class ImportInlinerTransformer extends Transformer {
24 final List<String> entryPoints;
25 final List<String> bindingStartDelimiters;
26
27 ImportInlinerTransformer(
28 [this.entryPoints, this.bindingStartDelimiters = const []]);
29
30 bool isPrimary(AssetId id) {
31 if (entryPoints != null) return entryPoints.contains(id.path);
32 // If no entry point is supplied, then any html file under web/ or test/ is
33 // an entry point.
34 return (id.path.startsWith('web/') || id.path.startsWith('test/')) &&
35 id.path.endsWith('.html');
36 }
37
38 apply(Transform transform) {
39 var logger = new BuildLogger(transform, convertErrorsToWarnings: true);
40 return new ImportInliner(transform, transform.primaryInput.id, logger,
41 bindingStartDelimiters: bindingStartDelimiters).run();
42 }
43 }
44
45 /// Helper class which actually does all the inlining of html imports for a
46 /// single entry point.
47 class ImportInliner {
48 // Can be an AggregateTransform or Transform
49 final transform;
50 // The primary input to start from.
51 final AssetId primaryInput;
52 // The logger to use.
53 final BuildLogger logger;
54 // The start delimiters for template bindings, such as '{{' or '[['.
55 final List<String> bindingStartDelimiters;
56
57 ImportInliner(this.transform, this.primaryInput, this.logger,
58 {this.bindingStartDelimiters: const []});
59
60 Future run() {
61 var crawler = new ImportCrawler(transform, primaryInput, logger);
62 return crawler.crawlImports().then((imports) {
63 var primaryDocument = imports[primaryInput].document;
64
65 // Normalize urls in the entry point.
66 var changed = new _UrlNormalizer(
67 primaryInput, primaryInput, logger, bindingStartDelimiters)
68 .visit(primaryDocument);
69
70 // Inline things if needed, always have at least one (the entry point).
71 if (imports.length > 1) {
72 _inlineImports(primaryDocument, imports);
73 } else if (!changed &&
74 primaryDocument.querySelectorAll('link[rel="import"]').where(
75 (import) => import.attributes['type'] != 'css').length ==
76 0) {
77 // If there were no url changes and no imports, then we are done.
78 return;
79 }
80
81 primaryDocument
82 .querySelectorAll('link[rel="import"]')
83 .where((import) => import.attributes['type'] != 'css')
84 .forEach((element) => element.remove());
85
86 transform.addOutput(
87 new Asset.fromString(primaryInput, primaryDocument.outerHtml));
88 });
89 }
90
91 void _inlineImports(
92 Document primaryDocument, LinkedHashMap<AssetId, ImportData> imports) {
93 // Add a hidden div at the top of the body, this is where we will inline
94 // all the imports.
95 var importWrapper = new Element.tag('div')..attributes['hidden'] = '';
96 var firstElement = primaryDocument.body.firstChild;
97 if (firstElement != null) {
98 primaryDocument.body.insertBefore(importWrapper, firstElement);
99 } else {
100 primaryDocument.body.append(importWrapper);
101 }
102
103 // Move all scripts/stylesheets/imports into the wrapper to maintain
104 // ordering.
105 _moveHeadToWrapper(primaryDocument, importWrapper);
106
107 // Add all the other imports!
108 imports.forEach((AssetId asset, ImportData data) {
109 if (asset == primaryInput) return;
110 var document = data.document;
111 // Remove all dart script tags.
112 document
113 .querySelectorAll('script[type="$dartType"]')
114 .forEach((script) => script.remove());
115 // Normalize urls in attributes and inline css.
116 new _UrlNormalizer(data.fromId, asset, logger, bindingStartDelimiters)
117 .visit(document);
118 // Replace the import with its contents by appending the nodes
119 // immediately before the import one at a time, and then removing the
120 // import from the document.
121 var element = data.element;
122 var parent = element.parent;
123 document.head.nodes
124 .toList(growable: false)
125 .forEach((child) => parent.insertBefore(child, element));
126 document.body.nodes
127 .toList(growable: false)
128 .forEach((child) => parent.insertBefore(child, element));
129 element.remove();
130 });
131 }
132 }
133
134 /// To preserve the order of scripts with respect to inlined
135 /// link rel=import, we move both of those into the body before we do any
136 /// inlining. We do not start doing this until the first import is found
137 /// however, as some scripts do need to be ran in the head to work
138 /// properly (webcomponents.js for instance).
139 ///
140 /// Note: we do this for stylesheets as well to preserve ordering with
141 /// respect to eachother, because stylesheets can be pulled in transitively
142 /// from imports.
143 void _moveHeadToWrapper(Document doc, Element wrapper) {
144 var foundImport = false;
145 for (var node in doc.head.nodes.toList(growable: false)) {
146 if (node is! Element) continue;
147 var tag = node.localName;
148 var type = node.attributes['type'];
149 var rel = node.attributes['rel'];
150 if (tag == 'link' && rel == 'import') foundImport = true;
151 if (!foundImport) continue;
152 if (tag == 'style' ||
153 tag == 'script' &&
154 (type == null || type == jsType || type == dartType) ||
155 tag == 'link' && (rel == 'stylesheet' || rel == 'import')) {
156 // Move the node into the wrapper, where its contents will be placed.
157 // This wrapper is a hidden div to prevent inlined html from causing a
158 // FOUC.
159 wrapper.append(node);
160 }
161 }
162 }
163
164 /// Internally adjusts urls in the html that we are about to inline.
165 // TODO(jakemac): Everything from here down is almost an exact copy from the
166 // polymer package. We should consolidate this logic by either removing it
167 // completely from polymer or exposing it publicly here and using that in
168 // polymer.
169 class _UrlNormalizer extends TreeVisitor {
170 /// [AssetId] for the main entry point.
171 final AssetId primaryInput;
172
173 /// Asset where the original content (and original url) was found.
174 final AssetId sourceId;
175
176 /// Counter used to ensure that every library name we inject is unique.
177 int _count = 0;
178
179 /// Path to the top level folder relative to the transform primaryInput.
180 /// This should just be some arbitrary # of ../'s.
181 final String topLevelPath;
182
183 /// Whether or not the normalizer has changed something in the tree.
184 bool changed = false;
185
186 // The start delimiters for template bindings, such as '{{' or '[['. If these
187 // are found before the first `/` in a url, then the url will not be
188 // normalized.
189 final List<String> bindingStartDelimiters;
190
191 final BuildLogger logger;
192
193 _UrlNormalizer(AssetId primaryInput, this.sourceId, this.logger,
194 this.bindingStartDelimiters)
195 : primaryInput = primaryInput,
196 topLevelPath = '../' * (path.url.split(primaryInput.path).length - 2);
197
198 bool visit(Node node) {
199 super.visit(node);
200 return changed;
201 }
202
203 visitElement(Element node) {
204 // TODO(jakemac): Support custom elements that extend html elements which
205 // have url-like attributes. This probably means keeping a list of which
206 // html elements support each url-like attribute.
207 if (!isCustomTagName(node.localName)) {
208 node.attributes.forEach((name, value) {
209 if (_urlAttributes.contains(name)) {
210 node.attributes[name] = _newUrl(value, node.sourceSpan);
211 changed = value != node.attributes[name];
212 }
213 });
214 }
215 if (node.localName == 'style') {
216 node.text = visitCss(node.text);
217 } else if (node.localName == 'script' &&
218 node.attributes['type'] == dartType &&
219 !node.attributes.containsKey('src')) {
220 changed = true;
221 }
222 return super.visitElement(node);
223 }
224
225 static final _url = new RegExp(r'url\(([^)]*)\)', multiLine: true);
226 static final _quote = new RegExp('["\']', multiLine: true);
227
228 /// Visit the CSS text and replace any relative URLs so we can inline it.
229 // Ported from:
230 // https://github.com/Polymer/vulcanize/blob/c14f63696797cda18dc3d372b78aa3378 acc691f/lib/vulcan.js#L149
231 // TODO(jmesserly): use csslib here instead? Parsing with RegEx is sadness.
232 // Maybe it's reliable enough for finding URLs in CSS? I'm not sure.
233 String visitCss(String cssText) {
234 var url = spanUrlFor(sourceId, primaryInput, logger);
235 var src = new SourceFile(cssText, url: url);
236 return cssText.replaceAllMapped(_url, (match) {
237 changed = true;
238 // Extract the URL, without any surrounding quotes.
239 var span = src.span(match.start, match.end);
240 var href = match[1].replaceAll(_quote, '');
241 href = _newUrl(href, span);
242 return 'url($href)';
243 });
244 }
245
246 String _newUrl(String href, SourceSpan span) {
247 // We only want to parse the part of the href leading up to the first
248 // folder, anything after that is not informative.
249 var hrefToParse;
250 var firstFolder = href.indexOf('/');
251 if (firstFolder == -1) {
252 hrefToParse = href;
253 } else if (firstFolder == 0) {
254 return href;
255 } else {
256 // Special case packages and assets urls.
257 if (href.contains('packages/')) {
258 var suffix = href.substring(href.indexOf('packages/') + 9);
259 return '${topLevelPath}packages/$suffix';
260 } else if (href.contains('assets/')) {
261 var suffix = href.substring(href.indexOf('assets/') + 7);
262 return '${topLevelPath}packages/$suffix';
263 }
264
265 hrefToParse = '${href.substring(0, firstFolder + 1)}';
266 }
267
268 // If we found a binding before the first `/`, then just return the original
269 // href, we can't determine anything about it.
270 if (bindingStartDelimiters.any((d) => hrefToParse.contains(d))) return href;
271
272 Uri uri;
273 // Various template systems introduce invalid characters to uris which would
274 // be typically replaced at runtime. Parse errors are assumed to be caused
275 // by this, and we just return the original href in that case.
276 try {
277 uri = Uri.parse(hrefToParse);
278 } catch (e) {
279 return href;
280 }
281 if (uri.isAbsolute) return href;
282 if (uri.scheme.isNotEmpty) return href;
283 if (uri.host.isNotEmpty) return href;
284 if (uri.path.isEmpty) return href; // Implies standalone ? or # in URI.
285 if (path.isAbsolute(hrefToParse)) return href;
286
287 var id = uriToAssetId(sourceId, hrefToParse, logger, span);
288 if (id == null) return href;
289
290 // Build the new path, placing back any suffixes that we stripped earlier.
291 var prefix =
292 (firstFolder == -1) ? id.path : id.path.substring(0, id.path.length);
293 var suffix = (firstFolder == -1) ? '' : href.substring(firstFolder);
294 var newPath = '$prefix$suffix';
295
296 if (newPath.startsWith('lib/')) {
297 return '${topLevelPath}packages/${id.package}/${newPath.substring(4)}';
298 }
299
300 if (newPath.startsWith('asset/')) {
301 return '${topLevelPath}assets/${id.package}/${newPath.substring(6)}';
302 }
303
304 if (primaryInput.package != id.package) {
305 // Technically we shouldn't get there
306 logger.error(internalErrorDontKnowHowToImport
307 .create({'target': id, 'source': primaryInput, 'extra': ''}),
308 span: span);
309 return href;
310 }
311
312 var builder = path.url;
313 return builder.normalize(builder.relative(builder.join('/', newPath),
314 from: builder.join('/', builder.dirname(primaryInput.path))));
315 }
316 }
317
318 /// Returns true if this is a valid custom element name. See:
319 /// <http://w3c.github.io/webcomponents/spec/custom/#dfn-custom-element-type>
320 bool isCustomTagName(String name) {
321 if (name == null || !name.contains('-')) return false;
322 return !invalidTagNames.containsKey(name);
323 }
324
325 /// These names have meaning in SVG or MathML, so they aren't allowed as custom
326 /// tags. See [isCustomTagName].
327 const invalidTagNames = const {
328 'annotation-xml': '',
329 'color-profile': '',
330 'font-face': '',
331 'font-face-src': '',
332 'font-face-uri': '',
333 'font-face-format': '',
334 'font-face-name': '',
335 'missing-glyph': '',
336 };
337
338 /// HTML attributes that expect a URL value.
339 /// <http://dev.w3.org/html5/spec/section-index.html#attributes-1>
340 ///
341 /// Every one of these attributes is a URL in every context where it is used in
342 /// the DOM. The comments show every DOM element where an attribute can be used.
343 ///
344 /// The _* version of each attribute is also supported, see http://goo.gl/5av8cU
345 const _urlAttributes = const [
346 'action',
347 '_action', // in form
348 'background',
349 '_background', // in body
350 'cite',
351 '_cite', // in blockquote, del, ins, q
352 'data',
353 '_data', // in object
354 'formaction',
355 '_formaction', // in button, input
356 'href',
357 '_href', // in a, area, link, base, command
358 'icon',
359 '_icon', // in command
360 'manifest',
361 '_manifest', // in html
362 'poster',
363 '_poster', // in video
364 'src',
365 '_src', // in audio, embed, iframe, img, input, script, source, track,video
366 ];
OLDNEW
« no previous file with comments | « web_components/lib/build/import_crawler.dart ('k') | web_components/lib/build/messages.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698