/***********************************************
* File: cb.js
* Desc: Captain Black Web Site -- Main functions
* Auth: Kevin Machin.
***********************************************/

/*******************
* GLOBAL VARIABLES *
*******************/

var gbCbLegacyCode = true;   // Allow legacy interfaces. TODO: Drop this eventually.
var giCbDebugFlags = 0x00;

// Debug bits...
var cbdbg_DIAG     = 0x01;  // Diagnostic information
var cbdbg_NAV      = 0x02;  // Extra navigation items displayed
var cbdbg_BORDERS  = 0x04;  // Highlight borders for positioning
var cbdbg_NOSCRIPT = 0x08;  // Produce controls for generating [noscript] HTML
var cbdbg_CODE     = 0x10;  // Write code as text on screen

// Page hiding
var giCbPageHide = 0;

// Meter
var giCbMeterWidth = 100, giCbMeterHeight = 10;

/********************
* Startup Functions *
********************/

var gapfnCbOnLoad = new Array;

try
{
    // Executed immediately
    cbStartup ();

    // Register function to be called when page is loaded
    cbOnLoad (window.onload);
    window.onload = cbLoaded;
}
catch (e)
{
    var s, sMsg = "Failed to execute start-up commands!";
    try
    {
        s = "<p class=\"warning\"><b>" + sMsg + "</b><br>" + e + "</p>";
        document.write (s);
    }
    catch (e)
    {
        s = sMsg + "\n" + e;
        alert (s);
    }
}

/////////////////////////////////////////////
// Func: cbStartup
// Desc: Function called during page loading.
// Args: None.
// Retn: Nothing.
/////////////////////////////////////////////

function cbStartup ()
{
    var sUrl, s, cBra, cKet;
    
    // StatCounter parameters
    /*jsl:ignore*/ // Keep lint quiet about undeclared globals that StatCounter needs
    sc_project    = 4142039;
    sc_invisible  = 1;
    sc_partition  = 51;
    sc_click_stat = 1;
    sc_security   = "27659d72";
    /*jsl:end*/
    sUrl = document.URL;
    sUrl = sUrl.slice (0, 5);
    if ((sUrl === "file:") || (cbDebugMask () & cbdbg_DIAG))
    {
        cBra = "&lt;"; cKet = "&gt;";
    }
    else
    {
        cBra = "<"; cKet = ">";
    }

    // Run the StatCounter code
    /* FIXME: Disabled for now...
    if ((sUrl !== "file:") || (giCbDebugFlags & cbdbg_DIAG))
    {
        s =  cBra + "script type=\"text/javascript\" ";
        s += "src=\"http://www.statcounter.com/counter/counter.js\"";
        s += cKet + cBra + "/script" + cKet;
        document.write (s);
    }*/
}
// End of cbStartup

/////////////////////////////////////////////
// Func: cbLoaded
// Desc: Function called when page is loaded.
// Args: None.
// Retn: Nothing.
/////////////////////////////////////////////

function cbLoaded ()
{
    var i, pfn;
    
    // Set styles for extra borders
    if (cbDebugMask () & cbdbg_BORDERS)
    {
        var oHead = document.getElementsByTagName ("head");
        oHead = oHead[0];
        var oLink = document.createElement ("link");
        oLink.rel = "stylesheet";
        oLink.type = "text/css";
        oLink.href = "file://U:/Web/Styles/debug.css";
        oHead.appendChild (oLink);
    }

    // Diagnostics
    if (cbDebugMask () & cbdbg_DIAG)
    {
        var oBody = document.body;
        var oDebug = document.createElement ("span");
        oDebug.className = "warning";
        oDebug.innerHTML = "<b>OnLoaded</b> executed (" + gapfnCbOnLoad.length + ")";
        oBody.insertBefore (oDebug, oBody.firstChild);
    }
    
    // Execute remaining handlers
    for (i = 0; i < gapfnCbOnLoad.length; i++)
    {
        pfn = gapfnCbOnLoad[i];
        if ((pfn !== undefined) && (pfn !== null))
        {
            pfn ();
        }
    }
}
// End of cbLoaded

///////////////////////////////////////////////////////////////////
// Func: cbOnLoad
// Desc: Register a function to be called when the page has loaded.
// Args: pfnNew -- New function to be registered.
// Retn: Nothing.
///////////////////////////////////////////////////////////////////

function cbOnLoad (pfnNew)
{
    // Save new handler
    if ((pfnNew !== undefined) && (pfnNew !== null))
    {
        gapfnCbOnLoad.push (pfnNew);
    }
}
// End of cbOnLoad

/*******************************
* OUTPUT AND DISPLAY FUNCTIONS *
*******************************/

