﻿//-----------------------------------------------------------------------------
// XString
//
// Copyright 2005-2010 - Xcential Group LLC.
//
//-----------------------------------------------------------------------------

XString.prototype = new XObject;
XString.prototype.constructor = XString;

//=============================================================================
// Constructor

function XString(
   text,
   create
)
{
   text = (text == null) ? "" : text;
   create = (create == null) ? true : false;

   if (create)
      return new XString(text, false);

   //--------------------------------------------------------------------------
   // Private Interface


   //--------------------------------------------------------------------------
   // Privileged Interface

   this.valueOf = function()
   {

      return oText;
   }

   //--------------------------------------------------------------------------

   this.setObjectValue = function(
      text
   )
   {
      text = (text == null) ? null : text;

      oText = XString.getStringFrom(text);

      return oText;
   }

   //--------------------------------------------------------------------------
   // Initialization

   var oText = this.setObjectValue(text);

}

XString.prototype.objectClass = "XString";

//=============================================================================
// Static Interface

XString.ENCODE_BACKSLASH         = "encode_backslash";
XString.ENCODE_ENTITY            = "encode_entity";
XString.ENCODE_ESCAPE            = "encode_escape";
XString.ENCODE_ESCAPE_SQL        = "encode_escape_sql";
XString.ENCODE_URI               = "encode_uri";
XString.ENCODE_URI_COMPONENT     = "encode_uri_component";
XString.ENCODE_MD5               = "encode_md5";
XString.ENCODE_BASE64            = "encode_base64";

XString.LOWER_CASE_WORDS = [];
XString.LOWER_CASE_WORDS["of"]   = "of";
XString.LOWER_CASE_WORDS["the"]  = "the";
XString.LOWER_CASE_WORDS["and"]  = "and";
XString.LOWER_CASE_WORDS["to"]   = "to";
XString.LOWER_CASE_WORDS["for"]  = "for";
XString.LOWER_CASE_WORDS["with"] = "with";
XString.LOWER_CASE_WORDS["at"]   = "at";
XString.LOWER_CASE_WORDS["on"]   = "on";
XString.LOWER_CASE_WORDS["in"]   = "in";
XString.LOWER_CASE_WORDS["out"]  = "out";
XString.LOWER_CASE_WORDS["from"] = "from";

XString.UPPER_CASE_WORDS = [];
XString.UPPER_CASE_WORDS["URI"]  = "URI";
XString.UPPER_CASE_WORDS["URN"]  = "URN";
XString.UPPER_CASE_WORDS["URL"]  = "URL";
XString.UPPER_CASE_WORDS["CONS"] = "CONS";
XString.UPPER_CASE_WORDS["CA"]   = "CA";
XString.UPPER_CASE_WORDS["LCB"]  = "LCB";
XString.UPPER_CASE_WORDS["LDC"]  = "LDC";
XString.UPPER_CASE_WORDS["TOSA"]  = "TOSA";
XString.UPPER_CASE_WORDS["ASM"]  = "ASM";
XString.UPPER_CASE_WORDS["SEN"]  = "SEN";
XString.UPPER_CASE_WORDS["RLS"]  = "RULES";
XString.UPPER_CASE_WORDS["RSS"]  = "RSS";
XString.UPPER_CASE_WORDS["RSS1"] = "RSS1";
XString.UPPER_CASE_WORDS["RSS2"] = "RSS2";
XString.UPPER_CASE_WORDS["HTML"] = "HTML";
XString.UPPER_CASE_WORDS["RDF"]  = "RDF";

XString.CAPITALIZED_PHRASES                        = [];
XString.CAPITALIZED_PHRASES["state of california"] = "State of California";
XString.CAPITALIZED_PHRASES["california"]          = "California";

XString.NO_TRIM_LEFT     = false;
XString.NO_TRIM_RIGHT    = false;
XString.TRIM_LEFT        = true;
XString.TRIM_RIGHT       = true;
XString.FULL_SENTENCE    = true;
XString.SEPARATE_HYPHENS = true;
XString.ADD_ELLIPSIS     = true;
XString.OPTIMIZE         = true;
XString.PASS_JUDGEMENT   = true;
XString.PRESERVE_CASE    = true;
XString.PRESERVE_NULL    = true;
XString.MAKE_NULL        = true;

XString.CAMEL_LOWER = "lower";
XString.CAMEL_UPPER = "upper";

XString.PLURAL = new Array();
XString.PLURAL["criteria"]      = "criterion";
XString.PLURAL["criterion"]     = "criterion";
XString.PLURAL["addendum"]      = "addenda";
XString.PLURAL["addenda"]       = "addenda";
XString.PLURAL["medium"]        = "media";
XString.PLURAL["media"]         = "media";
XString.PLURAL["alumnus"]       = "alumni";
XString.PLURAL["alumni"]        = "alumni";
XString.PLURAL["child"]         = "children";
XString.PLURAL["children"]      = "children";
XString.PLURAL["person"]        = "people";
XString.PLURAL["people"]        = "people";
XString.PLURAL["agendum"]       = "agendas";
XString.PLURAL["datum"]         = "data";
XString.PLURAL["data"]          = "data";
XString.PLURAL["information"]   = "information";
XString.PLURAL["info"]          = "info";
XString.PLURAL["knowledge"]     = "knowledge";
XString.PLURAL["to"]            = "to";
XString.PLURAL["of"]            = "of";
XString.PLURAL["for"]           = "for";
XString.PLURAL["on"]            = "on";
XString.PLURAL["under"]         = "under";
XString.PLURAL["before"]        = "on";
XString.PLURAL["after"]         = "on";

