Drafts Action Directory

HTTP URL

Posted by @clonezone, Last updated: 2018-07-04 12:40:23 UTC

This is a utility action for those creating other JavaScript actions. It does nothing on its own.

This library allows one to take an HTTP/S URL, split it into its component parts, substitute any of them, and get a reassembled URL back out again.

This is a stopgap until Drafts/JavaScript Core supports the URL class described at https://url.spec.whatwg.org/. It defines JavaScript HTTPURL, HTTPURLError, and HTTPURLSearchParams classes that implement a subset of the ones in the parallel classes from the standard without the “HTTP” prefix.

  • HTTP and HTTPS only.
  • No support for relative URLs.
  • ASCII codepoints only.
  • No IP addresses in the host section, yet.
  • No username/password support, yet.
  • No alternate query parameter syntax: ye olde ampersand and equals signs.
Install

Steps

  • script

    // Goals and non-goals of this implementation:
    //
    // * This is a stop-gap until Drafts/JavaScript Core supports
    //   https://url.spec.whatwg.org/.  If you're looking for more readable
    //   documentation, look at https://nodejs.org/api/url.html.
    // * HTTP/HTTPS only.
    // * Parsing and serialization is necessary.
    // * No support for relative URLs.
    // * Due to limitations of JavaScript regular expressions, no support for
    //   non-ASCII characters.
    // * No IP addresses, yet.
    // * No support for username & password, yet.
    // * No support for alternate query syntax.
    
    var HTTPURL;
    var HTTPURLError;
    var HTTPURLSearchParams;
    
    (
        function () {
            const protocolPattern        = '(https?)';
            const preOriginPattern       = '://';
            const domainComponentPattern = '[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?';
            const domainPattern          =
                `(${domainComponentPattern}(?:[.]${domainComponentPattern})*)`;
            const portPattern            = '(?:[:]([1-9][0-9]{0,4}))?';
            const pathPattern            = '([^?#]*)?';
            const queryPattern           = '([?](?:[^#]*))?';
            const fragmentPattern        = '(#(?:.*\\S)?)?';
            const regex                  = new RegExp(
                    '^\\s*'
                +   protocolPattern
                +   preOriginPattern
                +   domainPattern
                +   portPattern
                +   pathPattern
                +   queryPattern
                +   fragmentPattern
                +   '\\s*$',
                'i',
            );
    
            HTTPURLError = class HTTPURLError extends Error {};
    
            function parseQuery(self, string) {
                self.storage = [];
    
                if (! string || string.length < 1) {
                    return;
                }
    
                if (string[0] === '?') {
                    string = string.substr(1);
                }
    
                for (const param of string.split('&')) {
                    const equals = param.indexOf('=');
                    let   key;
                    let   value;
    
                    if (equals < 0) {
                        key   = param;
                        value = '';
                    } else {
                        key   = param.substring(0, equals);
                        value = param.substr(equals + 1);
                    }
    
                    self.storage.push(
                        [ decodeURIComponent(key), decodeURIComponent(value) ]
                    );
                }
            }
    
            HTTPURLSearchParams = class HTTPURLSearchParams {
                constructor(string) {
                    parseQuery(this, string);
                }
    
                replaceContent(string) {
                    parseQuery(this, string);
                }
    
                append(name, value) {
                    this.storage.push( [name, value] );
                }
    
                delete(name) {
                    this.storage = this.storage.filter(entry => entry[0] !== name);
                }
    
                entries() {
                    return this.storage;
                }
    
                [Symbol.iterator]() {
                    let nextIndex = 0;
    
                    return {
                        next: () => {
                            if (nextIndex < this.storage.length) {
                                return {
                                    value: this.storage[nextIndex++], done: false,
                                };
                            }
    
                            return {done: true};
                        }
                    };
                }
    
                forEach(callback, callbackArgument) {
                    for (const entry of this.storage) {
                        callback(entry[0], entry[1], callbackArgument);
                    }
                }
    
                get(name) {
                    let found = this.storage.find( entry => entry[0] === name );
                    if (found) {
                        return found[1];
                    }
    
                    return null;
                }
    
                getAll(name) {
                    return this.storage
                        .filter( entry => entry[0] === name )
                        .map( entry => entry[1] );
                }
    
                has(name) {
                    return !! this.storage.find( entry => entry[0] === name );
                }
    
                keys() {
                    return this.storage.map(entry => entry[0]);
                }
    
                set(name, value) {
                    const newStorage = [];
                    let   found      = false;
    
                    for (const entry of this.storage) {
                        if (entry[0] === name) {
                            if (! found) {
                                found = true;
    
                                newStorage.push( [name, value] );
                            }
                        } else {
                            newStorage.push(entry);
                        }
                    }
    
                    if (! found) {
                        newStorage.push( [name, value] );
                    }
    
                    this.storage = newStorage;
                }
    
                sort() {
                    this.storage.sort(
                        function (a, b) {
                            const a_key = a[0];
                            const b_key = b[0];
    
                            if (a_key < b_key) {
                                return -1;
                            }
    
                            if (a_key > b_key) {
                                return 1;
                            }
    
                            return 0;
                        }
                    );
                }
    
                values() {
                    return this.storage.map(entry => entry[1]);
                }
    
                toString() {
                    let string    = '';
                    let separator = '?';
    
                    for (const param of this.storage) {
                        string += separator;
                        string += encodeURIComponent( param[0] );
    
                        if ( param[1] ) {
                            string += '='
                            string += encodeURIComponent( param[1] );
                        }
    
                        separator = '&';
                    }
    
                    return string;
                }
            };
    
            function parseURL(self, string) {
                let match = regex.exec(string);
                if (! match) {
                    throw new HTTPURLError(
                        `"${string}" does not look like an HTTP URL.`
                    );
                }
    
                self.protocol = match[1];
                self.hostname = match[2];
                self.port     = match[3];
                self.pathname = match[4];
                self.search   = match[5];
                self.hash     = match[6];
            }
    
            HTTPURL = class HTTPURL {
                constructor(string) {
                    this.searchParams = new HTTPURLSearchParams('');
    
                    parseURL(this, string);
                }
    
                get href() {
                    return this.toString();
                }
    
                set href(string) {
                    parseURL(this, string);
                }
    
                get search() {
                    return this.searchParams.toString();
                }
    
                set search(string) {
                    this.searchParams.replaceContent(string);
                }
    
                toString() {
                    let string = `${this.protocol}://${this.hostname}`;
    
                    if (this.port) {
                        string += ':';
                        string += this.port;
                    }
    
                    if (this.pathname) {
                        string += this.pathname;
                    }
    
                    const search = this.search;
                    if (search) {
                        string += search;
                    }
    
                    if (this.hash) {
                        string += this.hash;
                    }
    
                    return string
                }
            }
        }
    )();
    
    // let url = new HTTPURL('https://hostname:443#');
    // console.log('Parse of simple URL:');
    // console.log(url);
    // console.log(url.href);
    // 
    // url.href = 'http://hostname.tld/foo/bar?foo=bar&baz=quuz&eggs&bacon&foo%20bar=bar%20foo&eggs&bacon#fragment';
    // console.log('');
    // console.log('Parse of complicated URL:');
    // console.log(url);
    // console.log(url.href);
    // 
    // url.search = '?alpha=beta&gamma=delta';
    // console.log('');
    // console.log('Assignment to .search:');
    // console.log(url);
    // console.log(url.href);
    // 
    // url.searchParams.append('null');
    // url.searchParams.append('null', '');
    // url.searchParams.append('epsilon', 'zeta');
    // console.log('');
    // console.log('Several calls to .searchParams.append():');
    // console.log(url);
    // console.log(url.href);
    // 
    // url.searchParams.delete('null');
    // console.log('');
    // console.log('.searchParams.delete():');
    // console.log(url);
    // console.log(url.href);
    // 
    // // Test of iterable interface:
    // console.log('');
    // console.log('Iterating over .searchParams:');
    // for (const queryParameter of url.searchParams) {
    //     console.log(queryParameter);
    // }
    // 
    // console.log('');
    // console.log('Iterating over .searchParams.entries():');
    // for (const queryParameter of url.searchParams.entries()) {
    //     console.log(queryParameter);
    // }
    // 
    // console.log('');
    // console.log('Iterating over .searchParams.keys():');
    // for (const key of url.searchParams.keys()) {
    //     console.log(key);
    // }
    // 
    // console.log('');
    // console.log('Iterating over .searchParams.values():');
    // for (const value of url.searchParams.values()) {
    //     console.log(value);
    // }
    // 
    // console.log('');
    // console.log('Internal iteration via .searchParams.forEach():');
    // url.searchParams.forEach(
    //     (name, value, extra) => { console.log( `${extra} ${name} ${value}` ); },
    //     'Here ->',
    // );
    // 
    // console.log('');
    // console.log('.searchParams.get():');
    // console.log( url.searchParams.get('gamma') );
    // console.log( url.searchParams.get('no such parameter') );
    // 
    // console.log('');
    // console.log('.searchParams.has():');
    // console.log( url.searchParams.has('gamma') );
    // console.log( url.searchParams.has('no such parameter') );
    // 
    // url.search = 'a=b&b=c&d=e&f=g&a=b&b=c&d=e&f=g'
    // console.log('');
    // console.log('.searchParams.getAll():');
    // console.log( url.searchParams.getAll('b') );
    // console.log( url.searchParams.getAll('no such parameter') );
    // 
    // url.searchParams.set('a', 'XXX')
    // url.searchParams.set('h', 'OOO')
    // console.log('');
    // console.log('.searchParams.set():');
    // console.log(url);
    // console.log(url.href);
    // 
    // url.search = 'a=b&b=c&d=e&f=g&a=b&b=c&d=e&f=g'
    // url.searchParams.sort();
    // console.log('');
    // console.log('.searchParams.sort():');
    // console.log(url);
    // console.log(url.href);
    

Options

  • After Success Default
    Notification Error
    Log Level Error

Comments

Actions available in the Action Directory are uploaded by community members. Use appropriate caution reviewing downloaded actions before use.