///////////////////////////////////////////////////////////////////////
// Func: cbErrMsg
// Desc: Display an error message.
// Args: psMsg -- Error message (null to delete element).
//       oErr  -- Error object with details.
//       oDoc  -- Document to send error message to (default current).
//       psId  -- Element Id to use for display (default "ErrMsg").
// Retn: Full text.
// Note: The function will attempt to use a display element with the ID
//       "ErrMsg". If no such element exists, then one will be added
//       (a paragraph). If that fails, an alert will be used instead.
///////////////////////////////////////////////////////////////////////

function cbErrMsg (psMsg)
{
    var oE, oB, oErr, oDoc, psId;

    // Handle arguments
    oErr = (arguments.length > 1) ? arguments[1] : "";
    oDoc = (arguments.length > 2) ? arguments[2] : document;
    psId = (arguments.length > 3) ? arguments[3] : "ErrMsg";
    if (oDoc === null)
    {
        oDoc = document;
    }

    // Look for pre-existing display element
    oE = oDoc.getElementById (psId);
    if ((psMsg === null) && (oE !== null))
    {
        // Remove element
        oB = oE.parentNode;
        oB.removeChild (oE);
    }
    else if (oE === null)
    {
        // Create a new display element
        try
        {
            oE = oDoc.createElement ("p");
            oB = oDoc.body;
            oB.insertBefore (oE, oB.childNodes[0]);
        }
        catch (e)
        {
            oE = null;
        }
    }

    // What to do?
    if (psMsg !== null)
    {
        if (oE === null)
        {
            // Dipsplay the message as an alert
            if (psMsg !== "")
            {
                psMsg += "\n";
            }
            alert (psMsg + oErr);
        }
        else
        {
            // Display message in element
            if (psMsg !== "")
            {
                psMsg = "<b>" + psMsg + "</b><br />";
                if (oE.nodeName == "SPAN")
                {
                    psMsg = "<br>" + psMsg;
                }
            }
            // TODO: Phasing out: oE.className = "error"
            oE.style.backgroundColor = "LightCyan";
            oE.style.color = "Red";
            oE.innerHTML = psMsg + oErr;
        }
    }

    // Return the full text
    return (psMsg);
}

////////////////////////////////////////////
// Func: cbPageShow
// Desc: Show or hide a page.
// Args: bShow -- True or false for showing.
// Note: Uses element with ID "hide".
////////////////////////////////////////////

function cbPageShow (bShow)
{
    var oE = document.getElementById ("hide");
    if (oE !== null)
    {
        if (bShow)
        {
        	--giCbPageHide;
            if (giCbPageHide <= 0)
            {
                oE.style.visibility = "visible";
                giCbPageHide = 0;
            }
        }
        else
        {
            oE.style.visibility = "hidden";
            giCbPageHide = 2;
        }
    }
}

/**********************
* DEBUGGING FUNCTIONS *
**********************/

/////////////////////////////////////////////////
// Func: cbDebugMask
// Desc: Get (and set) the debugging flags.
// Args: iFlags -- New debugging flags to enable.
// Retn: Previous (or current) debugging flags.
/////////////////////////////////////////////////

function cbDebugMask ()
{
    var iFlags = giCbDebugFlags; // Get current flags

    // Any new flags given?
    if (arguments.length > 0)
    {
        // Set the new flags
        giCbDebugFlags = arguments[0];
        if (giCbDebugFlags < 0)
        {
            giCbDebugFlags = 0;
        }
    }

    // Return the old/current flags
    return (iFlags);
}

/*******************
* STRING FUNCTIONS *
*******************/

//////////////////////////////////////
// Func: cbStrNum
// Desc: Convert a number to a string.
// Args: iNumber -- Number to convert.
//       iFlags  -- Flags to use.
// Retn: Converted string.
//////////////////////////////////////

// TODO: Put flags at top of file

var cbnum_NONE = 0x0; // No special flags
var cbnum_HEX2 = 0x1; // Two digit hexadecimal
var cbnum_HEX4 = 0x2; // Four digit hexadecimal
var cbnum_CSEP = 0x4; // Use comma separators

