// Copyright (C) 2007-2010 Bristle Software, Inc.
// 
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 1, or (at your option)
// any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc.

/******************************************************************************
* com.bristle.jslib.Util.js
*******************************************************************************
* Purpose:
*       This file contains utility routines that are reusable from any
*       JavaScript or HTML file.
* Usage:
*       - The typical scenario for using this file from an HTML file is:
*         <script language='JavaScript' src='com.bristle.jslib.Util.js'></script>
*         Call the various functions that reside here.
* Assumptions:
* Effects:
*       - None.
* Anticipated Changes:
* Notes:
* Implementation Notes:
* Portability Issues:
* Revision History:
*   $Log$
******************************************************************************/

// Create the "namespace" to hold the functions in this file.
// Note:  Don't redefine "com" or other objects that may be defined in other 
//        JavaScript files.  Such definitions step on each other and cause 
//        errors like:
//              "com.bristle.jslib is null or not an object."
//        on the first call to any function in the hierarchy.  This can be
//        very difficult to diagnose because the message points you to the 
//        file containing the first executed function call, not necessarily 
//        the file containing the error.  For example, creating the namespace
//        wrong for com.abc can step on the com portion of the namespace for 
//        com.xyz, and cause errors on calls to com.xyz.pdq.mno.function1().
if (typeof(com) == "undefined")               { eval("var com = {};"); } 
if (typeof(com.bristle) == "undefined")       { eval("com.bristle = {};"); } 
if (typeof(com.bristle.jslib) == "undefined") { eval("com.bristle.jslib = {};"); } 
com.bristle.jslib.Util = {};


// Disallow accidental creation of new document properties (to catch
// typos).
//?? No.  Bad idea because Google Analytics code relies on adding new
//?? properties to the window object on the fly, so it reports errors
//?? in IE 6, 7, and 8 (but not Firefox or Safari) when this is set false.
//??document.expando = false;

// Note: Need these because IE6, IE8, and perhaps others don't define
//       constants on static objects like Node.ELEMENT_NODE or even constants
//       on instances like xmlNode.ELEMENT_NODE.
com.bristle.jslib.Util.NODE_TYPE_ELEMENT_NODE                 = 1;
com.bristle.jslib.Util.NODE_TYPE_ATTRIBUTE_NODE               = 2;
com.bristle.jslib.Util.NODE_TYPE_TEXT_NODE                    = 3;
com.bristle.jslib.Util.NODE_TYPE_CDATA_SECTION_NODE           = 4;
com.bristle.jslib.Util.NODE_TYPE_ENTITY_REFERENCE_NODE        = 5;
com.bristle.jslib.Util.NODE_TYPE_ENTITY_NODE                  = 6;
com.bristle.jslib.Util.NODE_TYPE_PROCESSING_INSTRUCTION_NODE  = 7;
com.bristle.jslib.Util.NODE_TYPE_COMMENT_NODE                 = 8;
com.bristle.jslib.Util.NODE_TYPE_DOCUMENT_NODE                = 9;
com.bristle.jslib.Util.NODE_TYPE_DOCUMENT_TYPE_NODE           = 10;
com.bristle.jslib.Util.NODE_TYPE_DOCUMENT_FRAGMENT_NODE       = 11;
com.bristle.jslib.Util.NODE_TYPE_NOTATION_NODE                = 12;

/******************************************************************************
* Return true if the parameter is missing, null or undefined.
******************************************************************************/
com.bristle.jslib.Util.isMissingNullOrUndefined =
function(objIn)
{
    return ((objIn == null) 
            ? true
            : ((typeof(objIn) == "undefined")
               ? true
               : false
              )
           );
}

/******************************************************************************
* Return true if the parameter is missing, null, undefined, or an empty string.
******************************************************************************/
com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString =
function(objIn)
{
    return (com.bristle.jslib.Util.isMissingNullOrUndefined(objIn) 
            ? true
            : ((objIn == "")
               ? true
               : false
              )
           );
}

/******************************************************************************
* Return true if the parameter is missing, null, undefined, or negative.
******************************************************************************/
com.bristle.jslib.Util.isMissingNullUndefinedOrNegative =
function(objIn)
{
    return (com.bristle.jslib.Util.isMissingNullOrUndefined(objIn) 
            ? true
            : ((objIn < 0)
               ? true
               : false
              )
           );
}

/******************************************************************************
* Return a "corrected" value of the specified index into the specified string.
* The corrected value is ordinarily the same as the specified index.  However, 
* it is corrected to be zero if the string is undefined, null or empty, and 
* corrected to be the length of the string if the specified value is 
* undefined, null, negative, or greater than the length of the string.
******************************************************************************/
com.bristle.jslib.Util.getCorrectedIndex =
function(strIn, intIndex)
{
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(strIn))
    {
        return 0;
    }
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrNegative(intIndex)
        || intIndex > strIn.length
       )
    {
        return strIn.length;
    }
    return intIndex;
}

/******************************************************************************
* Wrap the specified string of HTML in a displayable border for
* showing to the user.
******************************************************************************/
com.bristle.jslib.Util.formatWithBorder = 
function(strHTML)
{
    return "\n <table border='1' align='center'>"
         + "\n  <tr>"
         + "\n   <td>"
         + "\n    <table border='1' cellspacing='8' cellpadding='8'>"
         + "\n     <tr>"
         + "\n      <td>"
         + "\n       " + strHTML
         + "\n      </td>"
         + "\n     </tr>"
         + "\n    </table>"
         + "\n   </td>"
         + "\n  </tr>"
         + "\n </table>"
         ;
}

/******************************************************************************
* Format the fields of the specified Error object as an HTML string
* for reporting to the user.
******************************************************************************/
com.bristle.jslib.Util.formatScriptError = 
function(objError)
{
    var strErrorDetails = "";
    if (com.bristle.jslib.Util.isMissingNullOrUndefined(objError))
    {
        // No details to add to the message.
    }
    else
    {
        // Interrogate the expected properties of the Error object, adding
        // any error details that exist to the message.
        // Note: If it really is a JavaScript Error object, this works fine.
        //       If it is another object type that has the right properties, 
        //       also fine.
        // Note: Different browsers have different properties of the Error
        //       object.  Show all that are defined.
        if (typeof (objError.name) != "undefined")
        {
            strErrorDetails += "Error Name:        " + objError.name + "\n";
        }
        if (typeof (objError.number) != "undefined")
        {
            // Note:  The number field in IE contains a facility code in the 
            //        upper bits and an error number in the lower bits.
            var intErrorNumber  = objError.number;
            var intFacilityCode = intErrorNumber >>> 16;
            var intErrorCode    = intErrorNumber & 0xFFFF;
            strErrorDetails += "Error Number:      " + intErrorNumber  + "\n";
            strErrorDetails += "Error Facility:    " + intFacilityCode + "\n";
            strErrorDetails += "Error Code:        " + intErrorCode  + "\n";
        }
        if (typeof (objError.message) != "undefined")
        {
            strErrorDetails += "Error Message:     " + objError.message + "\n";
        }
        if (typeof (objError.description) != "undefined")
        {
            strErrorDetails += "Error Description: " + objError.description + "\n";
        }
        if (typeof (objError.source) != "undefined")
        {
            strErrorDetails += "Error Source:      " + objError.source + "\n";
        }
        if (typeof (objError.fileName) != "undefined")
        {
            strErrorDetails += "Error File Name:   " + objError.fileName + "\n";
        }
        if (typeof (objError.lineNumber) != "undefined")
        {
            strErrorDetails += "Error Line Number: " + objError.lineNumber + "\n";
        }
        if (typeof (objError.stack) != "undefined")
        {
            strErrorDetails += "Error Stack Trace: " + objError.stack + "\n";
        }

        // No known Error properties.  Try reporting the object itself as a
        // string.  Maybe it will show something useful.
        if (strErrorDetails == "")
        {
            strErrorDetails = objError;
        }
    }

    // Note:  Must use <xmp> in case any of the objError properties contain 
    //        anything that looks like an XML/HTML tag.  Even <pre> does not 
    //        escape the tags like <xmp> does.  Otherwise, the tag is stripped 
    //        out of the text when displayed as HTML.
    return "<xmp>" + strErrorDetails + "</xmp>";
}

/******************************************************************************
* Scroll the specified object into view, within its enclosing object.
******************************************************************************/
com.bristle.jslib.Util.scrollIntoView = 
function(obj)
{
    try
    {
         obj.scrollIntoView();
    }
    catch(exception)
    {
        // Do nothing.  This is almost always a non-critical cosmetic call.
        // Skip it, suppressing exceptions, if not available.
    }
}

/******************************************************************************
* Set the cursor shape for the specified object, returning the previous 
* cursor shape.
******************************************************************************/
com.bristle.jslib.Util.strCURSOR_SHAPE_AUTO      = "auto";
com.bristle.jslib.Util.strCURSOR_SHAPE_HAND      = "hand";
com.bristle.jslib.Util.strCURSOR_SHAPE_HOURGLASS = "wait";
com.bristle.jslib.Util.setCursorShape =
function(obj, strShape)
{
    strOldShape = obj.style.cursor;
    obj.style.cursor = strShape;
    return strOldShape;
}

/******************************************************************************
* Set the hourglass cursor on or off for the specified object, returning the 
* previous cursor shape.
******************************************************************************/
com.bristle.jslib.Util.setHourglassCursor =
function(obj, blnHourglass)
{
    return com.bristle.jslib.Util.setCursorShape
            (obj, 
             blnHourglass 
             ? com.bristle.jslib.Util.strCURSOR_SHAPE_HOURGLASS 
             : com.bristle.jslib.Util.strCURSOR_SHAPE_AUTO
            );
}

/******************************************************************************
* Get the current style of the specified object.
*
* Note:  Can't just use the style property.  That is just the inline style 
*        specified in the HTML.  This is much more accurate, including 
*        default values, values set by linked or cascaded stylesheets, and
*        values set dynamically via DHTML.
* Note:  Can't just use the currentStyle property.  It is IE-specific.
******************************************************************************/
com.bristle.jslib.Util.getStyle =
function(obj)
{
    return ((typeof(obj.currentStyle) != "undefined")
            ? obj.currentStyle
            : ((typeof(window.getComputedStyle) != "undefined")
               ? window.getComputedStyle(obj,"")
               : obj.style
              )
           );
}

/******************************************************************************
* Get the visibility of the specified object.
******************************************************************************/
com.bristle.jslib.Util.isVisible =
function(obj)
{
    return !(com.bristle.jslib.Util.getStyle(obj).display == "none");
}