XString.SINGULAR = new Array();
XString.SINGULAR["criteria"]    = "criteria";
XString.SINGULAR["criterion"]   = "criteria";
XString.SINGULAR["addendum"]    = "addendum";
XString.SINGULAR["addenda"]     = "addendum";
XString.SINGULAR["medium"]      = "medium";
XString.SINGULAR["media"]       = "medium";
XString.SINGULAR["alumnus"]     = "alumnus";
XString.SINGULAR["alumni"]      = "alumnus";
XString.SINGULAR["child"]       = "child";
XString.SINGULAR["children"]    = "child";
XString.SINGULAR["person"]      = "person";
XString.SINGULAR["people"]      = "person";
XString.SINGULAR["agendum"]     = "agendum";
XString.SINGULAR["datum"]       = "datum";
XString.SINGULAR["data"]        = "datum";
XString.SINGULAR["information"] = "information";
XString.SINGULAR["info"]        = "info";
XString.SINGULAR["knowledge"]   = "knowledge";
XString.SINGULAR["to"]          = "to";
XString.SINGULAR["of"]          = "of";
XString.SINGULAR["for"]         = "for";
XString.SINGULAR["on"]          = "on";
XString.SINGULAR["under"]       = "under";
XString.SINGULAR["before"]      = "on";
XString.SINGULAR["after"]       = "on";

//-----------------------------------------------------------------------------

XString.getStringFrom = function(
   value
)
{
   value = (value == null) ? null : value;

   if (value == null)
      return "";

   switch (typeof(value))
   {
      case "string":
         return value;
         break;
      case "number":
         return String(value).toString();
         break;
      case "boolean":
         return (value) ? "true" : "false";
         break;
      case "function":
         try
         {
            return XString.getStringFrom(value());
         }
         catch (error)
         {
            return "{function}";
         }
         break;
      case "object":
         try
         {
            return value.toString();
         }
         catch (error)
         {
            return "{object}";
         }
         break;
   }

   return "{null}";
}

//-----------------------------------------------------------------------------

XString.toUpperCase = function(
   text
)
{

   return (text) ? text.toUpperCase() : text;
}

//-----------------------------------------------------------------------------

XString.toLowerCase = function(
   text
)
{

   return (text) ? text.toLowerCase() : text;
}

//-----------------------------------------------------------------------------

XString.trim = function(
   text,
   trimLeft,
   trimRight
)
{
   trimLeft  = (trimLeft == null)  ? XString.TRIM_LEFT  : trimLeft;
   trimRight = (trimRight == null) ? XString.TRIM_RIGHT : trimRight;

   if (trimLeft)
      text = text.replace(/^[\u00A0\u2002\u2003\s]+/, "");

   if (trimRight)
      text = text.replace(/[\u00A0\u2002\u2003\s]+$/, "");

   return text;
}

//-----------------------------------------------------------------------------

XString.truncate = function(
   text,
   maxLength,
   preserveNull,
   makeNull,
   addEllipsis
)
{
   text         = (text == null)         ? null  : text;
   maxLength    = (maxLength == null)    ? null  : maxLength;
   preserveNull = (preserveNull == null) ? false : preserveNull;
   makeNull     = (makeNull == null)     ? false : makeNull
   addEllipsis  = (addEllipsis == null)  ? false : addEllipsis;

   if (text == null)
   {
      if (preserveNull || makeNull)
         return null;
      else
         text = "";
   }

   if (text == "")
   {
      if (makeNull)
         return null;
      else
         return "";
   }

   if (maxLength == null || text.length <= maxLength)
      return text;

   if (addEllipsis)
      return XString.trim(text.substr(0, maxLength)) + "...";

   return text.substr(0, maxLength);
}

//-----------------------------------------------------------------------------

XString.normalize = function(
   text
)
{

   text = XString.trim(text, XString.TRIM_LEFT, XString.TRIM_RIGHT);
   text = text.replace(/[\u00A0\u2002\u2003\s]+/g, " ");

   return text;
}

//-----------------------------------------------------------------------------

XString.removeAccents = function(
   text
)
{

   text = text.replace(/[\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6]/g, "a");
   text = text.replace(/[\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6]/g, "A");
   text = text.replace(/[\u00E7]/g, "c");
   text = text.replace(/[\u00C7]/g, "C");
   text = text.replace(/[\u00E8\u00E9\u00EA\u00EB]/g, "e");
   text = text.replace(/[\u00C8\u00C9\u00CA\u00CB]/g, "E");
   text = text.replace(/[\u00EC\u00ED\u00EE\u00EF]/g, "i");
   text = text.replace(/[\u00CC\u00CD\u00CE\u00CF]/g, "I");
   text = text.replace(/[\u00F1]/g, "n");
   text = text.replace(/[\u00D1]/g, "N");
   text = text.replace(/[\u00F2\u00F3\u00F4\u00F5\u00F6]/g, "o");
   text = text.replace(/[\u00D2\u00D3\u00D4\u00D5\u00D6]/g, "O");
   text = text.replace(/[\u00F9\u00FA\u00FB\u00FC]/g, "u");
   text = text.replace(/[\u00D9\u00DA\u00DB\u00DC]/g, "U");
   text = text.replace(/[\u00FD\u00FF]/g, "y");
   text = text.replace(/[\u00FD]/g, "Y");

   return text;
}

