@quick-game/cli
Version:
Command line interface for rapid qg development
473 lines • 18.3 kB
JavaScript
/*
* 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:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * 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.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT
* OWNER OR 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.
*/
import * as Platform from '../platform/platform.js';
/**
* http://tools.ietf.org/html/rfc3986#section-5.2.4
*/
export function normalizePath(path) {
if (path.indexOf('..') === -1 && path.indexOf('.') === -1) {
return path;
}
// Remove leading slash (will be added back below) so we
// can handle all (including empty) segments consistently.
const segments = (path[0] === '/' ? path.substring(1) : path).split('/');
const normalizedSegments = [];
for (const segment of segments) {
if (segment === '.') {
continue;
}
else if (segment === '..') {
normalizedSegments.pop();
}
else {
normalizedSegments.push(segment);
}
}
let normalizedPath = normalizedSegments.join('/');
if (path[0] === '/' && normalizedPath) {
normalizedPath = '/' + normalizedPath;
}
if (normalizedPath[normalizedPath.length - 1] !== '/' &&
((path[path.length - 1] === '/') || (segments[segments.length - 1] === '.') ||
(segments[segments.length - 1] === '..'))) {
normalizedPath = normalizedPath + '/';
}
return normalizedPath;
}
export class ParsedURL {
isValid;
url;
scheme;
user;
host;
port;
path;
queryParams;
fragment;
folderPathComponents;
lastPathComponent;
blobInnerScheme;
#displayNameInternal;
#dataURLDisplayNameInternal;
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 lastSlashExceptTrailingIndex = this.path.lastIndexOf('/', this.path.length - 2);
if (lastSlashExceptTrailingIndex !== -1) {
this.lastPathComponent = this.path.substring(lastSlashExceptTrailingIndex + 1);
}
else {
this.lastPathComponent = this.path;
}
const lastSlashIndex = this.path.lastIndexOf('/');
if (lastSlashIndex !== -1) {
this.folderPathComponents = this.path.substring(0, lastSlashIndex);
}
}
static fromString(string) {
const parsedURL = new ParsedURL(string.toString());
if (parsedURL.isValid) {
return parsedURL;
}
return null;
}
static preEncodeSpecialCharactersInPath(path) {
// Based on net::FilePathToFileURL. Ideally we would handle
// '\\' as well on non-Windows file systems.
for (const specialChar of ['%', ';', '#', '?', ' ']) {
path = path.replaceAll(specialChar, encodeURIComponent(specialChar));
}
return path;
}
static rawPathToEncodedPathString(path) {
const partiallyEncoded = ParsedURL.preEncodeSpecialCharactersInPath(path);
if (path.startsWith('/')) {
return new URL(partiallyEncoded, 'file:///').pathname;
}
// URL prepends a '/'
return new URL('/' + partiallyEncoded, 'file:///').pathname.substr(1);
}
/**
* @param name Must not be encoded
*/
static encodedFromParentPathAndName(parentPath, name) {
return ParsedURL.concatenate(parentPath, '/', ParsedURL.preEncodeSpecialCharactersInPath(name));
}
/**
* @param name Must not be encoded
*/
static urlFromParentUrlAndName(parentUrl, name) {
return ParsedURL.concatenate(parentUrl, '/', ParsedURL.preEncodeSpecialCharactersInPath(name));
}
static encodedPathToRawPathString(encPath) {
return decodeURIComponent(encPath);
}
static rawPathToUrlString(fileSystemPath) {
let preEncodedPath = ParsedURL.preEncodeSpecialCharactersInPath(fileSystemPath.replace(/\\/g, '/'));
preEncodedPath = preEncodedPath.replace(/\\/g, '/');
if (!preEncodedPath.startsWith('file://')) {
if (preEncodedPath.startsWith('/')) {
preEncodedPath = 'file://' + preEncodedPath;
}
else {
preEncodedPath = 'file:///' + preEncodedPath;
}
}
return new URL(preEncodedPath).toString();
}
static relativePathToUrlString(relativePath, baseURL) {
const preEncodedPath = ParsedURL.preEncodeSpecialCharactersInPath(relativePath.replace(/\\/g, '/'));
return new URL(preEncodedPath, baseURL).toString();
}
static urlToRawPathString(fileURL, isWindows) {
console.assert(fileURL.startsWith('file://'), 'This must be a file URL.');
const decodedFileURL = decodeURIComponent(fileURL);
if (isWindows) {
return decodedFileURL.substr('file:///'.length).replace(/\//g, '\\');
}
return decodedFileURL.substr('file://'.length);
}
static sliceUrlToEncodedPathString(url, start) {
return url.substring(start);
}
static substr(devToolsPath, from, length) {
return devToolsPath.substr(from, length);
}
static substring(devToolsPath, start, end) {
return devToolsPath.substring(start, end);
}
static prepend(prefix, devToolsPath) {
return prefix + devToolsPath;
}
static concatenate(devToolsPath, ...appendage) {
return devToolsPath.concat(...appendage);
}
static trim(devToolsPath) {
return devToolsPath.trim();
}
static slice(devToolsPath, start, end) {
return devToolsPath.slice(start, end);
}
static join(devToolsPaths, separator) {
return devToolsPaths.join(separator);
}
static split(devToolsPath, separator, limit) {
return devToolsPath.split(separator, limit);
}
static toLowerCase(devToolsPath) {
return devToolsPath.toLowerCase();
}
static isValidUrlString(str) {
return new ParsedURL(str).isValid;
}
static urlWithoutHash(url) {
const hashIndex = url.indexOf('#');
if (hashIndex !== -1) {
return url.substr(0, hashIndex);
}
return url;
}
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;
}
static extractPath(url) {
const parsedURL = this.fromString(url);
return (parsedURL ? parsedURL.path : '');
}
static extractOrigin(url) {
const parsedURL = this.fromString(url);
return parsedURL ? parsedURL.securityOrigin() : Platform.DevToolsPath.EmptyUrlString;
}
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 '';
}
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);
}
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 with normalized path and other components as-is.
const parsedHref = this.fromString(trimmedHref);
if (parsedHref && parsedHref.scheme) {
const securityOrigin = parsedHref.securityOrigin();
const pathText = normalizePath(parsedHref.path);
const queryText = parsedHref.queryParams && `?${parsedHref.queryParams}`;
const fragmentText = parsedHref.fragment && `#${parsedHref.fragment}`;
return securityOrigin + pathText + queryText + fragmentText;
}
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;
}
const hrefMatches = href.match(/^[^#?]*/);
if (!hrefMatches || !href.length) {
throw new Error('Invalid href');
}
let hrefPath = hrefMatches[0];
const hrefSuffix = href.substring(hrefPath.length);
if (hrefPath.charAt(0) !== '/') {
hrefPath = parsedURL.folderPathComponents + '/' + hrefPath;
}
return securityOrigin + normalizePath(hrefPath) + hrefSuffix;
}
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(Boolean(lineColumnMatch));
if (!lineColumnMatch) {
return { url: string, lineNumber: 0, columnNumber: 0 };
}
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;
}
let url = beforePath + pathAndAfter.substring(0, pathAndAfter.length - lineColumnMatch[0].length);
if (lineColumnMatch[1] === undefined && lineColumnMatch[2] === undefined) {
const wasmCodeOffsetRegex = /wasm-function\[\d+\]:0x([a-z0-9]+)$/g;
const wasmCodeOffsetMatch = wasmCodeOffsetRegex.exec(pathAndAfter);
if (wasmCodeOffsetMatch && typeof (wasmCodeOffsetMatch[1]) === 'string') {
url = ParsedURL.removeWasmFunctionInfoFromURL(url);
columnNumber = parseInt(wasmCodeOffsetMatch[1], 16);
columnNumber = isNaN(columnNumber) ? undefined : columnNumber;
}
}
return { url, lineNumber, columnNumber };
}
static removeWasmFunctionInfoFromURL(url) {
const wasmFunctionRegEx = /:wasm-function\[\d+\]/;
const wasmFunctionIndex = url.search(wasmFunctionRegEx);
if (wasmFunctionIndex === -1) {
return url;
}
return ParsedURL.substring(url, 0, wasmFunctionIndex);
}
static beginsWithWindowsDriveLetter(url) {
return /^[A-Za-z]:/.test(url);
}
static beginsWithScheme(url) {
return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(url);
}
static isRelativeURL(url) {
return !this.beginsWithScheme(url) || this.beginsWithWindowsDriveLetter(url);
}
get displayName() {
if (this.#displayNameInternal) {
return this.#displayNameInternal;
}
if (this.isDataURL()) {
return this.dataURLDisplayName();
}
if (this.isBlobURL()) {
return this.url;
}
if (this.isAboutBlank()) {
return this.url;
}
this.#displayNameInternal = this.lastPathComponent;
if (!this.#displayNameInternal) {
this.#displayNameInternal = (this.host || '') + '/';
}
if (this.#displayNameInternal === '/') {
this.#displayNameInternal = this.url;
}
return this.#displayNameInternal;
}
dataURLDisplayName() {
if (this.#dataURLDisplayNameInternal) {
return this.#dataURLDisplayNameInternal;
}
if (!this.isDataURL()) {
return '';
}
this.#dataURLDisplayNameInternal = Platform.StringUtilities.trimEndWithMaxLength(this.url, 20);
return this.#dataURLDisplayNameInternal;
}
isAboutBlank() {
return this.url === 'about:blank';
}
isDataURL() {
return this.scheme === 'data';
}
isHttpOrHttps() {
return this.scheme === 'http' || this.scheme === 'https';
}
isBlobURL() {
return this.url.startsWith('blob:');
}
lastPathComponentWithFragment() {
return this.lastPathComponent + (this.fragment ? '#' + this.fragment : '');
}
domain() {
if (this.isDataURL()) {
return 'data:';
}
return this.host + (this.port ? ':' + this.port : '');
}
securityOrigin() {
if (this.isDataURL()) {
return 'data:';
}
const scheme = this.isBlobURL() ? this.blobInnerScheme : this.scheme;
return scheme + '://' + this.domain();
}
urlWithoutScheme() {
if (this.scheme && this.url.startsWith(this.scheme + '://')) {
return this.url.substring(this.scheme.length + 3);
}
return this.url;
}
static urlRegexInstance = null;
}
//# sourceMappingURL=ParsedURL.js.map