/******************************************************************************
* Set the visibility of the specified object.
******************************************************************************/
com.bristle.jslib.Util.setVisible =
function(obj, blnValue)
{
    // Note:  Set display property to "none" instead of setting visibility 
    //        property to "hidden".  Otherwise the display space consumed 
    //        by the element does not get reused.  The page does not reflow.
    //        The element is invisible but still consumes page space, per
    //        Danny Goodman O'Reilly Dynamic HTML book.
    // Note:  Set display property to "none" instead of setting visibility 
    //        property to "collapse", which is still not recognized by 
    //        Internet Explorer for Windows version 6.0, per Danny Goodman 
    //        O'Reilly Dynamic HTML book.
    obj.style.display = blnValue ? "inline" : "none";
}

/******************************************************************************
* Get the boldness of the specified object.
******************************************************************************/
com.bristle.jslib.Util.isBold =
function(obj)
{
    return (com.bristle.jslib.Util.getStyle(obj).fontWeight == "bold");
}

/******************************************************************************
* Set the boldness of the specified object.
******************************************************************************/
com.bristle.jslib.Util.setBold =
function(obj, blnValue)
{
    obj.style.fontWeight = blnValue ? "bold" : "normal";
}

/******************************************************************************
* Get the disabled property of the specified object.
******************************************************************************/
com.bristle.jslib.Util.isDisabled =
function(obj)
{
    return obj.disabled;
}

/******************************************************************************
* Set the disabled attribute of the HTML control with the specified 
* name, returning true if the control exists and the attribute can be 
* set; false otherwise.
******************************************************************************/
com.bristle.jslib.Util.setDisabled =
function(strControlName, blnValue)
{
    try
    {
        document.getElementById(strControlName).disabled = blnValue;
    }
    catch(exception)
    {
        return false;
    }
    return true;
}

/******************************************************************************
* Get the "fake disabled" property of the specified HTML control.
******************************************************************************/
com.bristle.jslib.Util.isFakeDisabled =
function(ctl)
{
    return ("-2" == ctl.tabIndex);
}

/******************************************************************************
* Set the "fake disabled" property of the specified HTML control, so that 
* it looks and acts disabled, but still supports a title popup that can be
* used to tell the user why it is disabled.
*
* Note: This doesn't actually prevent the events of the control from firing.
*       To complete the disabled effect, all event procedures of the control 
*       should call com.bristle.jslib.Util.isFakeDisabled() to decide whether 
*       to return without doing anything.
*
*@param ctl             The control to enable or disable
*@param blnDisabled     True if disabled; false otherwise.
*@param strClassName    The CSS class name to assign to the control to change
*                       its appearance to enabled or disabled.
*                       Optional.  Can be null or omitted, in which case the
*                       CSS class is left unchanged.  The empty string is a 
*                       valid value which can be used to clear the class name.
*@param strTitle        The value to set as the "title" property (hover popup)
*                       of the control, typically used to explain its purpose 
*                       or why it is disabled.
*                       Optional.  Can be null or omitted, in which case the
*                       title property is left unchanged.  The empty string 
*                       is a valid value which can be used to clear the title.
******************************************************************************/
com.bristle.jslib.Util.setFakeDisabled =
function(ctl, blnDisabled, strClassName, strTitle)
{
    // Note:  In most browsers, any negative number removes the control from 
    //        the tab sequence, and zero restores its default position in the 
    //        sequence.  However, in some older browsers, the default is -1
    //        instead of zero.  Therefore, use -2 as the special value, not -1.
    ctl.tabIndex  = (blnDisabled ? "-2" : "0");

    if (!com.bristle.jslib.Util.isMissingNullOrUndefined(strClassName))
    {
        ctl.className = strClassName;
    }

    if (!com.bristle.jslib.Util.isMissingNullOrUndefined(strTitle))
    {
        ctl.title = strTitle;
    }
}

/******************************************************************************
* Get the color of the specified object.
******************************************************************************/
com.bristle.jslib.Util.getColor =
function(obj)
{
    return com.bristle.jslib.Util.getStyle(obj).color;
}

/******************************************************************************
* Set the color of the specified object.
******************************************************************************/
com.bristle.jslib.Util.setColor =
function(obj, strValue)
{
    obj.style.color = strValue;
}

/******************************************************************************
* Get the background color of the specified object.
******************************************************************************/
com.bristle.jslib.Util.getBackgroundColor =
function(obj)
{
    return com.bristle.jslib.Util.getStyle(obj).backgroundColor;
}

/******************************************************************************
* Set the background color of the specified object.
******************************************************************************/
com.bristle.jslib.Util.setBackgroundColor =
function(obj, strValue)
{
    obj.style.backgroundColor = strValue;
}

/******************************************************************************
* Get the text content (without any HTML markup) of the specified object.
*
*@return        The text string
*@throws com.bristle.jslib.Exception.intEXC_UNSUPPORTED_BROWSER
******************************************************************************/
com.bristle.jslib.Util.getTextContent =
function(obj)
{
    if (typeof(obj.textContent) != "undefined")
    {
        return obj.textContent;
    }
    else if (typeof(obj.innerText) != "undefined")
    {
        return obj.innerText;
    }
    else
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_UNSUPPORTED_BROWSER
            ,"Unable to get the text content of the specified object"
            ,"com.bristle.jslib.Util.getTextContent"
            );
    }
}

/******************************************************************************
* Set the text content (without any HTML markup) of the specified object.
*
*@throws com.bristle.jslib.Exception.intEXC_UNSUPPORTED_BROWSER
******************************************************************************/
com.bristle.jslib.Util.setTextContent =
function(obj, strValue)
{
    if (typeof(obj.textContent) != "undefined")
    {
        obj.textContent = strValue;
    }
    else if (typeof(obj.innerText) != "undefined")
    {
        obj.innerText = strValue;
    }
    else
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_UNSUPPORTED_BROWSER
            ,"Unable to set the text content of the specified object"
            ,"com.bristle.jslib.Util.setTextContent"
            );
    }
}

/******************************************************************************
* Return true if the specified node is an HTML element, with a tagName 
* property that matches the optionally specified tag name.
*
*@param xmlNode    The node
*@param strTagName The HTML tag name to search for, or null.
*                  Default: null
*                  Examples: "TR", "LI"
*@return False if the node is not an element, or if the tag name was specified
*        and does not match.  True if the node is an element, and the tag name
*        was null or was a match.
*@throws com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
*                  When the specified object is not a DOM node
******************************************************************************/
com.bristle.jslib.Util.isAnHTMLElement =
function(xmlNode, strTagName)
{
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(xmlNode)
        || typeof(xmlNode.nextSibling) == "undefined"
       )
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
            ,"The specified object is not a DOM node"
            ,"com.bristle.jslib.Util.isAnHTMLElement"
            );
    }

    var blnTagNameSpecified = 
        !com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(strTagName);
    if (blnTagNameSpecified) strTagName = strTagName.toUpperCase();

    return (xmlNode.nodeType == com.bristle.jslib.Util.NODE_TYPE_ELEMENT_NODE
            && 
            (!blnTagNameSpecified || xmlNode.tagName == strTagName)
           );
}

/******************************************************************************
* Get the next HTML element that is a sibling of the specified DOM node, 
* skipping past elements that do not have the optionally specified HTML 
* tag name.
* Different from node.nextSibling which may return DOM nodes that are not 
* HTML elements (for example, text nodes), and which doesn't consider tag name.
*
*@param xmlNode    The node to start with
*@param strTagName The HTML tag name to search for, or null.
*                  Default: null
*                  Examples: "TR", "LI"
*@return The sibling HTML element or null
*@throws com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
*                  When the specified object is not a DOM node
******************************************************************************/
com.bristle.jslib.Util.getNextSiblingElement =
function(xmlNode, strTagName)
{
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(xmlNode)
        || typeof(xmlNode.nextSibling) == "undefined"
       )
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
            ,"The specified object is not a DOM node"
            ,"com.bristle.jslib.Util.getNextSiblingElement"
            );
    }

    var blnTagNameSpecified = 
        !com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(strTagName);
    if (blnTagNameSpecified) strTagName = strTagName.toUpperCase();

    do 
    {
        xmlNode = xmlNode.nextSibling;
    } 
    while (   xmlNode != null 
           && !com.bristle.jslib.Util.isAnHTMLElement(xmlNode, strTagName)
          )

    return xmlNode;
}

/******************************************************************************
* Get the previous HTML element that is a sibling of the specified DOM node,
* skipping past elements that do not have the optionally specified HTML 
* tag name.
* Different from node.previousSibling which may return DOM nodes that are not 
* HTML elements (for example, text nodes), and which doesn't consider tag name.
*
*@param xmlNode    The node to start with
*@param strTagName The HTML tag name to search for, or null.
*                  Default: null
*                  Examples: "TR", "LI"
*@return The sibling HTML element or null
*@throws com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
*                  When the specified object is not a DOM node
******************************************************************************/
com.bristle.jslib.Util.getPreviousSiblingElement =
function(xmlNode, strTagName)
{
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(xmlNode)
        || typeof(xmlNode.previousSibling) == "undefined"
       )
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
            ,"The specified object is not a DOM node"
            ,"com.bristle.jslib.Util.getPreviousSiblingElement"
            );
    }

    var blnTagNameSpecified = 
        !com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(strTagName);
    if (blnTagNameSpecified) strTagName = strTagName.toUpperCase();

    do 
    {
        xmlNode = xmlNode.previousSibling;
    } 
    while (   xmlNode != null 
           && !com.bristle.jslib.Util.isAnHTMLElement(xmlNode, strTagName)
          )

    return xmlNode;
}

/******************************************************************************
* Get the first HTML element that is a child of the specified DOM node,
* skipping past elements that do not have the optionally specified HTML 
* tag name.
* Different from node.firstChild which may return DOM nodes that are not 
* HTML elements (for example, text nodes), and which doesn't consider tag name.
*
*@param xmlNode    The parent node
*@param strTagName The HTML tag name to search for, or null.
*                  Default: null
*                  Examples: "TR", "LI"
*@return The child HTML element or null
*@throws com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
*                  When the specified object is not a DOM node
******************************************************************************/
com.bristle.jslib.Util.getFirstChildElement =
function(xmlNode, strTagName)
{
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(xmlNode)
        || typeof(xmlNode.firstChild) == "undefined"
       )
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
            ,"The specified object is not a DOM node"
            ,"com.bristle.jslib.Util.getFirstChildElement"
            );
    }

    var blnTagNameSpecified = 
        !com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(strTagName);
    if (blnTagNameSpecified) strTagName = strTagName.toUpperCase();

    var xmlChild = xmlNode.firstChild;
    if (   xmlChild != null 
        && !com.bristle.jslib.Util.isAnHTMLElement(xmlChild, strTagName)
       )
    {
        xmlChild = com.bristle.jslib.Util.getNextSiblingElement
                                                  (xmlChild, strTagName);
    }
    return xmlChild;
}

