JavaScript – Modify URL Parameters

Recently I had to develop a solution which involved changing the HREF attributes of links in the navigation bar of a site. This had to be done on the JavaScript side because at my company we are the 3rd-party JavaScript solution for modifying other companies’ sites. That being said, I came up with one solution because the URL parameter was always the same but I figured I would publicly release the code for doing this in such a way that you can modify the value of any URL parameter:

/**
 * @license Copyright 2013 - Chris West - MIT Licensed
 */
(function(expCharsToEscape, expEscapedSpace, expNoStart, undefined) {
  /**
   * Modifies the given URL, returning it with the given parameter
   * changed to the given value.  The parameter is added if it didn't
   * already exist.  The parameter is removed if null or undefined is
   * specified as the value.
   * @param {string} url  The URL to be modified.
   * @param {string} paramName  The URL parameter whose value will be
   *     modified.
   * @param {string} paramValue  The value to assign.  This will be
   *     escaped using encodeURIComponent.
   * @return {string}  The updated URL.
   */
  modURLParam = function(url, paramName, paramValue) {
    paramValue = paramValue != undefined
      ? encodeURIComponent(paramValue).replace(expEscapedSpace, '+')
      : paramValue;
    var pattern = new RegExp(
      '([?&]'
      + paramName.replace(expCharsToEscape, '\\$1')
      + '=)[^&]*'
    );
    if(pattern.test(url)) {
      return url.replace(
        pattern,
        function($0, $1) {
          return paramValue != undefined ? $1 + paramValue : ''; 
        }
      ).replace(expNoStart, '$1?');
    }
    else if (paramValue != undefined) {
      return url + (url.indexOf('?') + 1 ? '&' : '?')
        + paramName + '=' + paramValue;
    }
    else {
      return url;
    }
  };
})(/([\\\/\[\]{}().*+?|^$])/g, /%20/g, /^([^?]+)&/);

The following are some example calls to this function:

// Initial URL
var url = 'http://example.com/';
alert(url);

// http://example.com/?q=search+term
url = modURLParam(url, 'q', 'search term');
alert(url);

// http://example.com/?q=search+term&name=Guillermo
url = modURLParam(url, 'name', 'Guillermo');
alert(url);

// http://example.com/?q=search+term+2&name=Guillermo
url = modURLParam(url, 'q', 'search term 2');
alert(url);

// http://example.com/?name=Guillermo
url = modURLParam(url, 'q');
alert(url);

// http://example.com/?name=Guillermo&q=termino
url = modURLParam(url, 'q', 'termino');
alert(url);

// http://example.com/?name=Guillermo
url = modURLParam(url, 'q', null);
alert(url);

Feel free to re-use the code! 8)

JavaScript – Substitution Groups & Functions

One of the things that I often end up doing is using JavaScript’s replace function with a regular expression and a callback function. Recently, though, I have been thinking about redeveloping an HTML application (HTA) that I made a long time ago for PCs. This HTA gave me the ability to write regular expressions and replacement strings which could alter the matched groups in ways that normal string substitution doesn’t allow without using callback functions. Let’s take the following file names as examples:

  • 001-the-nephews-come-to-town.mpg
  • 002-scroogey-scroogers.mp4
  • 32-hewi’s-lost-pet.mp4
  • 109-copped-by-the-coppers.mpeg

The file renamer that I use to have would allow these files to be renamed as the following by using /^(d+)(.+)(?=.w+$)/ as the regular expression and "Episode ${1,RLZ} -${2,D2S,PROPER}" as the replacement:

  • Episode 1 – The Nephews Come To Town.mpg
  • Episode 2 – Scroogey Scroogers.mp4
  • Episode 32 – Hewi’s Lost Pet.mp4
  • Episode 109 – Copped By The Coppers.mpeg

As the first step towards achieving my goal, I wrote the following sub function which can accomplish this:

(function() {
  var functions = {};
  var hasOwnProperty = functions.hasOwnProperty;
  this.sub = function(subject, reTarget, strReplacement, objFns) {
    if(!reTarget && !strReplacement && !objFns) {
      for(var key in subject) {
        if(hasOwnProperty.call(subject, key)) {
          functions[key] = subject[key];
        }
      }
    }
    else {
      return subject.replace(reTarget, function(match) {
        var args = arguments;
        var reGroups = [];
        var i = args.length - 2;
        while(--i > 0) {
          reGroups.push(i);
        }
        reGroups = '(' + reGroups.join('|') + ')';
        reGroups = new RegExp('\$(?:' + reGroups + '|\{' + reGroups + '((?:,\w+)*)\})', 'g');
        return strReplacement.replace(reGroups, function(match, index, index2, fnsToUse) {
          fnsToUse = fnsToUse ? fnsToUse.slice(1).split(',') : [];
          var ret = args[index || index2];
          for(var i = 0, len = fnsToUse.length; i < len; i++) {
            var fnName = fnsToUse[i];
            var fn;
            if((objFns && hasOwnProperty.call(objFns, fnName) && (fn = objFns[fnName]))
                || (hasOwnProperty.call(functions, fnName) && (fn = functions[fnName]))) {
              ret = fn(ret, args);
            }
          }
          return ret;
        });
      });
    }
  };
})();

This function actually acts differently depending on the parameters that are supplied. The first way in which the function can be called is with four parameters:

  1. subject – string:
    The string that is to be modified.
  2. reTarget – RegExp:
    The regular expression used to capture substrings.
  3. strReplacement – string:
    The replacement string which can have the normal captured group expressions (eg. $1, $3, etc.), the new type of captured group expressions (eg. ${1}, ${3,RLZ}, etc.) and/or normal substrings.
  4. objFns – Object:
    Optional object whose keys correspond to functions referenced in the new type of group expressions. Each value should be a function where the first parameter passed to it will be the matched group and the second will be the arguments object which is normally passed to the callback function. The functions should return the string replacement for the captured group expression.
var filename = sub(
  "023-we-are-the-tigers.jpg",
  /^(d+)(.+)(?=.w+$)/,
  "Episode ${1,RLZ} -${2,D2S,PROPER}",
  {
    RLZ: function(match) {
      return match.replace(/^0+(?!$)/, '');
    },
    D2S: function(match) {
      return match.replace(/-/g, ' ');
    },
    PROPER: function(match) {
      return match.toProperCase();
    },
    UPPER: function(match) {
      return match.toUpperCase();
    },
    CHARCODE: function(match) {
      return '<' + match + '=' + match.charCodeAt(0) + '>';
    }
  }
);
alert(filename);  // "Episode 23 - We Are The Tigers.jpg"

var str = sub(
  "abcdefghijklmnopqrstuvwxyz",
  /([aeiou])/g,
  "<${1,UPPER}>",
  {
    UPPER: function(match) {
      return match.toUpperCase();
    }
  }
);
alert(str);  // "<A>bcd<E>fgh<I>jklmn<O>pqrst<U>vwxyz" 

The reason the fourth parameter is optional is because those captured group expression replacement functions could be predefined for all calls to this sub function. If the function is just called with one parameter, that parameter should be the same as the fourth parameter outlined previously. The captured group expression replacement functions will remain persistent for the remainder of the page/program session:

sub({
  RLZ: function(match) {
    return match.replace(/^0+(?!$)/, '');
  },
  D2S: function(match) {
    return match.replace(/-/g, ' ');
  },
  PROPER: function(match) {
    return match.toProperCase();
  }
});
var filenames = [
  "001-the-nephews-come-to-town.mpg",
  "002-scroogey-scroogers.mp4",
  "32-hewi's-lost-pet.mp4",
  "109-copped-by-the-coppers.mpeg"
];
var exp = /^(d+)(.+)(?=.w+$)/;
var replacement = "Episode ${1,RLZ} -${2,D2S,PROPER}";
for(var i = 0; i < filenames.length; i++) {
  filenames[i] = sub(filenames[i], exp, replacement);
}
alert(filenames.join('n'));