//=============================================================================
// Public Interface

XString.prototype.toString = function()
{

   return this.valueOf();
}

//-----------------------------------------------------------------------------

XString.prototype.length = function()
{

   return this.valueOf().length;
}

//-----------------------------------------------------------------------------

XString.prototype.toUpperCase = function()
{

   return this.setObjectValue(this.valueOf().toUpperCase());
}

//-----------------------------------------------------------------------------

XString.prototype.toLowerCase = function()
{

   return this.setObjectValue(this.valueOf().toLowerCase());
}

//-----------------------------------------------------------------------------

XString.prototype.toTitleCase = function(
   fullSentence,
   preserveCase
)
{
   fullSentence = (fullSentence == null) ? false : fullSentence;
   preserveCase = (preserveCase == null) ? false : preserveCase;

   var text = this.valueOf();
   if (text.length > 0)
   {
      text = XString.normalize(text);
      var words = text.split(" ");
      text = "";
      for (var i=0; i<words.length; i++)
      {
         var word = words[i];
         var left = null, right = null;
         if (XMatch(word, /^([^A-Za-z]*)([A-Za-z0-9\-]+)([^A-Za-z]*)$/))
         {
            left = XMatch.matches[1];
            word = XMatch.matches[2];
            right = XMatch.matches[3];
         }
         if (XString.UPPER_CASE_WORDS[word.toUpperCase()] != null)
            word = XString.UPPER_CASE_WORDS[word.toUpperCase()];
         else if (i != 0 && XString.LOWER_CASE_WORDS[word.toLowerCase()] != null)
            word = XString.LOWER_CASE_WORDS[word.toLowerCase()];
         else if (fullSentence)
            word = word.substr(0,1).toUpperCase() + ((preserveCase) ? word.substr(1, word.length-1) : word.substr(1, word.length-1).toLowerCase());
         else if (i==0)
            word = word.substr(0,1).toUpperCase() + ((preserveCase) ? word.substr(1, word.length-1) : word.substr(1, word.length-1).toLowerCase());
         else
            word = (preserveCase) ? word : word.toLowerCase();
         text += ((text.length > 0) ? " " : "") + ((left) ? left : "") + word + ((right) ? right : "");
      }

      if (!fullSentence)
      {
         text = " " + text + " ";
         for (index in XString.CAPITALIZED_PHRASES)
         {
            var phrase = XString.CAPITALIZED_PHRASES[index];
            text = text.replace(new RegExp(" " + phrase + "([\\.\\s])", "gi"), " " + phrase + "$1");
         }
         text = XString(text).trim();
      }

      // Special handling for obvious acronyms
      while (XMatch(text,/\.[a-z]/))
         text = XMatch.leftContext + XMatch.lastMatch.toUpperCase() + XMatch.rightContext;

   }

   return this.setObjectValue(text);
}

//-----------------------------------------------------------------------------

XString.prototype.toCamelCase = function(
   style
)
{
   style = (style == null) ? XString.CAMEL_UPPER : style;

   var text = this.valueOf();
   text = text.replace(/\s+/g, " ");
   var words = text.split(" ");
   text = "";
   for (var i=0; i<words.length ; i++)
   {
      var word = words[i];
      if (i == 0 && style == XString.CAMEL_LOWER)
         word = word.charAt(0).toLowerCase() + word.substr(1);
      else
         word = word.charAt(0).toUpperCase() + word.substr(1);
      text += word;
   }

   return this.setObjectValue(text);
}

//-----------------------------------------------------------------------------

XString.prototype.toEnum = function()
{

   var text = XString.normalize(this.valueOf());
   var words = text.split(" ");
   text = "";
   for (var i=0; i<words.length; i++)
   {
      var word = words[i];
      word = word.toUpperCase();
      text += ((text.length > 0) ? "_" : "") + word;
   }

   return this.setObjectValue(text);
}

//-----------------------------------------------------------------------------

XString.prototype.coerce = function(
   toType
)
{
   toType = (toType == null) ? null : toType.toLowerCase();

   var text = XString.normalize(this.valueOf());

   if (toType == "boolean")
   {
      if (text == "")
         return false;
      if ((/^false$/i).test(text))
         return false;
      if ((/^null$/i).test(text))
         return false;
      if ((/^NaN$/i).test(text))
         return false;
      if ((/^void$/i).test(text))
         return false;
      if ((/^undefined$/i).test(text))
         return false;
      if ((/^0+$/i).test(text))
         return false;
      return true;
   }
   else if (toType == "number")
   {
      return Number(text).valueOf();
   }
   else if (toType == null)
   {
      if ((/^false$/i).test(text))
         return false;
      if ((/^true$/i).test(text))
         return true;
      if ((/^[0-9]+$/i).test(text))
         return Number(text).valueOf();
      if ((/^[0-9]*\.[0-9]+$/i).test(text))
         return Number(text).valueOf();
      if ((/^[0-9]+\.[0-9]*$/i).test(text))
         return Number(text).valueOf();
      return text;
   }

   return text;
}

//-----------------------------------------------------------------------------