/******************************************************************************
* Get the last HTML element that is a child of the specified DOM node,
* skipping past elements that do not have the optionally specified HTML 
* tag name.
* Different from node.lastChild which may return DOM nodes that are not 
* HTML elements (for example, text nodes), and which doesn't consider tag name.
*
*@param xmlNode    The parent node
*@param strTagName The HTML tag name to search for, or null.
*                  Default: null
*                  Examples: "TR", "LI"
*@return The child HTML element or null
*@throws com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
*                  When the specified object is not a DOM node
******************************************************************************/
com.bristle.jslib.Util.getLastChildElement =
function(xmlNode, strTagName)
{
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(xmlNode)
        || typeof(xmlNode.lastChild) == "undefined"
       )
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
            ,"The specified object is not a DOM node"
            ,"com.bristle.jslib.Util.getLastChildElement"
            );
    }

    var blnTagNameSpecified = 
        !com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(strTagName);
    if (blnTagNameSpecified) strTagName = strTagName.toUpperCase();

    var xmlChild = xmlNode.lastChild;
    if (   xmlChild != null 
        && !com.bristle.jslib.Util.isAnHTMLElement(xmlChild, strTagName)
       )
    {
        xmlChild = com.bristle.jslib.Util.getPreviousSiblingElement
                                                  (xmlChild, strTagName);
    }
    return xmlChild;
}

/******************************************************************************
* Get the HTML element that is the nearest ancestor of the specified DOM node,
* skipping past elements that do not have the optionally specified HTML 
* tag name.
* Different from Range.commonAncestorContainer, Range.startContainer, and
* Range.endContainer, which may return DOM nodes that are not HTML elements
* (for example, text nodes), and which doesn't consider tag name.
* Same as the native parentElement property supported by some browsers,
* except for the additional tag name feature.
*
*@param xmlNode    The node to start with
*@param strTagName The HTML tag name to search for, or null.
*                  Default: null
*                  Examples: "TR", "LI"
*@return The ancestor HTML element or null
*@throws com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
*                  When the specified object is not a DOM node
******************************************************************************/
com.bristle.jslib.Util.getParentElement =
function(xmlNode, strTagName)
{
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(xmlNode)
        || typeof(xmlNode.parentNode) == "undefined"
       )
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
            ,"The specified object is not a DOM node"
            ,"com.bristle.jslib.Util.getParentElement"
            );
    }

    var blnTagNameSpecified = 
        !com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(strTagName);
    if (blnTagNameSpecified) strTagName = strTagName.toUpperCase();

    do 
    {
        xmlNode = xmlNode.parentNode;
    } 
    while (   xmlNode != null 
           && !com.bristle.jslib.Util.isAnHTMLElement(xmlNode, strTagName)
          )

    return xmlNode;
}

/******************************************************************************
* Get the HTML element that is the nearest ancestor of the specified DOM node,
* or the specified node itself if it is an HTML element.  In both cases, skip
* past elements that do not have the optionally specified HTML tag name.
*
*@param xmlNode    The node to start with
*@param strTagName The HTML tag name to search for, or null.
*                  Default: null
*                  Examples: "TR", "LI"
*@return The ancestor or self HTML element or null
*@throws com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
*                  When the specified object is not a DOM node
*@see com.bristle.jslib.Util.getParentElement
******************************************************************************/
com.bristle.jslib.Util.getParentElementOrSelfElement =
function(xmlNode, strTagName)
{
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(xmlNode)
        || typeof(xmlNode.parentNode) == "undefined"
       )
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
            ,"The specified object is not a DOM node"
            ,"com.bristle.jslib.Util.getParentElementOrSelfElement"
            );
    }

    var blnTagNameSpecified = 
        !com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(strTagName);
    if (blnTagNameSpecified) strTagName = strTagName.toUpperCase();

    return (com.bristle.jslib.Util.isAnHTMLElement(xmlNode, strTagName)
            ? xmlNode
            : com.bristle.jslib.Util.getParentElement(xmlNode, strTagName)
           );
}

/******************************************************************************
* Returns true if the specified DOM node or an ancester is an HTML element 
* with the specified HTML tag name.
*
*@param xmlNode    The node to start with
*@param strTagName The HTML tag name to search for.
*                  Examples: "TR", "LI"
*@return True if the node or an ancester has the HTML tag name; 
*        false otherwise.
*@throws com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
*                  When the specified object is not a DOM node
*@see com.bristle.jslib.Util.getParentElementOrSelfElement
******************************************************************************/
com.bristle.jslib.Util.isOrIsInsideHTMLElement =
function(xmlNode, strTagName)
{
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(xmlNode)
        || typeof(xmlNode.parentNode) == "undefined"
       )
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
            ,"The specified object is not a DOM node"
            ,"com.bristle.jslib.Util.isOrIsInsideHTMLElement"
            );
    }

    return (com.bristle.jslib.Util.getParentElementOrSelfElement
                    (xmlNode, strTagName)
            != null);
}

/******************************************************************************
* Return true if the specified ancestor node is an ancestor of the specified 
* node.
*
*@param xmlNode         The node
*@param xmlAncestorNode The ancestor node
*@return True if xmlAncestorNode is an ancestor of xmlNode; false otherwise.
******************************************************************************/
com.bristle.jslib.Util.isAncestorNode =
function(xmlNode, xmlAncestorNode)
{
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(xmlNode)
        || typeof(xmlNode.parentNode) == "undefined"
       )
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
            ,"The specified descendant is not a DOM node"
            ,"com.bristle.jslib.Util.isAncestorNode"
            );
    }
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(xmlNode)
        || typeof(xmlAncestorNode.parentNode) == "undefined"
       )
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_DOM_NODE
            ,"The specified ancestor is not a DOM node"
            ,"com.bristle.jslib.Util.isAncestorNode"
            );
    }

    while (xmlNode != null)
    {
        if (xmlNode.parentNode == xmlAncestorNode) return true;
        xmlNode = xmlNode.parentNode;
    } 
    return false;
}

/******************************************************************************
* Returns the specified percent of the specified amount.
*
*@param fltPercent  Percent (Example: 50 for 50%)
*@param fltAmount   Amount
******************************************************************************/
com.bristle.jslib.Util.getPercentOf =
function(fltPercent, fltAmount)
{
    // Note: Use parseFloat() to force numeric, not string values.
    fltPercent = parseFloat(fltPercent);
    fltAmount  = parseFloat(fltAmount);
    return (fltPercent/100) * fltAmount;
}

/******************************************************************************
* Add the specified percent of the specified amount to the other specified 
* amount, returning the result.
*
*@param fltAddTo     Amount to be added to
*@param fltPercent   Percent to add
*@param fltPercentOf Amount to add percent of
******************************************************************************/
com.bristle.jslib.Util.addPercentOf =
function(fltAddTo, fltPercent, fltPercentOf)
{
    // Note: Use parseFloat() to force numeric, not string values.
    fltAddTo = parseFloat(fltAddTo);
    return fltAddTo 
           + com.bristle.jslib.Util.getPercentOf(fltPercent, fltPercentOf);
}

/******************************************************************************
* Add the specified percent of the specified amount to the same amount,
* returning the result.
*
*@param fltAmount   Amount to add percent of itself to
*@param fltPercent  Percent to add
******************************************************************************/
com.bristle.jslib.Util.addPercentOfSelf =
function(fltAmount, fltPercent)
{
    return com.bristle.jslib.Util.addPercentOf(fltAmount, fltPercent, fltAmount);
}

/******************************************************************************
* Round the specified number to the specified number of decimal digits (digits
* following the decimal point) and then trim any trailing zeros, returning a 
* string containing the specified value with perhaps less precision.
*
*@param fltVal                  Number to trim
*@param intMaxDecimalDigits     Max number of non-zero decimal digits to 
*                               preserve.
******************************************************************************/
com.bristle.jslib.Util.trimNumber =
function(fltVal, intMaxDecimalDigits)
{
    // Force to exactly max number of decimal digits, rounding the value.
    // Note: Can't just do:  fltVal.toFixed() because fltVal may be coming 
    //       in as a string or other non-number.  Convert it first to avoid
    //       error:  Object doesn't support this property or method
    //       in IE 6.0.2800.1106
    // Note: toFixed() exists only in JavaScript 1.5+ (IE5.5+, NS6+)
    var strVal = new Number(fltVal).toFixed(intMaxDecimalDigits);

    // Trim trailing zeros from decimal (fractional) part.
    // Note: Must use:
    //          0 == strVal - strLessPrecise
    //       instead of:
    //          strVal == strLessPrecise
    //       because the 2nd form is a string comparison which returns false 
    //       for numerically equal values with different precisions, like 
    //       1.1 and 1.10.
    var strLessPrecise = strVal;
    while (intMaxDecimalDigits >= 0 && (0 == strVal - strLessPrecise))
    {
        // Accept the computed value and compute a possible new value.
        strVal = strLessPrecise;
        intMaxDecimalDigits--;
        if (intMaxDecimalDigits >= 0)
        {
            strLessPrecise 
                = new Number(strVal).toFixed(intMaxDecimalDigits);
        }
    }
    return strVal;
}

/******************************************************************************
* Generate a random integer between the specified min and max.
*
*@param intMin  Min acceptable random integer (inclusive).  Default: 0
*@param intMax  Max acceptable random integer (inclusive).  Default: 2^16-1
*@return        The random number
******************************************************************************/
com.bristle.jslib.Util.randomInt =
function(intMin, intMax)
{
    if (com.bristle.jslib.Util.isMissingNullOrUndefined(intMin))
    {
        intMin = 0;
    }
    if (com.bristle.jslib.Util.isMissingNullOrUndefined(intMax))
    {
        intMax = Math.pow(2, 16) - 1;
    }

                                                // Example with min 6 and max 10:
    var intRangeSize = intMax - intMin + 1;     // Range size = 5
    var intRandom = Math.random();              // Between 0 and 0.999...
    intRandom *= intRangeSize;                  // Between 0 and 4.999...
    intRandom = Math.floor(intRandom);          // Between 0 and 4
    intRandom += intMin;                        // Between 6 and 10
    return intRandom;
}

