| 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 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); | |
| OLD | NEW |