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

Side by Side Diff: packages/polymer/lib/src/build/html_finalizer.dart

Issue 2312183003: Removed Polymer from Observatory deps (Closed)
Patch Set: Created 4 years, 3 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
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 finalizes an html file for deployment:
6 /// - Extracts inline js scripts in csp mode.
7 /// - Inlines css files into the document.
8 /// - Validates polymer-element templates.
9 library polymer.src.build.html_finalizer;
10
11 import 'dart:async';
12 import 'dart:collection' show LinkedHashSet;
13
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:html/dom.dart'
19 show Document, DocumentFragment, Element, Node;
20 import 'package:html/dom_parsing.dart' show TreeVisitor;
21 import 'package:source_span/source_span.dart';
22
23 import 'common.dart';
24 import 'messages.dart';
25
26 /// Inlines css files and extracts inline js scripts into files if in csp mode.
27 // TODO(jakemac): Move to a different package. Will need to break out the
28 // binding-specific logic when this happens (add it to the linter?).
29 class _HtmlFinalizer extends PolymerTransformer {
30 final TransformOptions options;
31 final Transform transform;
32 final BuildLogger logger;
33 final AssetId docId;
34 final seen = new Set<AssetId>();
35 final scriptIds = new LinkedHashSet<AssetId>();
36 final inlinedStylesheetIds = new Set<AssetId>();
37 final extractedFiles = new Set<AssetId>();
38
39 /// The number of extracted inline Dart scripts. Used as a counter to give
40 /// unique-ish filenames.
41 int inlineScriptCounter = 0;
42
43 _HtmlFinalizer(TransformOptions options, Transform transform)
44 : options = options,
45 transform = transform,
46 logger = new BuildLogger(transform,
47 convertErrorsToWarnings: !options.releaseMode,
48 detailsUri: 'http://goo.gl/5HPeuP'),
49 docId = transform.primaryInput.id;
50
51 Future apply() {
52 seen.add(docId);
53
54 Document document;
55 bool changed = false;
56
57 return readPrimaryAsHtml(transform, logger).then((doc) {
58 document = doc;
59 new _UrlAttributeValidator(docId, logger).visit(document);
60
61 changed = _extractScripts(document) || changed;
62
63 return _inlineCss(document);
64 }).then((cssInlined) {
65 changed = changed || cssInlined;
66
67 var output = transform.primaryInput;
68 if (changed) output = new Asset.fromString(docId, document.outerHtml);
69 transform.addOutput(output);
70
71 // Write out the logs collected by our [BuildLogger].
72 if (options.injectBuildLogsInOutput) {
73 return logger.writeOutput();
74 }
75 });
76 }
77
78 /// Inlines any css files found into document. Returns a [bool] indicating
79 /// whether or not the document was modified.
80 Future<bool> _inlineCss(Document document) {
81 bool changed = false;
82
83 // Note: we need to preserve the import order in the generated output.
84 var tags = document.querySelectorAll('link[rel="stylesheet"]');
85 return Future.forEach(tags, (Element tag) {
86 var href = tag.attributes['href'];
87 var id = uriToAssetId(docId, href, logger, tag.sourceSpan,
88 errorOnAbsolute: false);
89 if (id == null) return null;
90 if (!options.shouldInlineStylesheet(id)) return null;
91
92 changed = true;
93 if (inlinedStylesheetIds.contains(id) &&
94 !options.stylesheetInliningIsOverridden(id)) {
95 logger.warning(CSS_FILE_INLINED_MULTIPLE_TIMES.create({'url': id.path}),
96 span: tag.sourceSpan);
97 }
98 inlinedStylesheetIds.add(id);
99 return _inlineStylesheet(id, tag);
100 }).then((_) => changed);
101 }
102
103 /// Inlines a single css file by replacing [link] with an inline style tag.
104 Future _inlineStylesheet(AssetId id, Element link) {
105 return transform.readInputAsString(id).catchError((error) {
106 // TODO(jakemac): Move this warning to the linter once we can make it run
107 // always (see http://dartbug.com/17199). Then hide this error and replace
108 // with a comment pointing to the linter error (so we don't double warn).
109 logger.warning(INLINE_STYLE_FAIL.create({'error': error}),
110 span: link.sourceSpan);
111 }).then((css) {
112 if (css == null) return null;
113 css = new _UrlNormalizer(transform, id, logger).visitCss(css);
114 var styleElement = new Element.tag('style')..text = css;
115 // Copy over the extra attributes from the link tag to the style tag.
116 // This adds support for no-shim, shim-shadowdom, etc.
117 link.attributes.forEach((key, value) {
118 if (!IGNORED_LINKED_STYLE_ATTRS.contains(key)) {
119 styleElement.attributes[key] = value;
120 }
121 });
122 link.replaceWith(styleElement);
123 });
124 }
125
126 /// Splits inline js scripts into their own files in csp mode.
127 bool _extractScripts(Document doc) {
128 if (!options.contentSecurityPolicy) return false;
129
130 bool changed = false;
131 for (var script in doc.querySelectorAll('script')) {
132 var src = script.attributes['src'];
133 if (src != null) continue;
134
135 var type = script.attributes['type'];
136 if (type == TYPE_DART) continue;
137
138 var extension = 'js';
139 final filename = path.url.basename(docId.path);
140 final count = inlineScriptCounter++;
141 var code = script.text;
142 // TODO(sigmund): ensure this path is unique (dartbug.com/12618).
143 script.attributes['src'] = src = '$filename.$count.$extension';
144 script.text = '';
145 changed = true;
146
147 var newId = docId.addExtension('.$count.$extension');
148 extractedFiles.add(newId);
149 transform.addOutput(new Asset.fromString(newId, code));
150 }
151 return changed;
152 }
153 }
154
155 /// Finalizes a single html document for deployment.
156 class HtmlFinalizer extends Transformer {
157 final TransformOptions options;
158
159 HtmlFinalizer(this.options);
160
161 /// Only run on entry point .html files.
162 bool isPrimary(AssetId id) => options.isHtmlEntryPoint(id);
163
164 Future apply(Transform transform) =>
165 new _HtmlFinalizer(options, transform).apply();
166 }
167
168 const TYPE_DART = 'application/dart';
169 const TYPE_JS = 'text/javascript';
170
171 /// Internally adjusts urls in the html that we are about to inline.
172 class _UrlNormalizer {
173 final Transform transform;
174
175 /// Asset where the original content (and original url) was found.
176 final AssetId sourceId;
177
178 /// Path to the top level folder relative to the transform primaryInput.
179 /// This should just be some arbitrary # of ../'s.
180 final String topLevelPath;
181
182 /// Whether or not the normalizer has changed something in the tree.
183 bool changed = false;
184
185 final BuildLogger logger;
186
187 _UrlNormalizer(transform, this.sourceId, this.logger)
188 : transform = transform,
189 topLevelPath = '../' *
190 (path.url.split(transform.primaryInput.id.path).length - 2);
191
192 static final _URL = new RegExp(r'url\(([^)]*)\)', multiLine: true);
193 static final _QUOTE = new RegExp('["\']', multiLine: true);
194
195 /// Visit the CSS text and replace any relative URLs so we can inline it.
196 // Ported from:
197 // https://github.com/Polymer/vulcanize/blob/c14f63696797cda18dc3d372b78aa3378 acc691f/lib/vulcan.js#L149
198 // TODO(jmesserly): use csslib here instead? Parsing with RegEx is sadness.
199 // Maybe it's reliable enough for finding URLs in CSS? I'm not sure.
200 String visitCss(String cssText) {
201 var url = spanUrlFor(sourceId, transform, logger);
202 var src = new SourceFile(cssText, url: url);
203 return cssText.replaceAllMapped(_URL, (match) {
204 // Extract the URL, without any surrounding quotes.
205 var span = src.span(match.start, match.end);
206 var href = match[1].replaceAll(_QUOTE, '');
207 href = _newUrl(href, span);
208 return 'url($href)';
209 });
210 }
211
212 String _newUrl(String href, SourceSpan span) {
213 var uri = Uri.parse(href);
214 if (uri.isAbsolute) return href;
215 if (!uri.scheme.isEmpty) return href;
216 if (!uri.host.isEmpty) return href;
217 if (uri.path.isEmpty) return href; // Implies standalone ? or # in URI.
218 if (path.isAbsolute(href)) return href;
219
220 var id = uriToAssetId(sourceId, href, logger, span);
221 if (id == null) return href;
222 var primaryId = transform.primaryInput.id;
223
224 if (id.path.startsWith('lib/')) {
225 return '${topLevelPath}packages/${id.package}/${id.path.substring(4)}';
226 }
227
228 if (id.path.startsWith('asset/')) {
229 return '${topLevelPath}assets/${id.package}/${id.path.substring(6)}';
230 }
231
232 if (primaryId.package != id.package) {
233 // Technically we shouldn't get there
234 logger.error(INTERNAL_ERROR_DONT_KNOW_HOW_TO_IMPORT
235 .create({'target': id, 'source': primaryId, 'extra': ''}),
236 span: span);
237 return href;
238 }
239
240 var builder = path.url;
241 return builder.relative(builder.join('/', id.path),
242 from: builder.join('/', builder.dirname(primaryId.path)));
243 }
244 }
245
246 /// Validates url-like attributes and throws warnings as appropriate.
247 /// TODO(jakemac): Move to the linter.
248 class _UrlAttributeValidator extends TreeVisitor {
249 /// Asset where the original content (and original url) was found.
250 final AssetId sourceId;
251
252 final BuildLogger logger;
253
254 _UrlAttributeValidator(this.sourceId, this.logger);
255
256 visit(Node node) {
257 return super.visit(node);
258 }
259
260 visitElement(Element node) {
261 // TODO(jakemac): Support custom elements that extend html elements which
262 // have url-like attributes. This probably means keeping a list of which
263 // html elements support each url-like attribute.
264 if (!isCustomTagName(node.localName)) {
265 node.attributes.forEach((name, value) {
266 if (_urlAttributes.contains(name)) {
267 if (!name.startsWith('_') && value.contains(_BINDING_REGEX)) {
268 logger.warning(USE_UNDERSCORE_PREFIX.create({'name': name}),
269 span: node.sourceSpan, asset: sourceId);
270 } else if (name.startsWith('_') && !value.contains(_BINDING_REGEX)) {
271 logger.warning(
272 DONT_USE_UNDERSCORE_PREFIX.create({'name': name.substring(1)}),
273 span: node.sourceSpan, asset: sourceId);
274 }
275 }
276 });
277 }
278 return super.visitElement(node);
279 }
280 }
281
282 /// HTML attributes that expect a URL value.
283 /// <http://dev.w3.org/html5/spec/section-index.html#attributes-1>
284 ///
285 /// Every one of these attributes is a URL in every context where it is used in
286 /// the DOM. The comments show every DOM element where an attribute can be used.
287 ///
288 /// The _* version of each attribute is also supported, see http://goo.gl/5av8cU
289 const _urlAttributes = const [
290 // in form
291 'action',
292 '_action',
293 // in body
294 'background',
295 '_background',
296 // in blockquote, del, ins, q
297 'cite',
298 '_cite',
299 // in object
300 'data',
301 '_data',
302 // in button, input
303 'formaction',
304 '_formaction',
305 // in a, area, link, base, command
306 'href',
307 '_href',
308 // in command
309 'icon',
310 '_icon',
311 // in html
312 'manifest',
313 '_manifest',
314 // in video
315 'poster',
316 '_poster',
317 // in audio, embed, iframe, img, input, script, source, track, video
318 'src',
319 '_src',
320 ];
321
322 /// When inlining <link rel="stylesheet"> tags copy over all attributes to the
323 /// style tag except these ones.
324 const IGNORED_LINKED_STYLE_ATTRS = const [
325 'charset',
326 'href',
327 'href-lang',
328 'rel',
329 'rev'
330 ];
331
332 /// Global RegExp objects.
333 final _BINDING_REGEX = new RegExp(r'(({{.*}})|(\[\[.*\]\]))');
OLDNEW
« no previous file with comments | « packages/polymer/lib/src/build/generated/messages.html ('k') | packages/polymer/lib/src/build/index_page_builder.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698