/******************************************************************************
* Generate a random float between the specified min and max.
*
*@param fltMin  Min acceptable random float (inclusive)
*@param fltMax  Max acceptable random float (exclusive)
*@return        The random number
******************************************************************************/
com.bristle.jslib.Util.randomFloat =
function(fltMin, fltMax)
{
                                                // Example with min 6 and max 10:
    var fltRangeSize = fltMax - fltMin;         // Range size = 4
    var fltRandom = Math.random();              // Between 0 and 0.999...
    fltRandom *= fltRangeSize;                  // Between 0 and 3.999...
    fltRandom += fltMin;                        // Between 6 and 9.999...
    return fltRandom;
}

/******************************************************************************
* Generate a unique variable name in the namespace of this Javascript library.
* For example:
*       com.bristle.jslib.Util.temp56948
*
*@return        The name.
*@throws com.bristle.jslib.Exception.intEXC_UNABLE_TO_GENERATE_UNIQUE_VAR_NAME
******************************************************************************/
com.bristle.jslib.Util.generateUniqueVarName =
function()
{
    // Loop, generating names until one is found that is not already defined,
    // giving up after generating 50 that were all already defined.
    var strName;
    var intMaxCount = 50;
    var intCount = intMaxCount;
    for (; intCount > 0; intCount--)
    {
        strName = "com.bristle.jslib.Util.temp" 
                + com.bristle.jslib.Util.randomInt();
        if (typeof(eval(strName)) == "undefined")
        {
            break;
        }
    }
    if (intCount == 0)
    {
        throw new com.bristle.jslib.Exception.Exception
        (com.bristle.jslib.Exception.intEXC_UNABLE_TO_GENERATE_UNIQUE_VAR_NAME,
        "Unable to generate unique variable name after " 
         + intMaxCount + " tries.",
         "com.bristle.jslib.Util.generateUniqueVarName"
         );
    }
    else
    {
        return strName;
    }
}

/******************************************************************************
* Set the caption of the specified button.
******************************************************************************/
com.bristle.jslib.Util.setButtonCaption =
function(cmd, strValue)
{
    cmd.innerHTML = strValue;
}

/******************************************************************************
* Return an empty string if the specified string is missing, null, or 
* undefined; otherwise return the specified string.
******************************************************************************/
com.bristle.jslib.Util.mapNullToEmptyString =
function(strIn)
{
    return com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(strIn)
           ? "" 
           : strIn;
}

/******************************************************************************
* Compress all whitespace, replacing each sequence of whitespace chars 
* (blanks, tabs, newlines, etc.) with a single blank.
******************************************************************************/
com.bristle.jslib.Util.compressWhitespace =
function(strIn)
{
    return strIn.replace(/\s+/gm, " ");
}

/******************************************************************************
* Trim leading spaces from the specified string.
******************************************************************************/
com.bristle.jslib.Util.ltrim =
function(strIn)
{
    //?? Could replace with single line:
    //??    strIn.replace(/^ */g, "")
    var intStart = 0;
    var intEnd   = strIn.length - 1;
    while (strIn.charAt(intStart) == " ") { intStart++; }
    return strIn.substring(intStart, intEnd + 1);
}

/******************************************************************************
* Trim trailing spaces from the specified string.
******************************************************************************/
com.bristle.jslib.Util.rtrim =
function(strIn)
{
    //?? Could replace with single line:
    //??    strIn.replace(/ *$/g, "")
    var intStart = 0;
    var intEnd   = strIn.length - 1;
    while (strIn.charAt(intEnd)   == " ") { intEnd--; }
    return strIn.substring(intStart, intEnd + 1);
}

/******************************************************************************
* Trim leading and trailing spaces from the specified string.
******************************************************************************/
com.bristle.jslib.Util.trim =
function(strIn)
{
    //?? Could replace with single line:
    //??    strIn.replace(/(^ *| *$)/g, "")
    return com.bristle.jslib.Util.ltrim(com.bristle.jslib.Util.rtrim(strIn));
}

/******************************************************************************
* Trim all leading whitespace, including newlines, from the specified string.
******************************************************************************/
com.bristle.jslib.Util.ltrimAllWhitespace =
function(strIn)
{
    return strIn.replace(/^[\s\n]*/gm, "");
}

/******************************************************************************
* Trim all trailing whitespace, including newlines, from the specified string.
******************************************************************************/
com.bristle.jslib.Util.rtrimAllWhitespace =
function(strIn)
{
    return strIn.replace(/[\s\n]*$/gm, "");
}

/******************************************************************************
* Trim all leading and trailing whitespace, including newlines, from the 
* specified string.
******************************************************************************/
com.bristle.jslib.Util.trimAllWhitespace =
function(strIn)
{
    return strIn.replace(/(^[\s\n]*|[\s\n]*$)/gm, "");
}

/******************************************************************************
* Pad the specified string to the specified length with leading chars.
******************************************************************************/
com.bristle.jslib.Util.lpad =
function(strIn, intLength, strPadChar)
{
    // Note:  Force strIn to a String.  It might erroneously be a number
    //        or something in which case length is undefined.
    var strRC = strIn.toString();
    while (intLength > strRC.length)
    {
        strRC = strPadChar + strRC;
    }
    return strRC;
}

/******************************************************************************
* Pad the specified string to the specified length with trailing chars.
******************************************************************************/
com.bristle.jslib.Util.rpad =
function(strIn, intLength, strPadChar)
{
    // Note:  Force strIn to a String.  It might erroneously be a number
    //        or something in which case length is undefined.
    var strRC = strIn.toString();
    while (intLength > strRC.length)
    {
        strRC = strRC + strPadChar;
    }
    return strRC;
}

/******************************************************************************
* Return a string containing intLength occurrences of the string strChar.
* If intLength is zero or negative, return the empty string ("");
*@param  strChar    String to repeat in the returned string.
*@param  intLength  Number of times to repeat.
*@return            Generated string.
******************************************************************************/
com.bristle.jslib.Util.makeStringOfChars =
function(strChar, intLength)
{
    var strRC = "";
    for (var i = 0; i < intLength; i++)
    {
        strRC += strChar;
    }
    return strRC;
}

/******************************************************************************
* Return true or false to indicate whether the specified string starts with 
* the specified prefix.
*@param  strIn      String to check.
*@param  strPrefix  Prefix string.
*@return            true or false.
******************************************************************************/
com.bristle.jslib.Util.startsWith =
function(strIn, strPrefix)
{
    if (strPrefix.length > strIn.length) return false;
    return (strIn.slice(0, strPrefix.length) == strPrefix);
}

/******************************************************************************
* Return true or false to indicate whether the specified string ends with 
* the specified suffix.
*@param  strIn      String to check.
*@param  strSuffix  Suffix string.
*@return            true or false.
******************************************************************************/
com.bristle.jslib.Util.endsWith =
function(strIn, strSuffix)
{
    if (strSuffix.length > strIn.length) return false;
    return (strIn.slice(strIn.length - strSuffix.length) == strSuffix);
}

/******************************************************************************
* Replace all occurrences of strFrom with strTo in strIn.
******************************************************************************/
com.bristle.jslib.Util.replaceAll =
function(strIn, strFrom, strTo)
{
    // Keep calling replace() until the string stops changing.
    //?? Bug: This is an infinite loop if strTo contains strFrom.  Each 
    //??      replace puts strFrom in the string, and since strFrom contains
    //??      strTo, that means each replace puts strTo in the string, so it
    //??      finds it there again on the next loop iteration.
    //??      Should re-write using the same logic as the Java version in 
    //??      com.bristle.javalib.util.StrUtil.replaceAll().
    var strInOld = "";
    var strInNew = strIn;
    while (strInOld != strInNew)
    {
        strInOld = strInNew;
        try
        {
            strInNew = strInNew.replace(strFrom, strTo);
        }
        catch(exception)
        {
            // Nothing to do.  If we were passed another data type instead of
            // a string, skip the replace, and just return the original value.
        }
    }
    return strInNew;
}

/******************************************************************************
* Return a string containing all of the default word delimiters (all ASCII 
* chars except letters and digits), except the optionally specified string of
* exception chars.
******************************************************************************/
com.bristle.jslib.Util.strCachedDefaultWordDelimiters = "";
com.bristle.jslib.Util.strCachedExceptChars           = "";
com.bristle.jslib.Util.getDefaultWordDelimiters =
function(strExceptChars)
{
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString
                                                      (strExceptChars))
    {
        strExceptChars = "";
    }

    // Used cached copy, if any.
    // Note: Compute the value only if this function is called, not earlier
    //       at initial load of the JavaScript file.  But, once computed, 
    //       cache the value, and reuse it in subsequent calls.
    if (   com.bristle.jslib.Util.strCachedDefaultWordDelimiters != "" 
        && strExceptChars == com.bristle.jslib.Util.strCachedExceptChars)
    {
        return com.bristle.jslib.Util.strCachedDefaultWordDelimiters;
    }

    // Clear cache and generate new value
    com.bristle.jslib.Util.strCachedDefaultWordDelimiters = "";
    com.bristle.jslib.Util.strCachedExceptChars           = strExceptChars;
    var intCHAR_CODE_DIGIT_0 = "0".charCodeAt(0);
    var intCHAR_CODE_DIGIT_9 = "9".charCodeAt(0);
    var intCHAR_CODE_LOWER_A = "a".charCodeAt(0);
    var intCHAR_CODE_LOWER_Z = "z".charCodeAt(0);
    var intCHAR_CODE_UPPER_A = "A".charCodeAt(0);
    var intCHAR_CODE_UPPER_Z = "Z".charCodeAt(0);
    for (var i = 0; i <= 127; i++)
    {
        var strChar = String.fromCharCode(i);
        if (   (i >= intCHAR_CODE_DIGIT_0 && i <= intCHAR_CODE_DIGIT_9)
            || (i >= intCHAR_CODE_LOWER_A && i <= intCHAR_CODE_LOWER_Z)
            || (i >= intCHAR_CODE_UPPER_A && i <= intCHAR_CODE_UPPER_Z)
            || strExceptChars.indexOf(strChar) >= 0
           )
        {
            // Skip digits, letters, and specified exception chars
        }
        else
        {
            com.bristle.jslib.Util.strCachedDefaultWordDelimiters += strChar;
        }
    }
    return com.bristle.jslib.Util.strCachedDefaultWordDelimiters;
}