function cbStrNum (iNumber)
{
    var iFlags, sNumber, sValue, iBase = 10, iDigits = 0, i, iLen, iRem;

    // Get flags
    iFlags = (arguments.length > 1) ? arguments[1] : cbnum_NONE;

    // Don't deal with floats
    iNumber = Math.floor (iNumber);

    // Decide on base and number of digits
    if (iFlags & cbnum_HEX2)
    {
        iBase = 16;
        iDigits = 2;
    }
    else if (iFlags & cbnum_HEX4)
    {
        iBase = 16;
        iDigits = 4;
    }

    // Convert
    sValue = iNumber.toString (iBase);
    if (iBase == 16)
    {
        // Hexadecimal, so pre-pad with zeros
        sNumber = sValue;
        while (sNumber.length < iDigits)
        {
            sNumber = "0" + sNumber;
        }
    }
    else if (iFlags & cbnum_CSEP)
    {
        // Put in commas every three digits
        iLen = iRem = sValue.length;
        sNumber = "";
        for (i = 0; i < iLen; i++, iRem--)
        {
            if ((i > 0) && ((iRem % 3) === 0))
            {
                sNumber += ",";
            }
            sNumber += sValue.slice (i, i + 1);
        }
    }
    else
    {
        sNumber = sValue;
    }

    // Return in uppercase
    return (sNumber.toUpperCase ());
}
// End of cbStrNum

///////////////////////////////////////////////
// Func: cbStrNode
// Desc: Obtain a node's string representation.
// Args: oNode -- Noe to use.
// Retn: String value.
///////////////////////////////////////////////

function cbStrNode (oNode)
{
    var sText = "";

    // Extract only text components
    if (oNode !== null)
    {
        nodeText (oNode);
    }

    ////////////////////////////////////////////////
    // Priv: nodeText
    // Desc: Extract textual components from a node.
    // Args: oN -- Node.
    // Retn: Nothing (updates text variable).
    ////////////////////////////////////////////////

    function nodeText (oN)
    {
        var iN, iNodes, oC, iType;

        // Iterate over child nodes
        iNodes = oN.childNodes.length;
        for (iN = 0; iN < iNodes; iN++)
        {
            // What kind of node?
            oC = oN.childNodes[iN];
            iType = oC.nodeType;
            if (iType === 1)
            {
                // Recurse into sub-element
                nodeText (oC);
            }
            else if (iType === 3)
            {
                // Text
                sText += oC.nodeValue;
            }
        }
    }
    // End of nodeText

    return (sText);
}
// End of cbStrNode

////////////////////////////////////////
// Func: cbStrComp
// Decs: Compare two strings.
// Args: sA -- First string to compare.
//       sB -- Second string to compare.
// Retn: <0 -- A comes before B.
//       >0 -- A comes after B.
//       =0 -- Identical.
// ToDo: Case handling?
////////////////////////////////////////

function cbStrComp (sA, sB)
{
    var iResult;

    if (sA < sB)
    {
        iResult = -1;
    }
    else if (sA > sB)
    {
        iResult = +1;
    }
    else
    {
        iResult = 0;
    }

    return (iResult);
}
// End of cbStrComp

/****************************
* GENERAL PURPOSE FUNCTIONS *
****************************/

/////////////////////////////////////////
// Func: cbArgs
// Desc: Convert arguments into an array.
// Args: atSource -- Source arguments.
// Retn: An array of the arguments.
/////////////////////////////////////////

function cbArgs (atSource)
{
    var atTarget = new Array, iNest = 0;

    // Expand the arguments into an array of strings
    arrayExpand (atTarget, atSource);

    ///////////////////////////////////////////
    // Priv: arrayExpand
    // Desc: Expand an array.
    // Args: aTarget -- Target array.
    //       aSource -- Source array.
    // Retn: Nothing (target array is updated).
    ///////////////////////////////////////////

    function arrayExpand (aTarget, aSource)
    {
        var i, sType, tSource, iSource, iTarget;

        // Get length of target array
        iTarget = aTarget.length;

        // What kind of source do we have?
        sType = typeof (aSource);
        switch (sType)
        {
        case "string":
        case "number":
            aTarget[iTarget] = aSource; iTarget++;
            //alert ("cbArgs (" + iNest + "): Saved string/number '" + aSource + "'");
            break;
        default:
            if (aSource === undefined)
            {
                throw ("Failed to expand undefined source argument! (" + sType + ")");
            }
            else if (aSource === null)
            {
                aTarget[iTarget] = aSource; iTarget++;
                //alert ("cbArgs (" + iNest + "): Saved null");
            }
            else if (aSource.length == undefined)
            {
                aTarget[iTarget] = aSource; // Probably object
                iTarget++;
            }
            else
            {
                // Assume array
                iSource = aSource.length;
                for (i = 0; i < iSource; i++)
                {
                    tSource = aSource[i];
                    //alert ("cbArgs (" + iNest + "): aSource[" + i + "] = '" + tSource + "'")
                    iNest++;
                    arrayExpand (aTarget, tSource);
                    iNest--;
                }
            }
            break;
        }
    }
    // End of arrayExpand

    // Return the array
    return (atTarget);
}
// End of cbArgs

