| OLD | NEW |
| (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'(({{.*}})|(\[\[.*\]\]))'); | |
| OLD | NEW |