/******************************************************************************
* Return the number of chars from the specified index moving left to the next 
* start of a word in the specified string.  Typically used to find the start
* of the word that contains the char at the specified index.
*
*@param strIn       The string to search
*@param intIndex    The zero-based index into the string.
*                   Corrected to length of string if undefined, null, 
*                   negative, or greater than length of string.
*@param strWordDelimiters
*                   A string containing chars to be used as word delimiters.
*                   Defaults to a string containing all ASCII chars except 
*                   letters and digits if undefined or null.
*@return            The number of chars
*                   Returns 0 if:
*                   - The char at the specified index is the start of a word
*                   - The char at the specified index is a word delimiter
*                   - The specified string is empty, undefined, or null
*                   - The specified or corrected index is the length of the
*                     string.
******************************************************************************/
com.bristle.jslib.Util.getLeftOffsetToStartOfWord =
function(strIn, intIndex, strWordDelimiters)
{
    // Check parameters and fill in defaults
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(strIn))
    {
        return 0;
    }
    intIndex = com.bristle.jslib.Util.getCorrectedIndex(strIn, intIndex);
    if (intIndex == strIn.length)
    {
        return 0;
    }
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString
                                                      (strWordDelimiters)) 
    {
        strWordDelimiters = com.bristle.jslib.Util.getDefaultWordDelimiters();
    }

    // Count the non-delimiter chars left of the index.
    var intOffset = 0;
    for (var i = intIndex; i > 0; i--)
    {
        // Stop if we are pointing to a word delimiter.  It is the start of
        // its own word.
        if (strWordDelimiters.indexOf(strIn.charAt(i)) >= 0)
        {
            break;
        }

        // Stop if we are pointing to a word char preceded by a word 
        // delimiter.  It is the start of a word.
        else if (i > 0 && strWordDelimiters.indexOf(strIn.charAt(i-1)) >= 0)
        {
            break;
        }

        intOffset++;
    }
    return intOffset;
}

/******************************************************************************
* Return the number of chars from the specified index moving right to the next 
* end of a word in the specified string.  Typically used to find the end
* of the word that contains the char at the specified index.
*
*@see com.bristle.jslib.Util.getLeftOffsetToStartOfWord()
*@return  The number of chars
*         Returns 0 if:
*         - The specified string is empty, undefined, or null
*         - The specified or corrected index is the length of the string.
*         Returns 1 if:
*         - The char at the specified index is the last char of a word
*         - The char at the specified index is a word delimiter
******************************************************************************/
com.bristle.jslib.Util.getRightOffsetToEndOfWord =
function(strIn, intIndex, strWordDelimiters)
{
    // Check parameters and fill in defaults
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(strIn))
    {
        return 0;
    }
    intIndex = com.bristle.jslib.Util.getCorrectedIndex(strIn, intIndex);
    if (intIndex == strIn.length)
    {
        return 0;
    }
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString
                                                      (strWordDelimiters)) 
    {
        strWordDelimiters = com.bristle.jslib.Util.getDefaultWordDelimiters();
    }

    // Return 1 if we are pointing to a word delimiter.  It is its own 
    // one-char word.
    if (strWordDelimiters.indexOf(strIn.charAt(intIndex)) >= 0)
    {
      return 1;
    }

    // Count the non-delimiter chars at or right of the index.
    var intOffset = 0;
    var strLength = strIn.length;
    for (var i = intIndex; i < strLength; i++)
    {
        if (strWordDelimiters.indexOf(strIn.charAt(i)) >= 0)
        {
            break;
        }
        intOffset++;
    }
    return intOffset;
}

/*****************************************************************************
* Return the index of the first start of a word found by moving left from 
* the specified index in the specified string.  Typically used to find the 
* start of the word that contains the char at the specified index.
*
*@see com.bristle.jslib.Util.getLeftOffsetToStartOfWord()
*@return  The index of the start of the word
*         Returns 0 if the specified string is empty, undefined, or null.
*         Returns the specified index if:
*         - The char at the specified index is the start of a word
*         - The char at the specified index is a word delimiter
*         Returns the string length (one more than the largest valid index) 
*         if the specified or corrected index is the string length.
******************************************************************************/
com.bristle.jslib.Util.getLeftIndexOfStartOfWord =
function(strIn, intIndex, strWordDelimiters)
{
    intIndex = com.bristle.jslib.Util.getCorrectedIndex(strIn, intIndex);
    return intIndex - com.bristle.jslib.Util.getLeftOffsetToStartOfWord
                                       (strIn, intIndex, strWordDelimiters);
}

/*****************************************************************************
* Return the index just past the first end of a word found by moving right 
* from the specified index in the specified string.  Typically used to find 
* the index just past the end of the word that contains the char at the 
* specified index.
*
*@see com.bristle.jslib.Util.getRightOffsetToEndOfWord()
*@return  The index just past the end of the word
*         Returns 0 if the specified string is empty, undefined, or null.
*         Returns the specified index plus 1 if:
*         - The char at the specified index is the last char of a word
*         - The char at the specified index is a word delimiter
*         Returns the string length (one more than the largest valid index) 
*         if the specified or corrected index is the string length.
******************************************************************************/
com.bristle.jslib.Util.getRightIndexOfEndOfWord =
function(strIn, intIndex, strWordDelimiters)
{
    intIndex = com.bristle.jslib.Util.getCorrectedIndex(strIn, intIndex);
    return intIndex + com.bristle.jslib.Util.getRightOffsetToEndOfWord
                                       (strIn, intIndex, strWordDelimiters);
}

/*****************************************************************************
* Return the word that contains the char at the specified index in the 
* specified string.
*
*@see com.bristle.jslib.Util.getLeftOffsetToStartOfWord()
*@return  The word
*         Returns the empty string if:
*         - The specified string is empty, undefined, or null.
*         - The specified or corrected index is the string length.
*         Returns a single word delimiter char if the char at the specified 
*         index is a delimiter.
******************************************************************************/
com.bristle.jslib.Util.getWholeWord =
function(strIn, intIndex, strWordDelimiters)
{
    var intStartIndex = com.bristle.jslib.Util.getLeftIndexOfStartOfWord
                                       (strIn, intIndex, strWordDelimiters);
    var intEndIndex = com.bristle.jslib.Util.getRightIndexOfEndOfWord
                                       (strIn, intIndex, strWordDelimiters);
    return strIn.substring(intStartIndex, intEndIndex);
}

/*****************************************************************************
* Return a string containing the sequence of words that contains the chars 
* at the specified start and end indexes into the specified string.  
* Typically used to map a specified start/end range in a string to a larger
* string that includes the entire start word and end word that may have been 
* only partially inside the range.
* Automatically swaps the start and end indexes if start is more than end.
*
*@see com.bristle.jslib.Util.getWholeWord()
*@return  A string containing the sequence of words
*         Returns the empty string if:
*         - The specified string is empty, undefined, or null.
*         - The specified or corrected start index is the string length.
*         Returns a single word if the specified start index refers to a 
*         char in a word, and the specified or corrected end index has the
*         same value as the start index.
*         Returns a single word delimiter char if the specified start index 
*         refers to a delimiter, and the specified or corrected end index 
*         has the same value as the start index.
******************************************************************************/
com.bristle.jslib.Util.getSequenceOfWholeWords =
function(strIn, intStartIndex, intEndIndex, strWordDelimiters)
{
    // Check parameters and fill in defaults
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(strIn))
    {
        return "";
    }
    intStartIndex 
              = com.bristle.jslib.Util.getCorrectedIndex(strIn, intStartIndex);
    intEndIndex 
              = com.bristle.jslib.Util.getCorrectedIndex(strIn, intEndIndex);
    if (intStartIndex > intEndIndex)
    {
        // Swap start and end idexes
        var intTemp = intStartIndex;
        intStartIndex = intEndIndex;
        intEndIndex = intTemp;
    }

    intStartIndex = com.bristle.jslib.Util.getLeftIndexOfStartOfWord
                                (strIn, intStartIndex, strWordDelimiters);
    intEndIndex = com.bristle.jslib.Util.getRightIndexOfEndOfWord
                                (strIn, intEndIndex, strWordDelimiters);
    var strRC = strIn.substring(intStartIndex, intEndIndex);
    return strRC;
}

/*****************************************************************************
* Return a string containing the trimmed sequence of words that contains the 
* chars at the specified start and end indexes into the specified string,
* where trimmed means that it returns the same value as 
* com.bristle.jslib.Util.getSequenceOfWholeWords() except that leading and
* trailing word delimiters are trimmed off of the word.  If the word itself
* is a word delimiter, that single delimiter is not trimmed.
* Typically used to map a specified start/end range in a string to a larger
* string that includes the entire start word and end word that may have been 
* only partially inside the range.  May also map to a shorter string, due to
* the trimming of word delimiters from one or both ends.
* Automatically swaps the start and end indexes if start is more than end.
*
*@see com.bristle.jslib.Util.getSequenceOfWholeWords()
*@return  A string containing the sequence of words
*         Returns the empty string if:
*         - The specified string is empty, undefined, or null.
*         - The specified or corrected start index is the string length.
*         - The resulting string would have contained only word delimiters.
*         Returns a single word if the specified start index refers to a 
*         char in a word, and the specified or corrected end index has the
*         same value as the start index.
*         Returns a single word delimiter char if the specified start index 
*         refers to a delimiter, and the specified or corrected end index 
*         has the same value as the start index.
******************************************************************************/
com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed =
function(strIn, intStartIndex, intEndIndex, strWordDelimiters)
{
    // Check parameters and fill in defaults
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString(strIn))
    {
        return "";
    }
    intStartIndex 
              = com.bristle.jslib.Util.getCorrectedIndex(strIn, intStartIndex);
    intEndIndex 
              = com.bristle.jslib.Util.getCorrectedIndex(strIn, intEndIndex);
    if (intStartIndex > intEndIndex)
    {
        // Swap start and end idexes
        var intTemp = intStartIndex;
        intStartIndex = intEndIndex;
        intEndIndex = intTemp;
    }
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString
                                                      (strWordDelimiters)) 
    {
        strWordDelimiters = com.bristle.jslib.Util.getDefaultWordDelimiters();
    }

    // Trim off any leading and trailing word delimiters.
    while (intStartIndex < intEndIndex
           &&
           strWordDelimiters.indexOf(strIn.charAt(intStartIndex)) >= 0
          )
    {
        intStartIndex++;
    }
    while (intStartIndex < intEndIndex
           &&
           strWordDelimiters.indexOf(strIn.charAt(intEndIndex)) >= 0
          )
    {
        intEndIndex--;
    }

    return com.bristle.jslib.Util.getSequenceOfWholeWords
                (strIn, intStartIndex, intEndIndex, strWordDelimiters);
}