///////////////////////////////////////////////////////////
// Func: cbAttr
// Desc: Extract attributes settings from an array.
// Args: atSource -- Source array.
//       oTag     -- [Optional] tag to apply attributes to.
// Retn: Array of attributes.
///////////////////////////////////////////////////////////

function cbAttr (atSource)
{
    var tArg, atTarget = new Array, sName, sValue;
    var i, iEq, iTarget = 0, oTag;

    // Get tag
    oTag = (arguments.length > 1) ? arguments[1] : null;

    // Iterate through source array
    for (i = 0; i < atSource.length; i++)
    {
        // Filter out attributes
        tArg = atSource[i];
        if (typeof (tArg) === "string")
        {
            iEq = tArg.indexOf ("=");
            if (iEq > 0)
            {
                // Save attribute
                sName  = tArg.slice (0, iEq);
                iEq++;
                sValue = tArg.slice (iEq);
                atTarget[iTarget] = new attribute (sName, sValue);
                iTarget++;

                // Apply to tag (if given)
                if (oTag !== null)
                {
                    oTag.setAttribute (sName, sValue);
                }

                // Remove from source array
                atSource.splice (i, 1); i--;
            }
        }
    }

    ////////////////////////////////////
    // Priv: attribute
    // Desc: Attribute contructor.
    // Args: psName  -- Attribute name.
    //       psValue -- Attribute value.
    // Retn: Reference to new attribute.
    ////////////////////////////////////

    function attribute (psName, psValue)
    {
        this.sName  = psName;
        this.sValue = psValue;
    }
    // End of attribute

    // Return target array
    return (atTarget);
}
// End of cbAttr

/////////////////////////////////////////////////
// Func: cbFindFrame
// Desc: Get the window handle for a named frame.
// Args: psName -- Name of frame to find.
// Retn: Window object.
/////////////////////////////////////////////////

function cbFindFrame (psName)
{
    var oWind = null, oW = window.parent;
    var iF, oF;

    for (iF = 0; iF < oW.length; iF++)
    {
        oF = oW.frames[iF];
        if (oF.name === psName) // TODO: Use Id as well or instead of?
        {
            oWind = oF;
            break;
        }
    }

    return (oWind);
}

//////////////////////////////////////////////////////////////////
// Func: cbEvent
// Desc: Produce a standard event object.
// Args: oEvent -- Incoming event from handler.
// Retn: Standardised event.
// Note: Deals with browser incompatibilities and inconsistencies.
//////////////////////////////////////////////////////////////////

function cbEvent (oEvent)
{
    var oElement;

    // See if we actually have an event
    if (!oEvent)
    {
        oEvent = window.event;
    }

    // Make sure we have the target element
    oElement = oEvent.target;
    if (!oElement)
    {
        oElement = oEvent.srcElement;
        oEvent.target = oElement;
    }

    return (oEvent);
}
// End of cbEvent

/****************
* TAG FUNCTIONS *
****************/

////////////////////////////////////////////
// Func: cbTag
// Desc: Create and manipulate a tag object.
// Args: psType -- Tag type, e.g. "td".
//       ...    -- Content & Attributes.
// Retn: Reference to created object.
////////////////////////////////////////////

function cbTag ()
{
    var oTag, i, iArgs, atArg, tArg, sType, psType;

    // Get arguments
    atArg = cbArgs (arguments);
    psType = atArg.shift ();

    // Create the tag
    oTag = document.createElement (psType);

    // Extract and apply any attributes
    cbAttr (atArg, oTag);

    // Apply the content
    iArgs = atArg.length;
    for (i = 0; i < iArgs; i++)
    {
        tArg = atArg[i];
        sType = typeof (tArg);
        if (sType === "string")
        {
            // Add to tag's textual content
            oTag.innerHTML += tArg;
        }
        else if (sType === "number")
        {
            // Add number to textual content
            oTag.innerHTML += tArg;
        }
        else if (tArg !== null)
        {
            // Add to tag's content
            //try
            //{
                oTag.appendChild (tArg);
            //}
            //catch (e)
            //{
                //alert (tArg);
            //}
        }
    }

    // Return reference
    return (oTag);
}
// End of cbTag

////////////////////////////////////////////
// Func: cbLink
// Desc: Create a hyperlink.
// Args: psText -- Text to display.
//       psFile -- File to link to.
//       ...    -- Extra attributes.
// Retn: Object reference.
////////////////////////////////////////////

