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 |