/*****************************************************************************
* Round the specified DOM Range object to the nearest word boundaries.
* That is, change the startOffset and endOffset of the Range object so 
* that the Range contains the trimmed sequence of words that contains the 
* chars at the start and end of the Range, where trimmed is defined in 
* com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed().
* Basically, the Range is shrunk to exclude any leading and trailing word
* delimiters, and grown to include the entire start word and end word that 
* may have been only partially inside the range.
*
*@see com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed()
*@return  The Range, which is also updated in place.
******************************************************************************/
com.bristle.jslib.Util.roundRangeToWholeWordsWithDelimitersTrimmed =
function(objRange, strWordDelimiters)
{
    // Check parameters and fill in defaults
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(objRange)
        || !(objRange instanceof Range)
       )
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_DOM_RANGE
            ,"The specified object is not a DOM Range"
            ,"com.bristle.jslib.Util.roundRangeToWholeWordsWithDelimitersTrimmed"
            );
    }
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString
                                                      (strWordDelimiters)) 
    {
        strWordDelimiters = com.bristle.jslib.Util.getDefaultWordDelimiters();
    }

    // Get text strings from start and end DOM nodes of the range, and the 
    // start and end indexes into those strings.
    // Note: Decrement the end index since it points one past the last 
    //       char of the range, but insist on at least the start index value.
    var xmlStartNode  = objRange.startContainer;
    var xmlEndNode    = objRange.endContainer;
    var strStartText  = xmlStartNode.nodeValue;
    var strEndText    = xmlEndNode.nodeValue;
    var intStartIndex = objRange.startOffset;
    var intEndIndex   = Math.max(intStartIndex, objRange.endOffset - 1);

    // Trim off any leading and trailing word delimiters.
    //?? Oops!  Can't really compare intStartIndex and intEndIndex which
    //?? are indexes into text of different nodes.
    while (intStartIndex < intEndIndex 
           &&
           strWordDelimiters.indexOf(strStartText.charAt(intStartIndex)) >= 0
          )
    {
        intStartIndex++;
    }
    //?? Oops!  Can't really compare intStartIndex and intEndIndex which
    //?? are indexes into text of different nodes.
    while (intStartIndex < intEndIndex
           &&
           strWordDelimiters.indexOf(strEndText.charAt(intEndIndex)) >= 0
          )
    {
        intEndIndex--;
    }

    // Adjust ends of range outward to nearest word boundaries
    intStartIndex = com.bristle.jslib.Util.getLeftIndexOfStartOfWord 
                                (strStartText, intStartIndex);
    intEndIndex   = com.bristle.jslib.Util.getRightIndexOfEndOfWord 
                                (strEndText,   intEndIndex);

    // Apply the changes to the range
    objRange.setStart(xmlStartNode, intStartIndex);
    objRange.setEnd  (xmlEndNode,   intEndIndex);

    // Return a reference to the range which has already been updated in place.
    return objRange;
}

/******************************************************************************
* Convert 2-char CR-LF sequences to single-char spaces because String.length 
* counts them as 2 chars, but TextRange.moveStart() and TextRange.moveEnd() 
* in IE6 move past them as a single char, so any computed offsets based on 
* length would be off by 1 for each newline in the text.
******************************************************************************/
com.bristle.jslib.Util.fixStringForOffsetComputation =
function(strIn)
{
    return strIn.replace(/\r\n/mig, " ");
}

/*****************************************************************************
* Round the specified IE TextRange object to the nearest word boundaries.
* That is, change the start and end of the TextRange object so that it 
* contains the trimmed sequence of words that contains the chars at the 
* start and end of the TextRange, where trimmed is defined in 
* com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed().
* Basically, the TextRange is shrunk to exclude any leading and trailing word
* delimiters, and grown to include the entire start word and end word that 
* may have been only partially inside the TextRange.
*
*@see com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed()
*@return  The TextRange, which is also updated in place.
******************************************************************************/
com.bristle.jslib.Util.roundTextRangeToWholeWordsWithDelimitersTrimmed =
function(objTextRange, strWordDelimiters)
{
    // Check parameters and fill in defaults
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(objTextRange)
        || typeof(objTextRange.moveStart) == "undefined"
        || typeof(objTextRange.moveEnd)   == "undefined"
           // Note:  Can't use:
           //           instanceof TextRange.
           //        IE 6 reports:
           //           Error: 'TextRange' is undefined
       )
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_TEXTRANGE
            ,"The specified object is not a TextRange"
            ,"com.bristle.jslib.Util.roundTextRangeToWholeWordsWithDelimitersTrimmed"
            );
    }
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString
                                                      (strWordDelimiters)) 
    {
        var strExceptTheseWordDelimiters = "<>";
        strWordDelimiters = com.bristle.jslib.Util.getDefaultWordDelimiters
                                  (strExceptTheseWordDelimiters);
    }

    // ************************************************************************
    // Note: Couldn't find a way to get the start/end node and start/end 
    //       offset of the IE-only TextRange or of the IE version of a 
    //       selection, so can't do the same algorithm as used by:
    //          roundRangeToWholeWordsWithDelimitersTrimmed()
    //       Have to do it completely differently here.
    // ************************************************************************

    // Get the initial string of text and its start/end indexes.
    // Note: intEndIndex points to the first char beyond the end of the string.
    //       Otherwise empty strings and strings of length 1 would not be 
    //       distinguishable.
    // Note: Get the displayed text, not the htmlText because that's what the 
    //       moveStart() and moveEnd() methods operate on when moving by 
    //       "character".
    var strText = objTextRange.text;
    strText = com.bristle.jslib.Util.fixStringForOffsetComputation(strText);
    var intStartIndex = 0;
    var intEndIndex   = strText.length;

    // Compute the offsets to trim off any leading and trailing word 
    // delimiters.
    var intStartOffset = 0;
    while (intStartIndex < intEndIndex 
           &&
           strWordDelimiters.indexOf(strText.charAt(intStartIndex)) >= 0
          )
    {
        intStartIndex++;
        intStartOffset++;
    }
    var intEndOffset = 0;
    while (intStartIndex < intEndIndex
           &&
           strWordDelimiters.indexOf(strText.charAt(intEndIndex - 1)) >= 0
          )
    {
        intEndIndex--;
        intEndOffset--;
    }

    // Allocate a duplicate TextRange, attempting to extend the start and 
    // end outward from the original TextRange, and use that as the context 
    // to decide whether to grow the specified TextRange.
    // Note: Any better way?  This is the only way I've found to access the 
    //       text adjacent to the specified TextRange.  Can get its own text, 
    //       but not learn what text was adjacent, except by moving the start 
    //       and end of it or a duplicate TextRange.
    var objLargerTextRange = objTextRange.duplicate();
    var strUNITS_CHAR = "character";
    var int50_CHARS_TO_THE_LEFT_OR_TO_THE_PAGE_START  = -50;
    objLargerTextRange.moveStart
            (strUNITS_CHAR, int50_CHARS_TO_THE_LEFT_OR_TO_THE_PAGE_START);
    var strLargerText = objLargerTextRange.text;
    strLargerText = com.bristle.jslib.Util.fixStringForOffsetComputation
                                           (strLargerText);
    var intCharsAddedToLeft = strLargerText.length - strText.length;
    var int50_CHARS_TO_THE_RIGHT_OR_TO_THE_PAGE_END = 50;
    objLargerTextRange.moveEnd
            (strUNITS_CHAR, int50_CHARS_TO_THE_RIGHT_OR_TO_THE_PAGE_END);
    strLargerText = objLargerTextRange.text;
    strLargerText = com.bristle.jslib.Util.fixStringForOffsetComputation
                                           (strLargerText);

    // Compute start/end indexes of the delimiter-trimmed portion of the 
    // smaller string within the entire larger string.  These will be the 
    // starting points used within the larger string when searching outward 
    // for word boundaries.
    // Note: Computing these indexes is more reliable than later searching 
    //       for the smaller string in the larger string because that has 
    //       2 problems:
    //       - Can find wrong substring.  For example, finds 1st occurrence
    //         of "a" in "available" when 2nd occurrence was intended.
    //       - Has no way to find the right substring when the substring 
    //         is empty.  Always finds the start of the larger string.
    //         This is a very common case because, when the user clicks 
    //         instead of dragging to select text, an empty TextRange is
    //         often created, but its locations is still meaningful.
    intStartIndex = intCharsAddedToLeft + intStartOffset;
    intEndIndex   = intCharsAddedToLeft + strText.length + intEndOffset;

    // Back up intEndIndex to point to the last char, if any, of the trimmed 
    // text, not one beyond the last char, since that's required by 
    // getRightOffsetToEndOfWord().  Otherwise, it would start on the next
    // word.
    if (strText.length > 0)
    {
        intEndIndex--;
    }

    // The offsets currently trim off leading and trailing word delimiters,
    // if any.  Adjust them outward to the nearest word boundaries.
    intStartOffset -= com.bristle.jslib.Util.getLeftOffsetToStartOfWord 
                                (strLargerText, intStartIndex);
    intEndOffset   += com.bristle.jslib.Util.getRightOffsetToEndOfWord 
                                (strLargerText, intEndIndex);

    // Reduce intEndOffset by one if we backed up intEndIndex earlier.
    if (strText.length > 0)
    {
        intEndOffset--;
    }

    // Apply the computed offsets to the original TextRange.
    if (intStartOffset != 0)
    {
        objTextRange.moveStart(strUNITS_CHAR, intStartOffset);
    }
    if (intEndOffset != 0)
    {
        objTextRange.moveEnd(strUNITS_CHAR,   intEndOffset);
    }

    // Return a reference to the range which has already been updated in place.
    return objTextRange;
}