XString.prototype.split = function(
   separator
)
{
   separator = (separator == null) ? " " : separator;

   var text = this.valueOf();
   if (separator == " ")
      text = XString.normalize(text);
   var textArray = (text.length > 0) ? text.split(separator) : [];

   return textArray;
}

//-----------------------------------------------------------------------------

XString.prototype.concat = function(
   cText
)
{
   cText = XString.getStringFrom(cText);

   var text = this.valueOf();
   text = text + cText;

   return this.setObjectValue(text);
}

//-----------------------------------------------------------------------------

XString.prototype.trim = function(
   trimLeft,
   trimRight
)
{
   trimLeft = (trimLeft == null)   ? XString.TRIM_LEFT  : trimLeft;
   trimRight = (trimRight == null) ? XString.TRIM_RIGHT : trimRight;

   return this.setObjectValue(XString.trim(this.valueOf(), trimLeft, trimRight));
}

//-----------------------------------------------------------------------------

XString.prototype.truncate = function(
   maxLength,
   preserveNull,
   makeNull,
   addEllipsis
)
{
   preserveNull = (preserveNull == null) ? false : preserveNull;
   makeNull     = (makeNull == null)     ? false : makeNull;
   addEllipsis  = (addEllipsis == null)  ? false : addEllipsis;

   var value = XString.truncate(this.valueOf(), maxLength, preserveNull, makeNull, addEllipsis);

   // Note: The object value set is not returned as it does not preserve nulls
   this.setObjectValue(value);

   return value;
}

//-----------------------------------------------------------------------------

XString.prototype.normalize = function()
{

   return this.setObjectValue(XString.normalize(this.valueOf()));
}

//-----------------------------------------------------------------------------

XString.prototype.removeAccents = function()
{

   return this.setObjectValue(XString.removeAccents(this.valueOf()));
}

//----------------------------------------------------------------------------

XString.prototype.pluralize = function(
   count
)
{
   count = (count == null) ? true : count;

   var text = this.normalize();

   if (text.length == 0)
      return text;

   if (count > 1 || count == true)
   {
      if (!(/[a-z]/).test(text))
         return text; // Acronyms or numbers - no lower case letters
      var lastWord = this.getLastWord(XString.SEPARATE_HYPHENS);
      var specialPlural = XString.PLURAL[lastWord.toLowerCase()];
      if (specialPlural != null)
         return text.replace(RegExp(lastWord + "$"),"") + lastWord.substr(0,1) + specialPlural.substr(1);
      else if ((/(ch|sh|ss)$/i).test(text))
         text = text + "es";
      else if ((/[^aeiou]o$/i).test(text))
         text = text + "es";
      else if ((/[^aeious]s$/i).test(text))
         text = text; // Already plural
      else if ((/(ies|es|ys)$/i).test(text))
         text = text; // Already plural
      else if ((/[^aeiou]y$/i).test(text))
         text = text.replace(/y$/i,"") + "ies";
      else if ((/man$/).test(text))
         text = text.replace(/man$/, "men");
      else if (!(/s$/).test(text))
         text = text + "s";
   }

   return text;
}

//----------------------------------------------------------------------------

XString.prototype.singularize = function()
{

   var text = this.normalize();

   if (text.length == 0)
      return text;

   if (!(/[a-z]/).test(text))
      return text; // Acronyms or numbers - no lower case letters
   var lastWord = this.getLastWord(XString.SEPARATE_HYPHENS);
   var specialSinglar = XString.SINGULAR[lastWord.toLowerCase()];
   if (specialSinglar != null)
      return text.replace(RegExp(lastWord + "$"),"") + lastWord.substr(0,1) + specialSinglar.substr(1);
   else if ((/(ch|sh|ss)es$/i).test(text))
      text = text.replace(/es$/i, "");
   else if ((/[^aeiou]oes$/i).test(text))
      text = text.replace(/es$/i, "");
   else if ((/[^aeiou]ies$/i).test(text))
      text = text.replace(/ies$/i,"") + "y";
   else if ((/men$/).test(text))
      text = text.replace(/men$/, "man");
   else if ((/[^s]s$/).test(text))
      text = text.replace(/s$/i, "");

   return text;
}

//-----------------------------------------------------------------------------

XString.prototype.openSpaces = function()
{

   var text = this.valueOf();

   text = text.replace(new RegExp("(" + XChar.EN_SPACE + ")", "g"), "$1 ")
   text = text.replace(/(\u2002)/g, XChar.EN_SPACE + " ");
   text = text.replace(new RegExp("(" + XChar.EM_SPACE + ")", "g"), "$1 ")
   text = text.replace(/(\u2003)/g, XChar.EM_SPACE + " ");

   return text;
}

//-----------------------------------------------------------------------------

XString.prototype.closeSpaces = function()
{

   var text = this.valueOf();

   text = text.replace(new RegExp("(" + XChar.EN_SPACE + ") ", "g"), "$1")
   text = text.replace(/(\u2002) /g, "$1")
   text = text.replace(new RegExp("(" + XChar.EM_SPACE + ") ", "g"), "$1")
   text = text.replace(/(\u2003) /g, "$1")

   return text;
}

//-----------------------------------------------------------------------------