The above code will result in the following filenames:

Episode 1 - The Nephews Come To Town.mpg
Episode 2 - Scroogey Scroogers.mp4
Episode 32 - Hewi's Lost Pet.mp4
Episode 109 - Copped By The Coppers.mpeg

Well, now that I have this code out of the way, hopefully the next step will be to actually make the File Renamer. When I do finish creating it, you can be sure to find it on this blog. 8)

JavaScript – String.prototype.matchAll(regexp)

One of the nice things that many people don’t know about JavaScript String replace() function is the fact that the second parameter can either be a string or a callback function. This callback function receives the entire matched substring, each parenthesized group (if not found the empty string is passed), the index of the match within the original string, and the original string. You can actually get these values using the String match() function, but this doesn’t work for retrieving these values for each instance of a globally matched regular expression. In this case, one could use the following code to define a matchAll() function:

String.prototype.matchAll = function(regexp) {
  var matches = [];
  this.replace(regexp, function() {
    var arr = ([]).slice.call(arguments, 0);
    var extras = arr.splice(-2);
    arr.index = extras[0];
    arr.input = extras[1];
    matches.push(arr);
  });
  return matches.length ? matches : null;
};

If a global regular expression is passed to the function defined above, an array of arrays similar to those that come from the native match() will be returned. As occurs with the native match() function, each sub-array will have an index property and an input property. The index property indicates the position where this particular match began in the original string. The input property indicates the string that the matches were found in.

The following is an example of running this function:

var str = 'Hello world!!!';
var regexp = /(w+)W*/g;
console.log(str.matchAll(regexp));

An array containing two arrays will be generated:

[
  {
    0: "Hello ",
    1: "Hello"
    index: 0,
    input: "Hello world!!!"
  },
  {
    0: "world!!!",
    1: "world"
    index: 6,
    input: "Hello world!!!"
  }
]

Even though the sub-arrays above are really object literals, the arrays that come back from this matchAll() function will be true arrays with those two properties (index and input) set for each. Hopefully this helps. 8)

Regular Expression – Match A String Literal

Sometimes you may have to use Textwrangler, SublimeText, Notepad++, or whatever IDE to search for a string which contains something commonly found outside of a string. For example, what if you want to find a string that in a Python file that had a hash symbol. In this case you could use the following regular expression:
"([^\"]+|\.)*#([^\"]+|\.)*"

In general, the regular expression can look like the following to match any string literal:
"([^\"]+|\.)*"|'([^\']+|\.)*'

Of course this regular expression really depends on the language that you are dealing with. Some languages such as VB require strings to be surrounded in double-quotes and each double-quote is escaped by placing another double-quote character in front of it.

JavaScript – RegExp Testing with Global Flag Set

If you create a regular expression and set its global flag to true, it is important to note that if you want to test multiple strings against that RegExp using the test() function you will most-likely want to zero out the regular expression’s lastIndex property. Take the following code for example:

var word, words = "Hello world!".split(" "), regExpO = /o/g, i = 0;
while(word = words[i++]) {
  console.log('"' + word + '" ' + (regExpO.test(word) ? 'passes' : 'fails'));
}

The following is output:

"Hello" passes
"world!" fails

On the other hand, if I were to zero out the lastIndex property every time, both parts would pass:

var word, words = "Hello world!".split(" "), regExpO = /o/g, i = 0;
while(word = words[i++]) {
  console.log('"' + word + '" ' + (regExpO.test(word) ? 'passes' : 'fails'));
  regExpO.lastIndex = 0;  // reset for next use of the text() function.
}

Now the following will be output:

"Hello" passes
"world!" passes

Although JavaScript is fun, at times it can be annoying as well because it at times acts different than you may expect. 8)