/*****************************************************************************
* Return true if the specified DOM Range object contains only a single char
* and that char is a word delimiter.
*
*@see com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed()
*@return  True if a single word delimiter; false otherwise
******************************************************************************/
com.bristle.jslib.Util.rangeIsASingleWordDelimiter =
function(objRange, strWordDelimiters)
{
    // Check parameters and fill in defaults
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(objRange)
        || !(objRange instanceof Range)
       )
    {

        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_DOM_RANGE
            ,"The specified object is not a DOM Range"
            ,"com.bristle.jslib.Util.roundRangeToWholeWordsWithDelimitersTrimmed"
            );
    }
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString
                                                      (strWordDelimiters)) 
    {
        strWordDelimiters = com.bristle.jslib.Util.getDefaultWordDelimiters();
    }

    var xmlStartNode  = objRange.startContainer;
    var xmlEndNode    = objRange.endContainer;
    if (xmlStartNode != xmlEndNode)
    {
        return false;
    }

    var strText  = xmlStartNode.nodeValue;
    var intStartIndex = objRange.startOffset;
    // Note: Decrement the end index since it points one past the last 
    //       char of the range, but insist on at least the start index value.
    // Note: Can compare intStartIndex and intEndIndex since we ensured above
    //       that they are indexes into the same node.
    var intEndIndex   = Math.max(intStartIndex, objRange.endOffset - 1);
    if (intStartIndex != intEndIndex)
    {
        return false;
    }

    if (strWordDelimiters.indexOf(strText.charAt(intStartIndex)) >= 0)
    {
        return true;
    }

    return false;
}

/*****************************************************************************
* Return true if the specified IE TextRange object contains only a single char
* and that char is a word delimiter.
*
*@see com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed()
*@return  True if a single word delimiter; false otherwise
******************************************************************************/
com.bristle.jslib.Util.textRangeIsASingleWordDelimiter =
function(objTextRange, strWordDelimiters)
{
    // Check parameters and fill in defaults
    if (   com.bristle.jslib.Util.isMissingNullOrUndefined(objTextRange)
        || typeof(objTextRange.moveStart) == "undefined"
        || typeof(objTextRange.moveEnd) == "undefined"
           // Note:  Can't use:
           //           instanceof TextRange.
           //        IE 6 reports:
           //           Error: 'TextRange' is undefined
      )
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_NOT_A_TEXTRANGE
            ,"The specified object is not a TextRange"
            ,"com.bristle.jslib.Util.roundTextRangeToWholeWordsWithDelimitersTrimmed"
            );
    }
    if (com.bristle.jslib.Util.isMissingNullUndefinedOrEmptyString
                                                      (strWordDelimiters)) 
    {
        strWordDelimiters = com.bristle.jslib.Util.getDefaultWordDelimiters();
    }

    var strText = objTextRange.text;
    if (strText.length == 1 && strWordDelimiters.indexOf(strText) >= 0)
    {
        return true;
    }

    return false;
}

/*****************************************************************************
* Return true or false to indicate whether any text is currently selected.
* This is useful in a MouseUp event to decide whether there was a simple 
* click, or a drag to select text.
*
*@return  true iif text is selected, false otherwise
*@throws com.bristle.jslib.Exception.intEXC_UNSUPPORTED_BROWSER
******************************************************************************/
com.bristle.jslib.Util.aRangeIsSelected =
function()
{
    if (window.getSelection)
    {
        var objSelection = window.getSelection();
        if (objSelection.getRangeAt 
            && objSelection.rangeCount > 0)
        {
            var objSelectionRange = objSelection.getRangeAt(0);
            if (objSelectionRange.collapsed)
            {
                return false;
            }
            else
            {
                return true;
            }
        }
        else
        {
            return false;
        }
    }
    else if (document.selection)
    {
        var objSelection = document.selection;
        if (objSelection.createRange)
        {
            var objSelectionTextRange = objSelection.createRange();
            if (objSelectionTextRange.text == "")
            {
                return false;
            }
            else
            {
                return true;
            }
        }
        else
        {
            return false;
        }
    }
    else
    {
        throw new com.bristle.jslib.Exception.Exception
            (com.bristle.jslib.Exception.intEXC_UNSUPPORTED_BROWSER
            ,"Unable to determine whether a range is selected"
            ,"com.bristle.jslib.Util.aRangeIsSelected"
            );
    }
}

/******************************************************************************
* Encode a URI component, replacing all special chars with hex values.
*
* Note:  May be able to replace with this with the new built-in 
*        encodeURIComponent() when we move to a newer version of 
*        Internet Explorer (5.5 or greater).
******************************************************************************/
com.bristle.jslib.Util.encodeURIComponent =
function(strIn)
{
    return com.bristle.jslib.Util.replaceAll (escape(strIn), "+", "%2B");
                    // Note:  Have to explicitly map plus signs to their
                    //        hex equivalent after calling escape().
                    //        Otherwise, escape() leaves them alone, and 
                    //        the server maps them to blank spaces when 
                    //        it receives them.  This problem may not 
                    //        occur with encodeURIComponent().
}

/******************************************************************************
* Encode a string to be used as an HTML attribute string -- double the single 
* quote chars, double the double quote chars, etc., making it suitable to wrap 
* in a pair of single quotes and use as an HTML attribute value.
******************************************************************************/
com.bristle.jslib.Util.encodeForHTMLAttributeString =
function(strIn)
{
    var strValid = strIn;
    strValid = com.bristle.jslib.Util.replaceAll(strValid, "'", "''");
    return strValid;
}

/******************************************************************************
* Encode a string to be used as a Javascript literal -- escape the special
* chars (apostrophe, quote, and backslash) with a backslash, making it 
* suitable to wrap in a pair of single quotes and use as a Javascript 
* string literal.
******************************************************************************/
com.bristle.jslib.Util.encodeForJavascriptString =
function(strIn)
{
    var strValid = strIn;
    strValid = com.bristle.jslib.Util.replaceAll(strValid, "x", "a");
//?? Doesn't work.  Infinite loop.  Why?  Because replaceAll() uses 
//?? String.replace() which treats it as a regular expression?
//?? May have to re-write replaceAll() to avoid regular expressions like I did
//?? in javalib.
//??    strValid = com.bristle.jslib.Util.replaceAll(strValid, "\\", "\\\\");
//??    strValid = com.bristle.jslib.Util.replaceAll(strValid, "\"", "\\\"");
//??    strValid = com.bristle.jslib.Util.replaceAll(strValid, "\'", "\\\'");
//?? Aha!  Problem is my implementation of replaceAll which is an infinite
//?? loop if strFrom includes strTo.  Fix that.
    return strValid;
}

/******************************************************************************
* Strip enclosing double quotes from the specified string.  Also 
* strip any leading or trailing whitespace outside the quotes.
******************************************************************************/
com.bristle.jslib.Util.stripQuotes =
function(strIn)
{
    var strOut = com.bristle.jslib.Util.trim(strIn);
    var intStart = 0;
    var intEnd   = strOut.length - 1;
    if (intStart < intEnd)
    {
        if ((strOut.charAt(intStart) == '"' && strOut.charAt(intEnd) == '"') 
            ||
            (strOut.charAt(intStart) == "'" && strOut.charAt(intEnd) == "'")
           )
        { 
            intStart++;
            intEnd--;
        }
    }
    return strOut.substring(intStart, intEnd + 1);
}

/******************************************************************************
* Build multi-line string containing names and values of all properties
* of an object.
******************************************************************************/
com.bristle.jslib.Util.getProps =
function(obj)
{
    var strRC = "";
    for (var p in obj)
    {
        strRC += p + ': ' + obj[p] + '\n';
    }
    return strRC;
}

/******************************************************************************
* Browser-detection code from O'Reilly Dynamic HTML book, pg 89.
******************************************************************************/
com.bristle.jslib.Util.blnIsIE6CSS = 
        (   (document.compatMode)
         && (document.compatMode.indexOf("CSS1") >= 0)
        )
        ? true : false;

/******************************************************************************
* Show a text message in the status bar of the browser's window, optionally 
* clearing it after a specified number of seconds.
* Note: In Internet Explorer IE 6.0.2800.1106, the screen is updated to show
*       the status immediately.  In Firefox 2.0.0.9, the screen is updated 
*       only after the currently running JavaScript code completes and 
*       control is returned to the browser.  Therefore, in Firefox, this is 
*       not useful for showing the status of multiple steps in a single 
*       long-running block of JavaScript code inside a single event handler.
* Anticipated Changes:
* - Does not yet check for apostrophe chars in the message, or any other 
*   chars that could cause syntax errors in calling setTimeout.  For now, 
*   the caller should double all apostrophes.
*
*@param strMessage  Message to show
*@param intSeconds  Number of seconds before clearing the message.
*                   Optional.  Default: Don't clear it.
******************************************************************************/
com.bristle.jslib.Util.strStatusBarMessageToBeCleared = "";
com.bristle.jslib.Util.showStatusBarMessage =
function(strMessage, intSeconds)
{
    window.status = strMessage;
    if (typeof(intSeconds) != "undefined")
    {
        // Schedule a code snippet after the specified number of seconds 
        // that will clear the status bar only if the original message is 
        // still being shown at that time.
        // Note: Prefix intSeconds with "0" and use parseInt() to guarantee 
        //       a valid numeric value, defaulting to zero.
        com.bristle.jslib.Util.strStatusBarMessageToBeCleared = strMessage;
        intSeconds = parseInt("0" + intSeconds, 10);
        window.setTimeout
            (  "if (com.bristle.jslib.Util.strStatusBarMessageToBeCleared"
             + "    == '" + strMessage + "') "
             + "{ com.bristle.jslib.Util.showStatusBarMessage(''); }"
            ,intSeconds * 1000
            ,"JavaScript");
    }
}