XString.prototype.isSpaceNeeded = function()
{

   var text = this.valueOf();

   if (text == null || text == "")
      return false;

   if ((/\s+$/).test(text))
      return false;

   if ((new RegExp(XChar.EM_SPACE + '$')).test(text))
      return false;

   if ((new RegExp(XChar.EN_SPACE + '$')).test(text))
      return false;

   return true;
}

//-----------------------------------------------------------------------------

XString.prototype.encode = function(
   style
)
{
   style = (style == null) ? XString.ENCODE_BACKSLASH : style;

   var text = this.valueOf();
   if (text.length == 0)
      return text;

   switch (style)
   {
      case XString.ENCODE_BACKSLASH:
         text = text.replace(/\./g, "\\.");
         text = text.replace(/\(/g, "\\(");
         text = text.replace(/\)/g, "\\)");
         text = text.replace(/\[/g, "\\[");
         text = text.replace(/\]/g, "\\]");
         text = text.replace(/\"/g, '\\"');
         text = text.replace(/\'/g, "\\'");
         break;
      case XString.ENCODE_ENTITY:
         text = text.replace(/\&/g, "&amp;");
         text = text.replace(/\</g, "&lt;");
         text = text.replace(/\>/g, "&gt;");
         text = text.replace(/\"/g, '&quot;');
         text = text.replace(/\'/g, "&#39;"); // &apos; is not defined in HTML
         break;
      case XString.ENCODE_ESCAPE:
         text = escape(text);
         break;
      case XString.ENCODE_ESCAPE_SQL:
         text = text.replace(/\'/g, "''");
         break;
      case XString.ENCODE_URI:
         text = encodeURI(text);
         break;
      case XString.ENCODE_URI_COMPONENT:
         text = encodeURIComponent(text);
         break;
      case XString.ENCODE_MD5:
         text = XUtils.generateMD5(text);
         break;
      case XString.ENCODE_BASE64:
         text = XUtils.generateBase64(text);
         break;
    }

   return this.setObjectValue(text);
}

//-----------------------------------------------------------------------------

XString.prototype.decode = function(
   style
)
{
   style = (style == null) ? XString.ENCODE_BACKSLASH : style;

   var text = this.valueOf();
   if (text.length == 0)
      return text;

   switch (style)
   {
      case XString.ENCODE_BACKSLASH:
         text = text.replace(/\\\./g, ".");
         text = text.replace(/\\\(/g, "(");
         text = text.replace(/\\\)/g, ")");
         text = text.replace(/\\\[/g, "[");
         text = text.replace(/\\\]/g, "]");
         text = text.replace(/\\\"/g, '"');
         text = text.replace(/\\\'/g, "'");
         break;
      case XString.ENCODE_ENTITY:
         text = text.replace(/&gt;/g, ">");
         text = text.replace(/&lt;/g, "<");
         text = text.replace(/&quot;/g, '"');
         text = text.replace(/&apos;/g, "'");
         text = text.replace(/&#39;/g, "'");
         text = text.replace(/&amp;/g, "&"); // Must be last
         break;
      case XString.ENCODE_ESCAPE:
         text = unescape(text);
         break;
      case XString.ENCODE_ESCAPE_SQL:
         text = text.replace(/''/g, "'");
         text = text.replace(/\\\%/g, "%");
         break;
      case XString.ENCODE_URI:
         text = decodeURI(text);
         break;
      case XString.ENCODE_URI_COMPONENT:
         text = decodeURIComponent(text);
         break;
    }

   return this.setObjectValue(text);
}

//-----------------------------------------------------------------------------

XString.prototype.startsWith = function(
   chars
)
{
   chars = (chars == null) ? "" : chars.toString();

   var text = this.valueOf();

   if (XString(text).isNothing())
      return false;

   var firstChar = text.charAt(0);

   return (chars.indexOf(firstChar) == -1) ? false : true;
}

//-----------------------------------------------------------------------------

XString.prototype.applyParameters = function(
   params
)
{
   params = (params == null) ? [] : params;

   var text = this.valueOf();

   for (var i=0; i<params.length; i++)
   {
      var param = params[i];
      switch (typeof(param))
      {
         case "string":
            text= text.replace(new RegExp("%" + i, "gm"), param);
            break;
         case "number":
            text= text.replace(new RegExp("%" + i, "gm"), String(param));
            break;
         case "boolean":
            text= text.replace(new RegExp("%" + i, "gm"), (param) ? "true" : "false");
            break;
         default:
            try
            {
               paramName = param[0];
               paramValue = param[1];
               text = text.replace(new RegExp("{" + paramName + "}", "gm"), paramValue);
            }
            catch (error)
            {}
            break;
      }
   }

   // After applying any parameters, look to see if any Ids need to be set
   while ((/{id}/).test(text))
      text = text.replace(/{id}/, XUtils.generateId("urn:xcential-com:id:", XUtils.SCHEME_RANDOM));

   return this.setObjectValue(text);
}

//-----------------------------------------------------------------------------

XString.prototype.getLastWord = function(
   separateHyphens
)
{
   separateHyphens = (separateHyphens == null) ? false : separateHyphens;

   var text = this.valueOf();

   var convertedText = XString.trim(text);
   convertedText = (separateHyphens) ? convertedText.replace(/.*[\-\s]+/g, "") : convertedText.replace(/.*\s+/g, "");

   return convertedText;
}

//-----------------------------------------------------------------------------

XString.prototype.diff = function(
   newString,
   optimize,
   passJudgement
)
{
   newString = (newString == null) ? "" : XString(newString).openSpaces();
   optimize = (optimize == null) ? false : optimize;
   passJudgement = (passJudgement == null) ? false : passJudgement;

   var NORMAL = 0;
   var INSERT = 1;
   var DELETE = 2;

   var space = " ";

   //--------------------------------------------------------------------------

   function isEqual(
      word1,
      word2
   )
   {

      if (XMatch(word1, /\{\{[^\}]+\}\}/))
         word1 = XMatch.rightContext;

      if (XMatch(word2, /\{\{[^\}]+\}\}/))
         word2 = XMatch.rightContext;

      return (word1 == word2) ? true : false;
   }


   //--------------------------------------------------------------------------

   function makeTokens(
      inputString
   )
   {
      var resultString = inputString;

      resultString = resultString.replace(/\s*\<\s*/g, " <");
      resultString = resultString.replace(/\s*\>\s*/g, "> ");
      resultString = resultString.replace(/\s+/g, " ");
      resultString = resultString.replace(/^\s+/, "");
      resultString = resultString.replace(/\s+$/, "");

      var tagFound = false;
      while (XMatch(resultString, /\<([^\>]*\s[^\>]*)\>/))
      {
         var elementText = XMatch.matches[1];
         var replacement = elementText.replace(/\s+/g, "%20");
         resultString = resultString.replace(elementText, replacement);
         tagFound = true;
      }

      var resultTokens = (resultString.length > 0) ? resultString.split(" ") : [];
      if (tagFound)
      {
         for (var i=0; i<resultTokens.length; i++)
         {
            resultToken = resultTokens[i];
            if ((/\<[^\\\>]*%20/).test(resultToken))
               resultTokens[i] = resultToken.replace("%20", " ");
         }
      }

      return resultTokens;
   }

   //--------------------------------------------------------------------------

   function getItem(
      item
   )
   {
      return (item) ? [item[0], item[1], item[2]] : null;
   }

   //--------------------------------------------------------------------------

   function getNextItem(
      items,
      shift
   )
   {
      shift = (shift == null) ? true : shift;

      var nextItem = (items.length > 0) ? ((shift) ? items.shift() : items[0] ) : null;

      return (nextItem != null) ? XString.trim(nextItem) : null;
   }

   //--------------------------------------------------------------------------

   function isItemTag(
      item
   )
   {
      if (item && (/^<.*>$/).test(item[1]))
         return true;
      else
         return false;
   }

   //--------------------------------------------------------------------------

   function buildDeleteString(
      deletions,
      deleteLine
   )
   {
      deleteLine = (deleteLine == null) ? false : deleteLine;

      var deleteString = "";
      if (deletions != null && deletions.length > 0)
      {
         var data = "";
         if (typeof(deletions) == "string")
            data = deletions;
         else
         {
            for (var i=0; i<deletions.length; i++)
            {
               var deletion = deletions[i];
               data += deletion;
               data += ((i+1<deletions.length) ? " " : "");
            }
         }
         deleteString = "|--" + data.replace(/\s+([^\s])/g, " |--$1");
      }

      return deleteString;
   }

   //--------------------------------------------------------------------------

   function buildInsertString(
      insertions,
      insertLine
   )
   {
      insertLine = (insertLine == null) ? false : insertLine;

      var insertString = "";
      if (insertions != null && insertions.length > 0)
      {
         var data = "";
         if (typeof(insertions) == "string")
            data = insertions;
         else
         {
            for (var i=0; i<insertions.length; i++)
            {
               data += insertions[i];
               data += (i+1<insertions.length) ? " " : "";
            }
         }
         insertString = "|++" + data.replace(/\s+([^\s])/g, " |++$1");
      }

      return insertString;
   }

   //--------------------------------------------------------------------------

   function buildItemString(
      item
   )
   {

      var itemString = null;
      switch (item[0])
      {
         case NORMAL:
            itemString = item[1];
            break
         case INSERT:
            itemString = buildInsertString(item[1], item[2]);
            break;
         case DELETE:
            itemString = buildDeleteString(item[1], item[2]);
            break;
      }

      return itemString;
   }

   //--------------------------------------------------------------------------

   function combineCommas(
      items
   )
   {

      // Combine commas
      var newItems = [];
      for (var i=0; i<items.length; i++)
      {
         var item = getItem(items[i]);
         if (!item)
            continue;
         if (isItemTag(item))
            continue;
         var numNewItems = newItems.length;
         var oneItemBack = (numNewItems>0) ? getItem(newItems[numNewItems-1]) : null;
         var twoItemsBack = (numNewItems>1) ? getItem(newItems[numNewItems-2]) : null;
         if (XMatch(item[1], /^\s*,\s*(.*)/))
         {
            var remainder = XMatch.matches[1];
            if (oneItemBack && isEqual(item[0], oneItemBack[0]) && !isItemTag(oneItemBack))
            {
               newItems[numNewItems-1][1] = oneItemBack[1] + ",";
               if (remainder.length > 0)
                  newItems.push([item[0], remainder, false]);
               continue;
            }
            else if (oneItemBack && item[0] == DELETE && !isItemTag(oneItemBack))
            {
               if (oneItemBack[0] == NORMAL)
               {
                  newItems[numNewItems-1] = [DELETE, oneItemBack[1] + ",", false];
                  newItems.push([INSERT, oneItemBack[1], false]);
                  if (remainder.length > 0)
                     newItems.push([item[0], remainder, false]);
                  continue;
               }
            }
            else if (oneItemBack && twoItemsBack && item[0] == NORMAL)
            {
               if (oneItemBack[0] == INSERT && !isItemTag(oneItemBack))
               {
                  if (twoItemsBack[0] == DELETE && !isItemTag(twoItemsBack))
                  {
                     newItems[numNewItems-1][1] = oneItemBack[1] + ",";
                     newItems[numNewItems-2][1] = twoItemsBack[1] + ",";
                     if (remainder.length > 0)
                        newItems.push([item[0], remainder, false]);
                     continue;
                  }
               }
            }
         }
         newItems.push(item);
      }

      return newItems;
   }

   //--------------------------------------------------------------------------

   function rearrangeChanges(
      items
   )
   {

      // Collapse insertions that are close and place deletions ahead of insertions
      for (var i=0; i<items.length; i++)
      {
         var item = getItem(items[i]);
         if (!item)
            continue;
         var oneAheadItem = (i<(items.length-1)) ? getItem(items[i+1]) : null;
         var twoAheadItem = (i<(items.length-2)) ? getItem(items[i+2]) : null;
         var threeAheadItem = (i<(items.length-3)) ? getItem(items[i+3]) : null;
         if (oneAheadItem)
         {
            // Place a delete ahead of an insert
            if (item[0] == INSERT && oneAheadItem[0] == DELETE)
            {
               items[i] = oneAheadItem;
               items[i+1] = item;
               i = (i>1) ? i-2 : i-1;
               continue;
            }
            if (twoAheadItem)
            {
               // If only difference is plurals, then include the next word in change
               if (item[0] == DELETE && oneAheadItem[0] == INSERT && twoAheadItem[0] == NORMAL)
               {
                  var itemLastWord = XString(item[1]).getLastWord();
                  var oneAheadItemLastWord = XString(oneAheadItem[1]).getLastWord();
                  if (isEqual((itemLastWord + "s"), oneAheadItemLastWord) || isEqual(itemLastWord, (oneAheadItemLastWord + "s")))
                  {
                     items[i] = null;
                     items[i+1] = [DELETE, item[1] + " " + twoAheadItem[1], false];
                     items[i+2] = [INSERT, oneAheadItem[1] + " " + twoAheadItem[1], false];
                     i = (i>1) ? i-2 : i-1;
                  }
               }
               // Merge an insert, normal single word, and another insert into a delete and an insert
               if (item[0] == INSERT && oneAheadItem[0] == NORMAL && twoAheadItem[0] == INSERT)
               {
                  if (!(/</).test(oneAheadItem[1])) // Don't merge through tags
                  {
                     if (!(/\s+/).test(oneAheadItem[1])) // Only a single word can be in the item
                     {
                        items[i] = null;
                        items[i+1] = [DELETE, oneAheadItem[1], false];
                        items[i+2] = [INSERT, item[1] + " " + oneAheadItem[1] + " " + twoAheadItem[1], false];
                        i = (i>1) ? i-2 : i-1;
                        continue;
                     }
                  }
               }
               // Merge an insert, normal and/or, delete and then an insert into a delete and an insert
               //if (threeAheadItem)
               //{
               //   if (item[0] == INSERT && oneAheadItem[0] == NORMAL && twoAheadItem[0] == DELETE && threeAheadItem[0] == INSERT)
               //   {
               //      if (!(/</).test(oneAheadItem[1])) // Don't merge through tags
               //      {
               //         if (!(/\s+/).test(oneAheadItem[1])  && !((/^and$/i).test(oneAheadItem[1]) || (/^or$/i).test(oneAheadItem[1]))) // Only a single word can be in the item
               //         {
               //            items[i] = null;
               //            items[i+1] = null;
               //            items[i+2] = [DELETE, oneAheadItem[1] + " " + twoAheadItem[1], false];
               //            items[i+3] = [INSERT, item[1] + " " + oneAheadItem[1] + " " + " " + threeAheadItem[1], false];
               //            i = (i>1) ? i-2 : i-1;
               //            continue;
               //         }
               //      }
               //   }
               //}
            }
         }
      }
      items = XArray(items).compress();

      // Group adjacent identical changes
      for (var i=0; i<items.length; i++)
      {
         var item = getItem(items[i]);
         if (!item)
            continue;
         var oneAheadItem = (i<(items.length-1)) ? getItem(items[i+1]) : null;
         if (oneAheadItem)
         {
            if (isEqual(item[0], oneAheadItem[0]))
            {
               items[i] = null;
               items[i+1] = [item[0], item[1] + " " + oneAheadItem[1], false];
               continue;
            }
         }
      }
      return XArray(items).compress();
   }

   //--------------------------------------------------------------------------

   function buildDiffedString(
      diffedItems
   )
   {

      var diffedString = "";

      var nextSpacer = "";
      for (var i=0; i<diffedItems.length; i++)
      {
         var diffedItem = buildItemString(diffedItems[i]);
         diffedString += nextSpacer + diffedItem;
         if (diffedItem.length == 0)
            nextSpacer = "";
         else
         {
            if ((/^\s*\<[^\>]*\>\s*$/).test(diffedItem))
            {
               var nextItem = ((i+1)<diffedItems.length) ? buildItemString(diffedItems[i+1]) : null;
               if ((/\/\>\s*$/).test(diffedItem) || (/\<\/[^\>]+\>\s*$/).test(diffedItem))
               {
                  if (nextItem != null && !(/^\s*\</).test(nextItem))
                     nextSpacer = " ";
                  else
                     nextSpace = "";
               }
               else if ((/\?\>\s*$/).test(diffedItem) && (nextItem != null && (/^\s*\<\?/).test(nextItem)))
                  nextSpacer = " ";
               else if ((/\?\>\s*$/).test(diffedItem) && (nextItem != null && (/[^<\s]/).test(nextItem)))
                  nextSpacer = " ";
               else
                  nextSpacer = "";
            }
            else
               nextSpacer = " ";
         }
      }

      return diffedString;
   }

   //--------------------------------------------------------------------------

   var origString = this.valueOf();

   var pendingInsertions = [];
   var pendingDeletions = [];

   // Break the strings into their component parts
   var origItems = makeTokens(origString);
   var newItems = makeTokens(newString);

   // Compute the differences
   var numOrigItems = origItems.length;
   var numNewItems = newItems.length;
   var diffedItems = [];
   if (numOrigItems == 0 && numNewItems == 0)
   {
      // Do nothing
   }
   if (numOrigItems == 0)
   {
      diffedItems.push([INSERT, newItems.join(" "), true]);
   }
   else if (numNewItems == 0)
   {
      diffedItems.push([DELETE, origItems.join(" "), true]);
   }
   else
   {

      var defaultMatchDistance = 1;
      while (newItems.length > 0)
      {
         var newItem = getNextItem(newItems);
         var matchNewItem = (XMatch(newItem, /\s*\<\s*([^\s\\\>]+).*\>\s*$/)) ? "<" + XMatch.matches[1] + ">" : newItem; // Just compare the element name
         var matchFound = false;
         var matchDistance = defaultMatchDistance;
         while (origItems.length > 0)
         {
            var origItem = getNextItem(origItems);
            var matchOrigItem = (XMatch(origItem, /\s*\<\s*([^\s\\\>]+).*\>\s*$/)) ? "<" + XMatch.matches[1] + ">" : origItem; // Just compare the element name
            if (isEqual(matchNewItem, matchOrigItem))
            {
               matchFound = true;
               if (!(/^\s*<[^\?].*?[^\/]>/).test(matchNewItem))
               {
                  for (var i=0; i<matchDistance-1+((pendingInsertions.length>0)? 1 : 0); i++)
                  {
                     if (newItems.length == 0 || origItems.length == 0)
                        break;
                     var testNewItem = newItems[i];
                     var testOrigItem = origItems[i];
                     if (testNewItem != testOrigItem)
                     {
                        matchFound = false;
                        break;
                     }
                  }
               }
               if (matchFound)
               {
                  if (pendingDeletions.length > 0)
                  {
                     diffedItems.push([DELETE, pendingDeletions.join(" "), false]);
                     pendingDeletions = [];
                  }
                  if (pendingInsertions.length > 0)
                  {
                     diffedItems.push([INSERT, pendingInsertions.join(" "), false])
                     pendingInsertions = [];
                  }
                  diffedItems.push([NORMAL, newItem, false]);
                  break;
               }
            }
            matchDistance = (matchDistance >= 3) ? 3 : matchDistance + 1;
            pendingDeletions.push(origItem);
            if (matchNewItem.length == 1 && (new RegExp(matchNewItem.replace(/([\(\)\.\*\?\+\[\]\#\^\$])/g,"\\$1"))).test(".,;!?")) // Grammar must match immediately
               break;
         }
         if (!matchFound)
         {
            origItems = pendingDeletions.concat(origItems);
            pendingDeletions = [];
            pendingInsertions.push(newItem);
         }
      }
      if (origItems.length > 0)
         pendingDeletions = pendingDeletions.concat(origItems);
      if (pendingDeletions.length > 0)
      {
         var deleteLine = (pendingDeletions.length == numOrigItems) ? true : false;
         diffedItems.push([DELETE, pendingDeletions.join(" "), deleteLine]);
         pendingDeletions = [];
      }
      if (pendingInsertions.length > 0)
      {
         var insertLine = (pendingInsertions.length == numNewItems) ? true : false;
         diffedItems.push([INSERT, pendingInsertions.join(" "), insertLine]);
         pendingInsertions = [];
      }
   }

   // Build the combined string
   diffedItems = combineCommas(diffedItems);
   diffedItems = (optimize) ? rearrangeChanges(diffedItems) : diffedItems;
   var diffedString = buildDiffedString(diffedItems, " ");

   // Don't bother if the changes are too extensive
   if (passJudgement)
   {
      if (diffedItems.length == 2 && diffedItems[0][0] == DELETE && diffedItems[1][0] == INSERT)
         return null;

      var numWords = XMatch(diffedString, /\s+/g);
      var numDeletions = XMatch(diffedString, /\|\-\-/g);
      var numInsertions = XMatch(diffedString, /\|\+\+/g);

      if ((numDeletions + numInsertions) / numWords > 0.85)
         return null;
   }

   diffedString = diffedString.replace(/\s+([\,\;])/g, "$1");

   return XString.trim(XString(diffedString).closeSpaces());
}

//=============================================================================