function cbLink ()
{
    var oLink, atArg, sTitle, oTag, i;

    // Get arguments
    atArg = cbArgs (arguments);
    var psText = (atArg.length > 0) ? atArg.shift () : "";
    var psFile = (atArg.length > 0) ? atArg.shift () : "";

    // Create anchor tag
    oLink = cbTag ("a", psText, atArg);

    // Add file reference
    if (psFile !== "")
    {
        i = psFile.indexOf (".");
        if (i < 2)
        {
            psFile += ".htm";
        }
        oLink.href = psFile;
    }

    // Add title if none has been set
    if (oLink.title === "")
    {
        sTitle = "Click to go to " + psText + "...";
        oTag = cbTag ("span", sTitle);
        oLink.title = cbStrNode (oTag);
    }

    return (oLink);
}
// End of cbLink

/******************
* TABLE FUNCTIONS *
******************/

///////////////////////////////////////////////////////
// Func: cbTable
// Desc: Create a new table element.
// Args: ... -- Zero or more heading row cell contents.
// Retn: Object reference.
///////////////////////////////////////////////////////

function cbTable ()
{
    var oTable, oLastRow, atArg;

    // Create table element
    oTable = document.createElement ("table");

    // Apply any attributes
    atArg = cbArgs (arguments);
    cbAttr (atArg, oTable);

    // Add heading row (if given)
    if (atArg.length > 0)
    {
        rowAdd ("th", atArg);
    }

    //////////////////////////////////////////////////////////
    // Priv: rowAdd
    // Desc: Add a row to the table.
    // Args: ... -- Zero or more cells' contents.
    // Retn: Row reference.
    // Note: Special cases "th" and "td" to set the cell type.
    //////////////////////////////////////////////////////////

    function rowAdd ()
    {
        var i, atArg, tArg, sType = "td";

        // Create the row element
        oLastRow = document.createElement ("tr");

        // Inherit class name from table
        oLastRow.className = oTable.className;

        // Apply any further attributes given
        atArg = cbArgs (arguments);
        cbAttr (atArg, oLastRow);

        // Add cells, if specified
        for (i = 0; i < atArg.length; i++)
        {
            tArg = atArg[i];
            if ((tArg == "th") || (tArg == "td"))
            {
                sType = tArg;
            }
            else
            {
                cellAdd (sType, tArg);
            }
        }

        // Add row to table
        oTable.appendChild (oLastRow);

        // Return the row reference
        return (oLastRow);
    }
    // End of rowAdd

    ////////////////////////////////////////////////////////////////
    // Priv: cellAdd
    // Desc: Add a cell to a table row.
    // Args: ... -- Zero or more arguments.
    // Retn: Cell reference.
    // Note: Arguments are contents by default, unless:
    //       "th" or "td" as first argument to indicate type of cell
    //       (default "td").
    //       Equals sign detected means attribute for cell,
    //       e.g. "style=text-align:right".
    ////////////////////////////////////////////////////////////////

    function cellAdd ()
    {
        var oCell, sType = "td", atArg, tArg, i, iArgs;
        var iNodes = 0;

        // Check for cell type in first argument
        atArg = cbArgs (arguments);
        i = 0; iArgs = atArg.length;
        tArg = (iArgs > 0) ? atArg[0] : "";
        if ((tArg === "td") || (tArg === "th"))
        {
            sType = tArg; i++;
        }

        // Create the cell
        oCell = cbTag (sType);

        // Inherit class name from last row
        oCell.className = oLastRow.className;

        // Apply any further attributes
        cbAttr (atArg, oCell);

        // Iterate over (remaining) arguments
        for (iArgs = atArg.length; i < iArgs; i++)
        {
            // Apply content to cell
            tArg = atArg[i];
            if (tArg !== null)
            {
                // What kind of argument?
                sType = typeof (tArg);
                if ((sType === "string") || (sType === "number"))
                {
                    if (iNodes === 0)
                    {
                        oCell.innerHTML = tArg;
                    }
                    else
                    {
                        tArg = document.createTextNode (tArg);
                        oCell.appendChild (tArg);
                    }
                }
                else
                {
                    //try
                    //{
                        oCell.appendChild (tArg);
                    //}
                    /*catch (e)
                    {
                        // FIXME
                        alert (tArg) //"Failed to put '" + sType + "' into cell!\n" + e);
                    }*/
                }
                iNodes++;
            }
        }

        // Attach cell to table
        oLastRow.appendChild (oCell);

        // Return cell reference
        return (oCell);
    }
    // End of cellAdd

    ////////////////////////////////////////
    // Meth: numberAdd
    // Desc: Add a cell containing a number.
    // Args: iNumber -- Number to use.
    // Retn: Cell reference.
    ////////////////////////////////////////

    function numberAdd (iNumber)
    {
        var oCell;

        // Create cell with comma-separated number in it
        oCell = cellAdd (cbStrNum (iNumber, cbnum_CSEP));
        oCell.className = "number";

        return (oCell);
    }
    // End of numberAdd

    /////////////////////////////////////////////////////
    // Priv: meterAdd
    // Desc: Add a meter to a table, using several cells.
    // Args: iVal -- Meter's current value.
    //       iMax -- Meter's maximum value.
    // Retn: Last cell reference.
    /////////////////////////////////////////////////////

    function meterAdd (iVal, iMax)
    {
        var oCell, iPercent;

        // Values
        oCell = numberAdd (iVal);
        oCell = numberAdd (iMax);

        // Percentage
        iPercent = cbPercent (iVal, iMax);
        oCell = cellAdd (iPercent + "%");
        oCell.className = "number";
        oCell.style.borderRight = "0";

        // Bar
        oCell = barAdd (iVal, iMax);
        oCell.style.borderLeft = "0";

        return (oCell);
    }
    // End of meterAdd

    //////////////////////////////////////////////////
    // Priv: barAdd
    // Desc: Add a progress bar, using multiple cells.
    // Args: iVal -- Bar's current value.
    //       iMax -- Bar's maximum value.
    // Retn: Reference to last cell.
    //////////////////////////////////////////////////

    function barAdd (iVal, iMax)
    {
        var oCell, oBar;

        // Create the bar and put it into a cell
        oBar = cbBar (iVal, iMax);
        oCell = cellAdd (oBar);

        return (oCell);
    }
    // End of barAdd

    //////////////////////////////////////////////////////
    // Priv: downLoad
    // Desc: Add download information, using a table cell.
    // Args: psFile  -- File spec to download.
    //       psTitle -- Title for display.
    // Retn: Cell reference.
    //////////////////////////////////////////////////////

    function downLoad ()
    {
        var oCell, psFile, psTitle, iFormats = 0;

        // Get arguments
        psFile  = (arguments.length > 0) ? arguments[0] : "";
        psTitle = (arguments.length > 1) ? arguments[1] : "";

        // Create cell
        oCell = cellAdd ();
        oCell.style.textAlign = "Center";

        // Add formats
        if (psFile !== "")
        {
            formatAdd ("pdf", "portable document");
            formatAdd ("rtf", "rich text");
        }

        ///////////////////////////////////////
        // Priv: formatAdd
        // Desc: Append a download format link.
        // Args: psExtn -- File extension.
        //       psName -- Name for format.
        // Retn: Nothing.
        ///////////////////////////////////////

        function formatAdd (psExtn, psName)
        {
            var sL, sU, sT, oL;

            // Form lower and upper case forms of extension
            sL = psExtn.toLowerCase ();
            sU = psExtn.toUpperCase ();

            // Form title
            sT = "Click to download ";
            if (psTitle !== "")
            {
                sT += "'" + psTitle + "' ";
            }
            sT += "in " + psName + " format...";

            // Create link
            oL = cbLink (sU, psFile + "." + sL, "target=_blank");
            oL.title = sT;

            // Add link to cell
            iFormats++;
            if (iFormats > 1)
            {
                oCell.innerHTML += " ";
            }
            oCell.appendChild (oL);
        }
        // End of formatAdd

        return (oCell);
    }
    // End of downLoad

    ////////////////////////////////////////////////
    // Priv: linkAdd
    // Desc: Add a hyperlink to a table (in a cell).
    // Args: psText -- Text for display.
    //       psFile -- Hyperlink file.
    //       ...    -- Extra attributes.
    // Retn: Cell reference.
    ////////////////////////////////////////////////

    function linkAdd ()
    {
        var oCell, oLink, atArg;

        // Get arguments
        atArg = cbArgs (arguments);
        var psText = atArg.shift ();
        var psFile = atArg.shift ();

        // Create hyperlink and put it into a cell
        oLink = cbLink (psText, psFile, atArg);
        oCell = cellAdd (oLink);

        return (oCell);
    }
    // End of linkAdd

    // Return references
    oTable.row      = rowAdd;
    oTable.cell     = cellAdd;
    oTable.number   = numberAdd;
    oTable.meter    = meterAdd;
    oTable.bar      = barAdd;
    oTable.downLoad = downLoad;
    oTable.link     = linkAdd;
    return (oTable);
}
// End of cbTable

