import { replaceAll } from "./util/utils" ; type URLQueryParseState = "ParameterName" | "ParameterValue" | "Invalid" ; /**
 * A class that handles the query portion of a URLBuilder.
 */ export class URLQuery { private readonly _rawQuery : { [ queryParameterName ] : string | string [ ] ; } = { } ; /**
     * Get whether or not there any query parameters in this URLQuery.
     */ public any ( ) { return Object . keys ( this . _rawQuery ) . length > 0 ; } /**
     * 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.
     */ public set ( parameterName , parameterValue ) { if ( parameterName ) { if ( parameterValue != undefined ) { const newValue = Array . isArray ( parameterValue ) ? parameterValue : parameterValue . 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.
     */ public get ( parameterName ) { return parameterName ? this . _rawQuery [ parameterName ] : undefined ; } /**
     * Get the string representation of this query. The return value will not start with a "?".
     */ public 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.
     */ public 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 { private _scheme : string | undefined ; private _host : string | undefined ; private _port : string | undefined ; private _path : string | undefined ; private _query : URLQuery | undefined ; /**
     * 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.
     */ public setScheme ( scheme ) { if ( ! scheme ) { this . _scheme = undefined ; } else { this . set ( scheme , "SCHEME" ) ; } } /**
     * Get the scheme that has been set in this URL.
     */ public 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.
     */ public setHost ( host ) { if ( ! host ) { this . _host = undefined ; } else { this . set ( host , "SCHEME_OR_HOST" ) ; } } /**
     * Get the host that has been set in this URL.
     */ public 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.
     */ public setPort ( port ) { if ( port == undefined || port === "" ) { this . _port = undefined ; } else { this . set ( port . toString ( ) , "PORT" ) ; } } /**
     * Get the port that has been set in this URL.
     */ public 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.
     */ public setPath ( path ) { if ( ! path ) { this . _path = undefined ; } else { const schemeIndex = path . indexOf ( "://" ) ; if ( schemeIndex !== - 1 ) { const schemeStart = path . lastIndexOf ( "/" , schemeIndex ) ; 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.
     */ public 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.
     */ public getPath ( ) { return this . _path ; } /**
     * Set the query in this URL.
     */ public 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.
     */ public 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.
     */ public getQueryParameterValue ( queryParameterName ) { return this . _query ? this . _query . get ( queryParameterName ) : undefined ; } /**
     * Get the query in this URL.
     */ public getQuery ( ) { return this . _query ? this . _query . toString ( ) : undefined ; } /**
     * Set the parts of this URL by parsing the provided text using the provided startState.
     */ private set ( text , startState ) { const tokenizer = new URLTokenizer ( text , startState ) ; while ( tokenizer . next ( ) ) { const token = tokenizer . current ( ) ; 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" : const 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 }` ) ; } } } } public 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.
     */ public 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 ) ) ; } } public static parse ( text ) { const result = new URLBuilder ( ) ; result . set ( text , "SCHEME_OR_HOST" ) ; return result ; } } type URLTokenizerState = "SCHEME" | "SCHEME_OR_HOST" | "HOST" | "PORT" | "PATH" | "QUERY" | "DONE" ; type URLTokenType = "SCHEME" | "HOST" | "PORT" | "PATH" | "QUERY" ; export class URLToken { public constructor ( public readonly text , public readonly type ) { } public static scheme ( text ) { return new URLToken ( text , "SCHEME" ) ; } public static host ( text ) { return new URLToken ( text , "HOST" ) ; } public static port ( text ) { return new URLToken ( text , "PORT" ) ; } public static path ( text ) { return new URLToken ( text , "PATH" ) ; } public 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 <= characterCode && characterCode <= 57 ) || ( 65 <= characterCode && characterCode <= 90 ) || ( 97 <= characterCode && characterCode <= 122 ) ) ; } /**
 * A class that tokenizes URL strings.
 */ export class URLTokenizer { readonly _textLength : number ; _currentState : URLTokenizerState ; _currentIndex : number ; _currentToken : URLToken | undefined ; public constructor ( readonly _text , state ? ) { this . _textLength = _text ? _text . length : 0 ; this . _currentState = state != undefined ? 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.
     */ public current ( ) { return this . _currentToken ; } /**
     * Advance to the next URLToken and return whether or not a URLToken was found.
     */ public 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" ; }