/******************************************************************************
* Run regression tests for this JavaScript file.
* Note: This requires the files:
*               com.bristle.jslib.Exception.js
*               com.bristle.jslib.MsgBox.js
*       for assertion checking and assertion failure reporting. 
******************************************************************************/
com.bristle.jslib.Util.runTests =
function()
{
    function assert(strAssertion)
    {
        com.bristle.jslib.MsgBox.assert
                (strAssertion, null, "com.bristle.jslib.Util.runTests");
    }

    debugPrint("START com.bristle.jslib.Util.runTests()");

    assert('0 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc", 0)');
    assert('1 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc", 1)');
    assert('2 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc", 2)');
    assert('0 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc", 3)');
    assert('0 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc", 4)');
    assert('0 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc def", 0)');
    assert('1 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc def", 1)');
    assert('2 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc def", 2)');
    assert('0 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc def", 3)');
    assert('0 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc def", 4)');
    assert('1 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc def", 5)');
    assert('2 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc def", 6)');
    assert('0 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc def", 7)');
    assert('0 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc def", 8)');
    assert('0 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc def", 9)');
    assert('0 == com.bristle.jslib.Util.getLeftOffsetToStartOfWord("abc def", -1)');

    assert('0 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc", 0)');
    assert('0 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc", 1)');
    assert('0 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc", 2)');
    assert('3 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc", 3)');
    assert('3 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc", 4)');
    assert('0 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc def", 0)');
    assert('0 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc def", 1)');
    assert('0 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc def", 2)');
    assert('3 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc def", 3)');
    assert('4 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc def", 4)');
    assert('4 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc def", 5)');
    assert('4 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc def", 6)');
    assert('7 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc def", 7)');
    assert('7 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc def", 8)');
    assert('7 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc def", 9)');
    assert('7 == com.bristle.jslib.Util.getLeftIndexOfStartOfWord("abc def", -1)');

    assert('3 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc", 0)');
    assert('2 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc", 1)');
    assert('1 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc", 2)');
    assert('0 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc", 3)');
    assert('0 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc", 4)');
    assert('3 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc def", 0)');
    assert('2 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc def", 1)');
    assert('1 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc def", 2)');
    assert('1 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc def", 3)');
    assert('3 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc def", 4)');
    assert('2 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc def", 5)');
    assert('1 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc def", 6)');
    assert('0 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc def", 7)');
    assert('0 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc def", 8)');
    assert('0 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc def", 9)');
    assert('0 == com.bristle.jslib.Util.getRightOffsetToEndOfWord("abc def", -1)');

    assert('3 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc", 0)');
    assert('3 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc", 1)');
    assert('3 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc", 2)');
    assert('3 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc", 3)');
    assert('3 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc", 4)');
    assert('3 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc def", 0)');
    assert('3 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc def", 1)');
    assert('3 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc def", 2)');
    assert('4 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc def", 3)');
    assert('7 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc def", 4)');
    assert('7 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc def", 5)');
    assert('7 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc def", 6)');
    assert('7 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc def", 7)');
    assert('7 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc def", 8)');
    assert('7 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc def", 9)');
    assert('7 == com.bristle.jslib.Util.getRightIndexOfEndOfWord("abc def", -1)');

    assert('"abc" == com.bristle.jslib.Util.getWholeWord("abc", 0)');
    assert('"abc" == com.bristle.jslib.Util.getWholeWord("abc", 1)');
    assert('"abc" == com.bristle.jslib.Util.getWholeWord("abc", 2)');
    assert('""    == com.bristle.jslib.Util.getWholeWord("abc", 3)');
    assert('""    == com.bristle.jslib.Util.getWholeWord("abc", 4)');
    assert('"abc" == com.bristle.jslib.Util.getWholeWord("abc def", 0)');
    assert('"abc" == com.bristle.jslib.Util.getWholeWord("abc def", 1)');
    assert('"abc" == com.bristle.jslib.Util.getWholeWord("abc def", 2)');
    assert('" "   == com.bristle.jslib.Util.getWholeWord("abc def", 3)');
    assert('"def" == com.bristle.jslib.Util.getWholeWord("abc def", 4)');
    assert('"def" == com.bristle.jslib.Util.getWholeWord("abc def", 5)');
    assert('"def" == com.bristle.jslib.Util.getWholeWord("abc def", 6)');
    assert('""    == com.bristle.jslib.Util.getWholeWord("abc def", 7)');
    assert('""    == com.bristle.jslib.Util.getWholeWord("abc def", 8)');
    assert('""    == com.bristle.jslib.Util.getWholeWord("abc def", 9)');
    assert('""    == com.bristle.jslib.Util.getWholeWord("abc def", -1)');
    assert('" "   == com.bristle.jslib.Util.getWholeWord(" abc def ", 0)');
    assert('"abc" == com.bristle.jslib.Util.getWholeWord(" abc def ", 1)');
    assert('"abc" == com.bristle.jslib.Util.getWholeWord(" abc def ", 2)');
    assert('"abc" == com.bristle.jslib.Util.getWholeWord(" abc def ", 3)');
    assert('" "   == com.bristle.jslib.Util.getWholeWord(" abc def ", 4)');
    assert('"def" == com.bristle.jslib.Util.getWholeWord(" abc def ", 5)');
    assert('"def" == com.bristle.jslib.Util.getWholeWord(" abc def ", 6)');
    assert('"def" == com.bristle.jslib.Util.getWholeWord(" abc def ", 7)');
    assert('" "   == com.bristle.jslib.Util.getWholeWord(" abc def ", 8)');
    assert('""    == com.bristle.jslib.Util.getWholeWord(" abc def ", 9)');
    assert('""    == com.bristle.jslib.Util.getWholeWord(" abc def ", -1)');

    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 0, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 1, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 2, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 3, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 4, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 0, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 1, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 2, 0)');
    assert('"abc "      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 3, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 4, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 5, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 6, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 7, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 8, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 9, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", -1, 0)');
    assert('" "         == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 0, 0)');
    assert('" abc"      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 1, 0)');
    assert('" abc"      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 2, 0)');
    assert('" abc"      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 3, 0)');
    assert('" abc "     == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 4, 0)');
    assert('" abc def"  == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 5, 0)');
    assert('" abc def"  == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 6, 0)');
    assert('" abc def"  == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 7, 0)');
    assert('" abc def " == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 8, 0)');
    assert('" abc def " == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 9, 0)');
    assert('" abc def " == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", -1, 0)');

    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 0, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 1, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 2, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 3, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 4, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 0, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 1, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 2, 1)');
    assert('"abc "      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 3, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 4, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 5, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 6, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 7, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 8, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 9, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", -1, 1)');
    assert('" abc"      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 0, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 1, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 2, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 3, 1)');
    assert('"abc "      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 4, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 5, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 6, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 7, 1)');
    assert('"abc def "  == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 8, 1)');
    assert('"abc def "  == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 9, 1)');
    assert('"abc def "  == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", -1, 1)');

    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 0, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 1, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 2, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 3, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 4, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 0, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 1, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 2, 2)');
    assert('"abc "      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 3, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 4, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 5, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 6, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 7, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 8, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 9, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", -1, 2)');
    assert('" abc"      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 0, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 1, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 2, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 3, 2)');
    assert('"abc "      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 4, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 5, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 6, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 7, 2)');
    assert('"abc def "  == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 8, 2)');
    assert('"abc def "  == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 9, 2)');
    assert('"abc def "  == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", -1, 2)');

    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 0, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 1, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 2, 3)');
    assert('""          == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 3, 3)');
    assert('""          == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 4, 3)');
    assert('"abc "      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 0, 3)');
    assert('"abc "      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 1, 3)');
    assert('"abc "      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 2, 3)');
    assert('" "         == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 3, 3)');
    assert('" def"      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 4, 3)');
    assert('" def"      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 5, 3)');
    assert('" def"      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 6, 3)');
    assert('" def"      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 7, 3)');
    assert('" def"      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 8, 3)');
    assert('" def"      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 9, 3)');
    assert('" def"      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", -1, 3)');
    assert('" abc"      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 0, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 1, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 2, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 3, 3)');
    assert('"abc "      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 4, 3)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 5, 3)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 6, 3)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 7, 3)');
    assert('"abc def "  == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 8, 3)');
    assert('"abc def "  == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 9, 3)');
    assert('"abc def "  == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", -1, 3)');

    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 0, 4)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 1, 4)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 2, 4)');
    assert('""          == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 3, 4)');
    assert('""          == com.bristle.jslib.Util.getSequenceOfWholeWords("abc", 4, 4)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 0, 4)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 1, 4)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 2, 4)');
    assert('" def"      == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 3, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 4, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 5, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 6, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 7, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 8, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", 9, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWords("abc def", -1, 4)');
    assert('" abc "     == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 0, 4)');
    assert('"abc "      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 1, 4)');
    assert('"abc "      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 2, 4)');
    assert('"abc "      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 3, 4)');
    assert('" "         == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 4, 4)');
    assert('" def"      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 5, 4)');
    assert('" def"      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 6, 4)');
    assert('" def"      == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 7, 4)');
    assert('" def "     == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 8, 4)');
    assert('" def "     == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", 9, 4)');
    assert('" def "     == com.bristle.jslib.Util.getSequenceOfWholeWords(" abc def ", -1, 4)');

    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 0, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 1, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 2, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 3, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 4, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 0, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 1, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 2, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 3, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 4, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 5, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 6, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 7, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 8, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 9, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", -1, 0)');
    assert('" "         == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 0, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 1, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 2, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 3, 0)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 4, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 5, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 6, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 7, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 8, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 9, 0)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", -1, 0)');

    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 0, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 1, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 2, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 3, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 4, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 0, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 1, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 2, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 3, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 4, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 5, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 6, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 7, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 8, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 9, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", -1, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 0, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 1, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 2, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 3, 1)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 4, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 5, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 6, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 7, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 8, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 9, 1)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", -1, 1)');

    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 0, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 1, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 2, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 3, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 4, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 0, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 1, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 2, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 3, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 4, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 5, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 6, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 7, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 8, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 9, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", -1, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 0, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 1, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 2, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 3, 2)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 4, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 5, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 6, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 7, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 8, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 9, 2)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", -1, 2)');

    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 0, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 1, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 2, 3)');
    assert('""          == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 3, 3)');
    assert('""          == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 4, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 0, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 1, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 2, 3)');
    assert('" "         == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 3, 3)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 4, 3)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 5, 3)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 6, 3)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 7, 3)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 8, 3)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 9, 3)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", -1, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 0, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 1, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 2, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 3, 3)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 4, 3)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 5, 3)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 6, 3)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 7, 3)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 8, 3)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 9, 3)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", -1, 3)');

    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 0, 4)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 1, 4)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 2, 4)');
    assert('""          == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 3, 4)');
    assert('""          == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc", 4, 4)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 0, 4)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 1, 4)');
    assert('"abc def"   == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 2, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 3, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 4, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 5, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 6, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 7, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 8, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", 9, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed("abc def", -1, 4)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 0, 4)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 1, 4)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 2, 4)');
    assert('"abc"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 3, 4)');
    assert('" "         == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 4, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 5, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 6, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 7, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 8, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", 9, 4)');
    assert('"def"       == com.bristle.jslib.Util.getSequenceOfWholeWordsWithDelimitersTrimmed(" abc def ", -1, 4)');
    
    assert('"abc"       == com.bristle.jslib.Util.trimAllWhitespace("   abc   ")');

    debugPrint("END   com.bristle.jslib.Util.runTests()");
}