/****************
* BAR FUNCTIONS *
****************/

///////////////////////////////////////////////
// Func: cbBar
// Desc: Create a progress bar.
// Args: iVal       -- Current value.
//       iMax       -- Maximum value.
//       iBarWidth  -- Width of bar in pixels.
//       iBarHeight -- Height of bar in pixels.
// Retn: Object reference.
///////////////////////////////////////////////

function cbBar (iVal, iMax)
{
    var fRatio, iWidth, sTitle, oT, oC;

    // Set up some defaults
    var iBarWidth  = (arguments.length > 2) ? arguments[2] : giCbMeterWidth;
    var iBarHeight = (arguments.length > 3) ? arguments[3] : giCbMeterHeight;

    // Create table to contain the bar
    sTitle =  cbStrNum (iVal, cbnum_CSEP) + "/";
    sTitle += cbStrNum (iMax, cbnum_CSEP);
    sTitle += " (" + cbStrNum (iMax - iVal, cbnum_CSEP) + " to go)";
    oT = cbTable ();
    oT.title = sTitle;
    oT.style.width  = iBarWidth  + "px";
    oT.style.height = iBarHeight + "px";
    oT.style.margin = "2px 0 0 0";
    oT.style.border = "1px solid black";
    oT.style.borderCollapse = "Collapse";
    oT.style.padding = "0";
    oT.row ();

    // Work out ratio
    if (iMax === 0)
    {
        fRatio = 0;
    }
    else
    {
        fRatio = iVal / iMax;
    }

    // Add cells where appropriate
    iWidth = Math.floor (fRatio * iBarWidth);
    if (iWidth > 0)
    {
        oC = oT.cell ();
        oC.style.margin = "0";
        oC.style.border = "1px solid Black";
        oC.style.padding = "0";
        oC.style.width  = iWidth + "px";
        oC.style.height = iBarHeight + "px";
        oC.style.backgroundColor = "DarkRed";
    }
    iWidth = iBarWidth - iWidth;
    if (iWidth > 0)
    {
        oC = oT.cell ();
        oC.style.margin = "0";
        oC.style.border = "1px solid Black";
        oC.style.padding = "0";
        oC.style.width  = iWidth + "px";
        oC.style.height = iBarHeight + "px";
    }

    // Return object
    return (oT);
}
// End of cbBar

