| /* |
| * Copyright (C) 2012 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * |
| * 2. Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. AND ITS CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE INC. |
| * OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| /** |
| * @unrestricted |
| */ |
| export class ParsedURL { |
| /** |
| * @param {string} url |
| */ |
| constructor(url) { |
| this.isValid = false; |
| this.url = url; |
| this.scheme = ''; |
| this.user = ''; |
| this.host = ''; |
| this.port = ''; |
| this.path = ''; |
| this.queryParams = ''; |
| this.fragment = ''; |
| this.folderPathComponents = ''; |
| this.lastPathComponent = ''; |
| |
| const isBlobUrl = this.url.startsWith('blob:'); |
| const urlToMatch = isBlobUrl ? url.substring(5) : url; |
| const match = urlToMatch.match(ParsedURL._urlRegex()); |
| if (match) { |
| this.isValid = true; |
| if (isBlobUrl) { |
| this._blobInnerScheme = match[2].toLowerCase(); |
| this.scheme = 'blob'; |
| } else { |
| this.scheme = match[2].toLowerCase(); |
| } |
| this.user = match[3]; |
| this.host = match[4]; |
| this.port = match[5]; |
| this.path = match[6] || '/'; |
| this.queryParams = match[7] || ''; |
| this.fragment = match[8]; |
| } else { |
| if (this.url.startsWith('data:')) { |
| this.scheme = 'data'; |
| return; |
| } |
| if (this.url.startsWith('blob:')) { |
| this.scheme = 'blob'; |
| return; |
| } |
| if (this.url === 'about:blank') { |
| this.scheme = 'about'; |
| return; |
| } |
| this.path = this.url; |
| } |
| |
| const lastSlashIndex = this.path.lastIndexOf('/'); |
| if (lastSlashIndex !== -1) { |
| this.folderPathComponents = this.path.substring(0, lastSlashIndex); |
| this.lastPathComponent = this.path.substring(lastSlashIndex + 1); |
| } else { |
| this.lastPathComponent = this.path; |
| } |
| } |
| |
| /** |
| * @param {string} string |
| * @return {?ParsedURL} |
| */ |
| static fromString(string) { |
| const parsedURL = new ParsedURL(string.toString()); |
| if (parsedURL.isValid) { |
| return parsedURL; |
| } |
| return null; |
| } |
| |
| /** |
| * @param {string} fileSystemPath |
| * @return {string} |
| */ |
| static platformPathToURL(fileSystemPath) { |
| fileSystemPath = fileSystemPath.replace(/\\/g, '/'); |
| if (!fileSystemPath.startsWith('file://')) { |
| if (fileSystemPath.startsWith('/')) { |
| fileSystemPath = 'file://' + fileSystemPath; |
| } else { |
| fileSystemPath = 'file:///' + fileSystemPath; |
| } |
| } |
| return fileSystemPath; |
| } |
| |
| /** |
| * @param {string} fileURL |
| * @param {boolean} isWindows |
| * @return {string} |
| */ |
| static urlToPlatformPath(fileURL, isWindows) { |
| console.assert(fileURL.startsWith('file://'), 'This must be a file URL.'); |
| if (isWindows) { |
| return fileURL.substr('file:///'.length).replace(/\//g, '\\'); |
| } |
| return fileURL.substr('file://'.length); |
| } |
| |
| /** |
| * @param {string} url |
| * @return {string} |
| */ |
| static urlWithoutHash(url) { |
| const hashIndex = url.indexOf('#'); |
| if (hashIndex !== -1) { |
| return url.substr(0, hashIndex); |
| } |
| return url; |
| } |
| |
| /** |
| * @return {!RegExp} |
| */ |
| static _urlRegex() { |
| if (ParsedURL._urlRegexInstance) { |
| return ParsedURL._urlRegexInstance; |
| } |
| // RegExp groups: |
| // 1 - scheme, hostname, ?port |
| // 2 - scheme (using the RFC3986 grammar) |
| // 3 - ?user:password |
| // 4 - hostname |
| // 5 - ?port |
| // 6 - ?path |
| // 7 - ?query |
| // 8 - ?fragment |
| const schemeRegex = /([A-Za-z][A-Za-z0-9+.-]*):\/\//; |
| const userRegex = /(?:([A-Za-z0-9\-._~%!$&'()*+,;=:]*)@)?/; |
| const hostRegex = /((?:\[::\d?\])|(?:[^\s\/:]*))/; |
| const portRegex = /(?::([\d]+))?/; |
| const pathRegex = /(\/[^#?]*)?/; |
| const queryRegex = /(?:\?([^#]*))?/; |
| const fragmentRegex = /(?:#(.*))?/; |
| |
| ParsedURL._urlRegexInstance = new RegExp( |
| '^(' + schemeRegex.source + userRegex.source + hostRegex.source + portRegex.source + ')' + pathRegex.source + |
| queryRegex.source + fragmentRegex.source + '$'); |
| return ParsedURL._urlRegexInstance; |
| } |
| |
| /** |
| * @param {string} url |
| * @return {string} |
| */ |
| static extractPath(url) { |
| const parsedURL = this.fromString(url); |
| return parsedURL ? parsedURL.path : ''; |
| } |
| |
| /** |
| * @param {string} url |
| * @return {string} |
| */ |
| static extractOrigin(url) { |
| const parsedURL = this.fromString(url); |
| return parsedURL ? parsedURL.securityOrigin() : ''; |
| } |
| |
| /** |
| * @param {string} url |
| * @return {string} |
| */ |
| static extractExtension(url) { |
| url = ParsedURL.urlWithoutHash(url); |
| const indexOfQuestionMark = url.indexOf('?'); |
| if (indexOfQuestionMark !== -1) { |
| url = url.substr(0, indexOfQuestionMark); |
| } |
| const lastIndexOfSlash = url.lastIndexOf('/'); |
| if (lastIndexOfSlash !== -1) { |
| url = url.substr(lastIndexOfSlash + 1); |
| } |
| const lastIndexOfDot = url.lastIndexOf('.'); |
| if (lastIndexOfDot !== -1) { |
| url = url.substr(lastIndexOfDot + 1); |
| const lastIndexOfPercent = url.indexOf('%'); |
| if (lastIndexOfPercent !== -1) { |
| return url.substr(0, lastIndexOfPercent); |
| } |
| return url; |
| } |
| return ''; |
| } |
| |
| /** |
| * @param {string} url |
| * @return {string} |
| */ |
| static extractName(url) { |
| let index = url.lastIndexOf('/'); |
| const pathAndQuery = index !== -1 ? url.substr(index + 1) : url; |
| index = pathAndQuery.indexOf('?'); |
| return index < 0 ? pathAndQuery : pathAndQuery.substr(0, index); |
| } |
| |
| /** |
| * @param {string} baseURL |
| * @param {string} href |
| * @return {?string} |
| */ |
| static completeURL(baseURL, href) { |
| // Return special URLs as-is. |
| const trimmedHref = href.trim(); |
| if (trimmedHref.startsWith('data:') || trimmedHref.startsWith('blob:') || trimmedHref.startsWith('javascript:') || |
| trimmedHref.startsWith('mailto:')) { |
| return href; |
| } |
| |
| // Return absolute URLs as-is. |
| const parsedHref = this.fromString(trimmedHref); |
| if (parsedHref && parsedHref.scheme) { |
| return trimmedHref; |
| } |
| |
| const parsedURL = this.fromString(baseURL); |
| if (!parsedURL) { |
| return null; |
| } |
| |
| if (parsedURL.isDataURL()) { |
| return href; |
| } |
| |
| if (href.length > 1 && href.charAt(0) === '/' && href.charAt(1) === '/') { |
| // href starts with "//" which is a full URL with the protocol dropped (use the baseURL protocol). |
| return parsedURL.scheme + ':' + href; |
| } |
| |
| const securityOrigin = parsedURL.securityOrigin(); |
| const pathText = parsedURL.path; |
| const queryText = parsedURL.queryParams ? '?' + parsedURL.queryParams : ''; |
| |
| // Empty href resolves to a URL without fragment. |
| if (!href.length) { |
| return securityOrigin + pathText + queryText; |
| } |
| |
| if (href.charAt(0) === '#') { |
| return securityOrigin + pathText + queryText + href; |
| } |
| |
| if (href.charAt(0) === '?') { |
| return securityOrigin + pathText + href; |
| } |
| |
| let hrefPath = href.match(/^[^#?]*/)[0]; |
| const hrefSuffix = href.substring(hrefPath.length); |
| if (hrefPath.charAt(0) !== '/') { |
| hrefPath = parsedURL.folderPathComponents + '/' + hrefPath; |
| } |
| return securityOrigin + Root.Runtime.normalizePath(hrefPath) + hrefSuffix; |
| } |
| |
| /** |
| * @param {string} string |
| * @return {!{url: string, lineNumber: (number|undefined), columnNumber: (number|undefined)}} |
| */ |
| static splitLineAndColumn(string) { |
| // Only look for line and column numbers in the path to avoid matching port numbers. |
| const beforePathMatch = string.match(ParsedURL._urlRegex()); |
| let beforePath = ''; |
| let pathAndAfter = string; |
| if (beforePathMatch) { |
| beforePath = beforePathMatch[1]; |
| pathAndAfter = string.substring(beforePathMatch[1].length); |
| } |
| |
| const lineColumnRegEx = /(?::(\d+))?(?::(\d+))?$/; |
| const lineColumnMatch = lineColumnRegEx.exec(pathAndAfter); |
| let lineNumber; |
| let columnNumber; |
| console.assert(lineColumnMatch); |
| |
| if (typeof(lineColumnMatch[1]) === 'string') { |
| lineNumber = parseInt(lineColumnMatch[1], 10); |
| // Immediately convert line and column to 0-based numbers. |
| lineNumber = isNaN(lineNumber) ? undefined : lineNumber - 1; |
| } |
| if (typeof(lineColumnMatch[2]) === 'string') { |
| columnNumber = parseInt(lineColumnMatch[2], 10); |
| columnNumber = isNaN(columnNumber) ? undefined : columnNumber - 1; |
| } |
| |
| return { |
| url: beforePath + pathAndAfter.substring(0, pathAndAfter.length - lineColumnMatch[0].length), |
| lineNumber: lineNumber, |
| columnNumber: columnNumber |
| }; |
| } |
| |
| /** |
| * @param {string} url |
| * @return {boolean} |
| */ |
| static isRelativeURL(url) { |
| return !(/^[A-Za-z][A-Za-z0-9+.-]*:/.test(url)); |
| } |
| |
| get displayName() { |
| if (this._displayName) { |
| return this._displayName; |
| } |
| |
| if (this.isDataURL()) { |
| return this.dataURLDisplayName(); |
| } |
| if (this.isBlobURL()) { |
| return this.url; |
| } |
| if (this.isAboutBlank()) { |
| return this.url; |
| } |
| |
| this._displayName = this.lastPathComponent; |
| if (!this._displayName) { |
| this._displayName = (this.host || '') + '/'; |
| } |
| if (this._displayName === '/') { |
| this._displayName = this.url; |
| } |
| return this._displayName; |
| } |
| |
| /** |
| * @return {string} |
| */ |
| dataURLDisplayName() { |
| if (this._dataURLDisplayName) { |
| return this._dataURLDisplayName; |
| } |
| if (!this.isDataURL()) { |
| return ''; |
| } |
| this._dataURLDisplayName = this.url.trimEndWithMaxLength(20); |
| return this._dataURLDisplayName; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| isAboutBlank() { |
| return this.url === 'about:blank'; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| isDataURL() { |
| return this.scheme === 'data'; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| isBlobURL() { |
| return this.url.startsWith('blob:'); |
| } |
| |
| /** |
| * @return {string} |
| */ |
| lastPathComponentWithFragment() { |
| return this.lastPathComponent + (this.fragment ? '#' + this.fragment : ''); |
| } |
| |
| /** |
| * @return {string} |
| */ |
| domain() { |
| if (this.isDataURL()) { |
| return 'data:'; |
| } |
| return this.host + (this.port ? ':' + this.port : ''); |
| } |
| |
| /** |
| * @return {string} |
| */ |
| securityOrigin() { |
| if (this.isDataURL()) { |
| return 'data:'; |
| } |
| const scheme = this.isBlobURL() ? this._blobInnerScheme : this.scheme; |
| return scheme + '://' + this.domain(); |
| } |
| |
| /** |
| * @return {string} |
| */ |
| urlWithoutScheme() { |
| if (this.scheme && this.url.startsWith(this.scheme + '://')) { |
| return this.url.substring(this.scheme.length + 3); |
| } |
| return this.url; |
| } |
| } |