Skip to content
Permalink
9bfb9ba527
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
597 lines (597 sloc) 19.1 KB
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { replaceAll } from "./util/utils";
/**
* A class that handles the query portion of a URLBuilder.
*/
export class URLQuery {
constructor() {
this._rawQuery = {};
}
/**
* Get whether or not there any query parameters in this URLQuery.
*/
any() {
return Object.keys(this._rawQuery).length > 0;
}
/**
* Get the keys of the query string.
*/
keys() {
return Object.keys(this._rawQuery);
}
/**
* Set a query parameter with the provided name and value. If the parameterValue is undefined or
* empty, then this will attempt to remove an existing query parameter with the provided
* parameterName.
*/
set(parameterName, parameterValue) {
const caseParameterValue = parameterValue;
if (parameterName) {
if (caseParameterValue !== undefined && caseParameterValue !== null) {
const newValue = Array.isArray(caseParameterValue)
? caseParameterValue
: caseParameterValue.toString();
this._rawQuery[parameterName] = newValue;
}
else {
delete this._rawQuery[parameterName];
}
}
}
/**
* Get the value of the query parameter with the provided name. If no parameter exists with the
* provided parameter name, then undefined will be returned.
*/
get(parameterName) {
return parameterName ? this._rawQuery[parameterName] : undefined;
}
/**
* Get the string representation of this query. The return value will not start with a "?".
*/
toString() {
let result = "";
for (const parameterName in this._rawQuery) {
if (result) {
result += "&";
}
const parameterValue = this._rawQuery[parameterName];
if (Array.isArray(parameterValue)) {
const parameterStrings = [];
for (const parameterValueElement of parameterValue) {
parameterStrings.push(`${parameterName}=${parameterValueElement}`);
}
result += parameterStrings.join("&");
}
else {
result += `${parameterName}=${parameterValue}`;
}
}
return result;
}
/**
* Parse a URLQuery from the provided text.
*/
static parse(text) {
const result = new URLQuery();
if (text) {
if (text.startsWith("?")) {
text = text.substring(1);
}
let currentState = "ParameterName";
let parameterName = "";
let parameterValue = "";
for (let i = 0; i < text.length; ++i) {
const currentCharacter = text[i];
switch (currentState) {
case "ParameterName":
switch (currentCharacter) {
case "=":
currentState = "ParameterValue";
break;
case "&":
parameterName = "";
parameterValue = "";
break;
default:
parameterName += currentCharacter;
break;
}
break;
case "ParameterValue":
switch (currentCharacter) {
case "&":
result.set(parameterName, parameterValue);
parameterName = "";
parameterValue = "";
currentState = "ParameterName";
break;
default:
parameterValue += currentCharacter;
break;
}
break;
default:
throw new Error("Unrecognized URLQuery parse state: " + currentState);
}
}
if (currentState === "ParameterValue") {
result.set(parameterName, parameterValue);
}
}
return result;
}
}
/**
* A class that handles creating, modifying, and parsing URLs.
*/
export class URLBuilder {
/**
* Set the scheme/protocol for this URL. If the provided scheme contains other parts of a URL
* (such as a host, port, path, or query), those parts will be added to this URL as well.
*/
setScheme(scheme) {
if (!scheme) {
this._scheme = undefined;
}
else {
this.set(scheme, "SCHEME");
}
}
/**
* Get the scheme that has been set in this URL.
*/
getScheme() {
return this._scheme;
}
/**
* Set the host for this URL. If the provided host contains other parts of a URL (such as a
* port, path, or query), those parts will be added to this URL as well.
*/
setHost(host) {
if (!host) {
this._host = undefined;
}
else {
this.set(host, "SCHEME_OR_HOST");
}
}
/**
* Get the host that has been set in this URL.
*/
getHost() {
return this._host;
}
/**
* Set the port for this URL. If the provided port contains other parts of a URL (such as a
* path or query), those parts will be added to this URL as well.
*/
setPort(port) {
if (port === undefined || port === null || port === "") {
this._port = undefined;
}
else {
this.set(port.toString(), "PORT");
}
}
/**
* Get the port that has been set in this URL.
*/
getPort() {
return this._port;
}
/**
* Set the path for this URL. If the provided path contains a query, then it will be added to
* this URL as well.
*/
setPath(path) {
if (!path) {
this._path = undefined;
}
else {
const schemeIndex = path.indexOf("://");
if (schemeIndex !== -1) {
const schemeStart = path.lastIndexOf("/", schemeIndex);
// Make sure to only grab the URL part of the path before setting the state back to SCHEME
// this will handle cases such as "/a/b/c/https://microsoft.com" => "https://microsoft.com"
this.set(schemeStart === -1 ? path : path.substr(schemeStart + 1), "SCHEME");
}
else {
this.set(path, "PATH");
}
}
}
/**
* Append the provided path to this URL's existing path. If the provided path contains a query,
* then it will be added to this URL as well.
*/
appendPath(path) {
if (path) {
let currentPath = this.getPath();
if (currentPath) {
if (!currentPath.endsWith("/")) {
currentPath += "/";
}
if (path.startsWith("/")) {
path = path.substring(1);
}
path = currentPath + path;
}
this.set(path, "PATH");
}
}
/**
* Get the path that has been set in this URL.
*/
getPath() {
return this._path;
}
/**
* Set the query in this URL.
*/
setQuery(query) {
if (!query) {
this._query = undefined;
}
else {
this._query = URLQuery.parse(query);
}
}
/**
* Set a query parameter with the provided name and value in this URL's query. If the provided
* query parameter value is undefined or empty, then the query parameter will be removed if it
* existed.
*/
setQueryParameter(queryParameterName, queryParameterValue) {
if (queryParameterName) {
if (!this._query) {
this._query = new URLQuery();
}
this._query.set(queryParameterName, queryParameterValue);
}
}
/**
* Get the value of the query parameter with the provided query parameter name. If no query
* parameter exists with the provided name, then undefined will be returned.
*/
getQueryParameterValue(queryParameterName) {
return this._query ? this._query.get(queryParameterName) : undefined;
}
/**
* Get the query in this URL.
*/
getQuery() {
return this._query ? this._query.toString() : undefined;
}
/**
* Set the parts of this URL by parsing the provided text using the provided startState.
*/
set(text, startState) {
const tokenizer = new URLTokenizer(text, startState);
while (tokenizer.next()) {
const token = tokenizer.current();
let tokenPath;
if (token) {
switch (token.type) {
case "SCHEME":
this._scheme = token.text || undefined;
break;
case "HOST":
this._host = token.text || undefined;
break;
case "PORT":
this._port = token.text || undefined;
break;
case "PATH":
tokenPath = token.text || undefined;
if (!this._path || this._path === "/" || tokenPath !== "/") {
this._path = tokenPath;
}
break;
case "QUERY":
this._query = URLQuery.parse(token.text);
break;
default:
throw new Error(`Unrecognized URLTokenType: ${token.type}`);
}
}
}
}
/**
* Serializes the URL as a string.
* @returns the URL as a string.
*/
toString() {
let result = "";
if (this._scheme) {
result += `${this._scheme}://`;
}
if (this._host) {
result += this._host;
}
if (this._port) {
result += `:${this._port}`;
}
if (this._path) {
if (!this._path.startsWith("/")) {
result += "/";
}
result += this._path;
}
if (this._query && this._query.any()) {
result += `?${this._query.toString()}`;
}
return result;
}
/**
* If the provided searchValue is found in this URLBuilder, then replace it with the provided
* replaceValue.
*/
replaceAll(searchValue, replaceValue) {
if (searchValue) {
this.setScheme(replaceAll(this.getScheme(), searchValue, replaceValue));
this.setHost(replaceAll(this.getHost(), searchValue, replaceValue));
this.setPort(replaceAll(this.getPort(), searchValue, replaceValue));
this.setPath(replaceAll(this.getPath(), searchValue, replaceValue));
this.setQuery(replaceAll(this.getQuery(), searchValue, replaceValue));
}
}
/**
* Parses a given string URL into a new {@link URLBuilder}.
*/
static parse(text) {
const result = new URLBuilder();
result.set(text, "SCHEME_OR_HOST");
return result;
}
}
export class URLToken {
constructor(text, type) {
this.text = text;
this.type = type;
}
static scheme(text) {
return new URLToken(text, "SCHEME");
}
static host(text) {
return new URLToken(text, "HOST");
}
static port(text) {
return new URLToken(text, "PORT");
}
static path(text) {
return new URLToken(text, "PATH");
}
static query(text) {
return new URLToken(text, "QUERY");
}
}
/**
* Get whether or not the provided character (single character string) is an alphanumeric (letter or
* digit) character.
*/
export function isAlphaNumericCharacter(character) {
const characterCode = character.charCodeAt(0);
return ((48 /* '0' */ <= characterCode && characterCode <= 57) /* '9' */ ||
(65 /* 'A' */ <= characterCode && characterCode <= 90) /* 'Z' */ ||
(97 /* 'a' */ <= characterCode && characterCode <= 122) /* 'z' */);
}
/**
* A class that tokenizes URL strings.
*/
export class URLTokenizer {
constructor(_text, state) {
this._text = _text;
this._textLength = _text ? _text.length : 0;
this._currentState = state !== undefined && state !== null ? state : "SCHEME_OR_HOST";
this._currentIndex = 0;
}
/**
* Get the current URLToken this URLTokenizer is pointing at, or undefined if the URLTokenizer
* hasn't started or has finished tokenizing.
*/
current() {
return this._currentToken;
}
/**
* Advance to the next URLToken and return whether or not a URLToken was found.
*/
next() {
if (!hasCurrentCharacter(this)) {
this._currentToken = undefined;
}
else {
switch (this._currentState) {
case "SCHEME":
nextScheme(this);
break;
case "SCHEME_OR_HOST":
nextSchemeOrHost(this);
break;
case "HOST":
nextHost(this);
break;
case "PORT":
nextPort(this);
break;
case "PATH":
nextPath(this);
break;
case "QUERY":
nextQuery(this);
break;
default:
throw new Error(`Unrecognized URLTokenizerState: ${this._currentState}`);
}
}
return !!this._currentToken;
}
}
/**
* Read the remaining characters from this Tokenizer's character stream.
*/
function readRemaining(tokenizer) {
let result = "";
if (tokenizer._currentIndex < tokenizer._textLength) {
result = tokenizer._text.substring(tokenizer._currentIndex);
tokenizer._currentIndex = tokenizer._textLength;
}
return result;
}
/**
* Whether or not this URLTokenizer has a current character.
*/
function hasCurrentCharacter(tokenizer) {
return tokenizer._currentIndex < tokenizer._textLength;
}
/**
* Get the character in the text string at the current index.
*/
function getCurrentCharacter(tokenizer) {
return tokenizer._text[tokenizer._currentIndex];
}
/**
* Advance to the character in text that is "step" characters ahead. If no step value is provided,
* then step will default to 1.
*/
function nextCharacter(tokenizer, step) {
if (hasCurrentCharacter(tokenizer)) {
if (!step) {
step = 1;
}
tokenizer._currentIndex += step;
}
}
/**
* Starting with the current character, peek "charactersToPeek" number of characters ahead in this
* Tokenizer's stream of characters.
*/
function peekCharacters(tokenizer, charactersToPeek) {
let endIndex = tokenizer._currentIndex + charactersToPeek;
if (tokenizer._textLength < endIndex) {
endIndex = tokenizer._textLength;
}
return tokenizer._text.substring(tokenizer._currentIndex, endIndex);
}
/**
* Read characters from this Tokenizer until the end of the stream or until the provided condition
* is false when provided the current character.
*/
function readWhile(tokenizer, condition) {
let result = "";
while (hasCurrentCharacter(tokenizer)) {
const currentCharacter = getCurrentCharacter(tokenizer);
if (!condition(currentCharacter)) {
break;
}
else {
result += currentCharacter;
nextCharacter(tokenizer);
}
}
return result;
}
/**
* Read characters from this Tokenizer until a non-alphanumeric character or the end of the
* character stream is reached.
*/
function readWhileLetterOrDigit(tokenizer) {
return readWhile(tokenizer, (character) => isAlphaNumericCharacter(character));
}
/**
* Read characters from this Tokenizer until one of the provided terminating characters is read or
* the end of the character stream is reached.
*/
function readUntilCharacter(tokenizer, ...terminatingCharacters) {
return readWhile(tokenizer, (character) => terminatingCharacters.indexOf(character) === -1);
}
function nextScheme(tokenizer) {
const scheme = readWhileLetterOrDigit(tokenizer);
tokenizer._currentToken = URLToken.scheme(scheme);
if (!hasCurrentCharacter(tokenizer)) {
tokenizer._currentState = "DONE";
}
else {
tokenizer._currentState = "HOST";
}
}
function nextSchemeOrHost(tokenizer) {
const schemeOrHost = readUntilCharacter(tokenizer, ":", "/", "?");
if (!hasCurrentCharacter(tokenizer)) {
tokenizer._currentToken = URLToken.host(schemeOrHost);
tokenizer._currentState = "DONE";
}
else if (getCurrentCharacter(tokenizer) === ":") {
if (peekCharacters(tokenizer, 3) === "://") {
tokenizer._currentToken = URLToken.scheme(schemeOrHost);
tokenizer._currentState = "HOST";
}
else {
tokenizer._currentToken = URLToken.host(schemeOrHost);
tokenizer._currentState = "PORT";
}
}
else {
tokenizer._currentToken = URLToken.host(schemeOrHost);
if (getCurrentCharacter(tokenizer) === "/") {
tokenizer._currentState = "PATH";
}
else {
tokenizer._currentState = "QUERY";
}
}
}
function nextHost(tokenizer) {
if (peekCharacters(tokenizer, 3) === "://") {
nextCharacter(tokenizer, 3);
}
const host = readUntilCharacter(tokenizer, ":", "/", "?");
tokenizer._currentToken = URLToken.host(host);
if (!hasCurrentCharacter(tokenizer)) {
tokenizer._currentState = "DONE";
}
else if (getCurrentCharacter(tokenizer) === ":") {
tokenizer._currentState = "PORT";
}
else if (getCurrentCharacter(tokenizer) === "/") {
tokenizer._currentState = "PATH";
}
else {
tokenizer._currentState = "QUERY";
}
}
function nextPort(tokenizer) {
if (getCurrentCharacter(tokenizer) === ":") {
nextCharacter(tokenizer);
}
const port = readUntilCharacter(tokenizer, "/", "?");
tokenizer._currentToken = URLToken.port(port);
if (!hasCurrentCharacter(tokenizer)) {
tokenizer._currentState = "DONE";
}
else if (getCurrentCharacter(tokenizer) === "/") {
tokenizer._currentState = "PATH";
}
else {
tokenizer._currentState = "QUERY";
}
}
function nextPath(tokenizer) {
const path = readUntilCharacter(tokenizer, "?");
tokenizer._currentToken = URLToken.path(path);
if (!hasCurrentCharacter(tokenizer)) {
tokenizer._currentState = "DONE";
}
else {
tokenizer._currentState = "QUERY";
}
}
function nextQuery(tokenizer) {
if (getCurrentCharacter(tokenizer) === "?") {
nextCharacter(tokenizer);
}
const query = readRemaining(tokenizer);
tokenizer._currentToken = URLToken.query(query);
tokenizer._currentState = "DONE";
}
//# sourceMappingURL=url.js.map