//////////////////////////////
// Func: cbPercent
// Desc: Compute a percentage.
// Args: iVal -- Value.
//       iMax -- Maximum.
// Retn: Percentage.
//////////////////////////////

function cbPercent (iVal, iMax)
{
    var iPercent;

    if (iMax === 0)
    {
        iPercent = 0;
    }
    else
    {
        iPercent = Math.floor ((iVal * 100) / iMax);
    }

    return (iPercent);
}
// End of cbPercent

/********************
* LEGACY INTERFACES *
********************/

if (gbCbLegacyCode)
{
    var gbCbMeterStarted = false, gbCbMeter3D = 1;
    var giCbMeterStep = 4;
    var goMeterTable = null;
}

function cbLegacyCheck ()
{
    if (!gbCbLegacyCode)
    {
        throw ("Legacy code interface disabled.");
    }
}

function CbWrite (psText)
{
    cbLegacyCheck ();

    var iFlags = (arguments.length > 1) ? arguments[1] : 0;
    var oDoc   = (arguments.length > 2) ? arguments[2] : document;

    if (oDoc !== null)
    {
        oDoc.write (psText);
    }
    return (psText);
}

////////////////////////////////////////////////////////////////////
// Func: CbMeterSetSize
// Desc: Set the display dimensions for TableMeter function.
// Args: iWidth  -- Width of meters in pixels. Default 100.
//       iHeight -- Height in pixels. Default 12.
//       iStep   -- Size of 'pixels' in meter bars. Default 4.
//       b3D     -- Three dimensional look? Default yes.
////////////////////////////////////////////////////////////////////

function CbMeterSetSize (iWidth)
{
    cbLegacyCheck ();

    // Set the width
    if (iWidth >= 10)
    {
        giCbMeterWidth = iWidth;
    }

    // Set the height
    if (arguments.length > 1)
    {
        var iHeight = arguments[1];
        if (iHeight >= 4)
        {
            giCbMeterHeight = iHeight;
        }
    }

    // Set the step
    if (arguments.length > 2)
    {
        var iStep = arguments[2];
        if (iStep > 0)
        {
            giCbMeterStep = iStep;
        }
    }

    // Set the 3D look
    if (arguments.length > 3)
    {
        gbCbMeter3D = arguments[3];
    }
}

/////////////////////////////////////////////////////////////////
// Func: CbMeterStart
// Desc: Begin a new word meter table
// Args: psTitle -- Heading for titles (default "Title").
//       psValue -- Heading for current values (default "Count").
//       psMax   -- Heading for maxima (default "Expected").
//       psMeter -- Heading for meters (default "Meter").
/////////////////////////////////////////////////////////////////

function CbMeterStart ()
{
    var s, oC;

    // Write an element for we can find ourselves later
    s = "<p id=\"CbMeterLoad\">Loading...</p>";
    document.write (s);

    // Check for interface being available
    cbLegacyCheck ();

    // Deal with arguments
    var psTitle = (arguments.length > 0) ? arguments[0] : "Title";
    var psValue = (arguments.length > 1) ? arguments[1] : "Count";
    var psMax   = (arguments.length > 2) ? arguments[2] : "Expected";
    var psMeter = (arguments.length > 3) ? arguments[3] : "Meter";
    if (psMeter.slice (0, 1) == "@")
    {
        psMeter = psMeter.slice (2);
    }

    // Set size and styles
    CbMeterSetSize (0, 10);
    CbMeterStyle ();

    // Start table
    goMeterTable = cbTable ();
    goMeterTable.className = "meter";
    goMeterTable.row ("th", psTitle, psValue, psMax);
    oC = goMeterTable.cell ("th", psMeter);
    oC.colSpan = 2;

    gbCbMeterStarted = true;
}

///////////////////////////////////
// Func: CbMeterEnd
// Desc: Finish a word meter table.
// Args: None.
///////////////////////////////////

function CbMeterEnd ()
{
    cbLegacyCheck ();

    // Register end function
    cbOnLoad (meterEnd);

    gbCbMeterStarted = false;

    // Loader function
    function meterEnd ()
    {
        var oE, oP;

        // Find ourselves
        oE = document.getElementById ("CbMeterLoad");
        oP = oE.parentNode;
        
        // Remove loader message
        oP.removeChild (oE);

        // Render the meters
        oP.appendChild (goMeterTable);
    }
    
    return (goMeterTable);
}

///////////////////////////////////////////
// Func: CbMeterRow
// Desc: Render a new word meter table row.
// Args: psTitle -- Title of story.
//       iValue  -- Word count.
//       iMax    -- Expected maximum.
///////////////////////////////////////////

function CbMeterRow (psTitle, iValue, iMax)
{
    var iPercent, oBar, oC;

    cbLegacyCheck ();

    // Start table if necessary
    if (!gbCbMeterStarted)
    {
        CbMeterStart ();
    }

    // Percentage
    if (iMax === 0)
    {
        iPercent = 0;
    }
    else
    {
        iPercent = Math.floor ((iValue * 100) / iMax);
    }

    // New table row with given details
    goMeterTable.row ();
    oC = goMeterTable.cell (psTitle);
    oC = goMeterTable.number (iValue);
    oC.className = "metval";
    oC = goMeterTable.number (iMax);
    oC.className = "metval";
    oC = goMeterTable.cell (iPercent + "%");
    oC.className = "metval";
    oC.style.borderRight = "0";
    oBar = cbBar (iValue, iMax);
    oC = goMeterTable.cell (oBar);
    oC.className = "metval";
    oC.style.borderLeft = "0";
}

///////////////////////////////////////////////
// Func: CbMeterStyle
// Desc: Set the styles for the meter elements.
// Args: None.
///////////////////////////////////////////////

function CbMeterStyle ()
{
    cbLegacyCheck ();

    // TODO: Use proper style sheet?
    var s = "<style type=\"text/css\">\n<!--\n";
    s += "table.meter\n{\n";
    s += "  margin:           0;\n";
    s += "  border-top:       2px solid White;\n";
    s += "  border-bottom:    2px solid Black;\n";
    s += "  border-left:      2px solid LightGray;\n";
    s += "  border-right:     2px solid Gray;\n";
    s += "  border-collapse:  Collapse;\n";
    s += "  border-spacing:   0;\n";
    s += "  padding:          0;\n";
    s += "}\nth.meter, td.meter, td.metval\n{\n";
    s += "  margin:           0;\n";
    s += "  border:           1px solid Gray;\n";
    s += "  padding:          2px;\n";
    s += "  vertical-align:   Top;\n";
    s += "  font-family:      Tahoma;\n";
    s += "  font-size:        6pt;\n";
    s += "  text-align:       Left;\n";
    s += "}\nth.meter\n{\n";
    s += "  background-color: Maroon;\n";
    s += "  color:            #FFFF80;\n";
    s += "}\ntd.meter, td.metval\n{\n";
    s += "  background-color: DimGray;\n";
    s += "  color:            LightCyan;\n";
    s += "}\ntd.metval\n{\n";
    s += "  font-family:      Courier New;\n";
    s += "  text-align:       Right;\n}\n";
    s += "-->\n</style>\n";
    document.write (s);
}
// End of CbMeterStyle
/* End of file cb.js */
