/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is ChatZilla.
 *
 * The Initial Developer of the Original Code is
 * New Dimensions Consulting, Inc.
 * Portions created by the Initial Developer are Copyright (C) 1999
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Robert Ginda, rginda@ndcico.com, original author
 *   Samuel Sieb, samuel@sieb.net, MIRC color codes
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

/* This file contains the munger functions and rules used by ChatZilla.
 * It's generally a bad idea to call munger functions inside ChatZilla for
 * anything but munging (chat) output.
 */

function initMunger()
{
    /* linkRE: the general URL linkifier regular expression:
     *
     * - start with whitespace, non-word, or begining-of-line
     * - then match:
     *   - EITHER scheme (word + hyphen), colon, then lots of non-whitespace
     *   - OR "www" followed by at least 2 sets of:
     *     - "." plus some non-whitespace, non-"." characters
     * - must end match with a word-break
     * - include a "/" or "=" beyond break if present
     * - end with whitespace, non-word, or end-of-line
     */
    client.linkRE =
        /(?:\s|\W|^)((?:(\w[\w-]+):[^\s]+|www(\.[^.\s]+){2,})\b[\/=\)]?)(?=\s|\W|$)/;

    // Colours: \x03, with optional foreground and background colours
    client.colorRE = /(\x03((\d{1,2})(,\d{1,2}|)|))/;

    const LOW_PRIORITY = 5;
    const NORMAL_PRIORITY = 10;
    const HIGH_PRIORITY = 15;
    const HIGHER_PRIORITY = 20;

    var munger = client.munger = new CMunger(insertHyphenatedWord);
    // Special internal munger!
    munger.addRule(".inline-buttons", /(\[\[.*?\]\])/, insertInlineButton,
                   10, 5, false);
    munger.addRule("quote", /(``|'')/, insertQuote,
                   NORMAL_PRIORITY, NORMAL_PRIORITY);
    munger.addRule("bold", /(?:[\s(\[]|^)(\*[^*()]*\*)(?:[\s\]).,;!\?]|$)/,
                   "chatzilla-bold", NORMAL_PRIORITY, NORMAL_PRIORITY);
    munger.addRule("underline", /(?:[\s(\[]|^)(\_[^_()]*\_)(?:[\s\]).,;!\?]|$)/,
                   "chatzilla-underline", NORMAL_PRIORITY, NORMAL_PRIORITY);
    munger.addRule("italic", /(?:\s|^)(\/[^\/()]*\/)(?:[\s.,]|$)/,
                   "chatzilla-italic", NORMAL_PRIORITY, NORMAL_PRIORITY);
    /* allow () chars inside |code()| blocks */
    munger.addRule("teletype", /(?:\s|^)(\|[^|]*\|)(?:[\s.,]|$)/,
                   "chatzilla-teletype", NORMAL_PRIORITY, NORMAL_PRIORITY);
    munger.addRule(".mirc-colors", client.colorRE, mircChangeColor,
                   NORMAL_PRIORITY, NORMAL_PRIORITY);
    munger.addRule(".mirc-bold", /(\x02)/, mircToggleBold,
                   NORMAL_PRIORITY, NORMAL_PRIORITY);
    munger.addRule(".mirc-underline", /(\x1f)/, mircToggleUnder,
                   NORMAL_PRIORITY, NORMAL_PRIORITY);
    munger.addRule(".mirc-color-reset", /(\x0f)/, mircResetColor,
                   NORMAL_PRIORITY, NORMAL_PRIORITY);
    munger.addRule(".mirc-reverse", /(\x16)/, mircReverseColor,
                   NORMAL_PRIORITY, NORMAL_PRIORITY);
    munger.addRule("ctrl-char", /([\x01-\x1f])/, showCtrlChar,
                   NORMAL_PRIORITY, NORMAL_PRIORITY);
    munger.addRule("link", client.linkRE, insertLink, NORMAL_PRIORITY, HIGH_PRIORITY);

    // This has a higher starting priority so as to get it to match before the
    // normal link, which won't know about mailto and then fail.
    munger.addRule(".mailto",
       /(?:\s|\W|^)((mailto:)?[^:;\\<>\[\]()\'\"\s\u201d]+@[^.<>\[\]()\'\"\s\u201d]+\.[^<>\[\]()\'\"\s\u201d]+)/i,
                   insertMailToLink, NORMAL_PRIORITY, HIGHER_PRIORITY, false);
    munger.addRule("bugzilla-link",
                   /(?:\s|\W|^)(bug\s+(?:#?\d+|#[^\s,]{1,20})(?:\s+comment\s+#?\d+)?)/i,
                   insertBugzillaLink, NORMAL_PRIORITY, NORMAL_PRIORITY);
    munger.addRule("channel-link",
                /(?:\s|\W|^)[@%+]?(#[^<>,\[\](){}\"\s\u201d]*[^:,.<>\[\](){}\'\"\s\u201d])/i,
                   insertChannelLink, NORMAL_PRIORITY, NORMAL_PRIORITY);
    munger.addRule("talkback-link", /(?:\W|^)(TB\d{8,}[A-Z]?)(?:\W|$)/,
                   insertTalkbackLink, NORMAL_PRIORITY, NORMAL_PRIORITY);

    munger.addRule("face",
         /((^|\s)(?:[>]?[B8=:;(xX][~']?[-^v"]?(?:[)|(PpSs0oO\?\[\]\/\\]|D+)|>[-^v]?\)|[oO9][._][oO9])(\s|$))/,
         insertSmiley, NORMAL_PRIORITY, NORMAL_PRIORITY);
    munger.addRule("rheet", /(?:\s|\W|^)(rhee+t\!*)(?:\s|$)/i, insertRheet, 10, 10);
    munger.addRule("word-hyphenator",
                   new RegExp ("(\\S{" + client.MAX_WORD_DISPLAY + ",})"),
                   insertHyphenatedWord, LOW_PRIORITY, NORMAL_PRIORITY);

    client.enableColors = client.prefs["munger.colorCodes"];
    var branch = client.prefManager.prefBranch;
    for (var entry in munger.entries)
    {
        if (!isinstance(munger.entries[entry], Object))
            continue;

        for (var rule in munger.entries[entry])
        {
            if (rule[0] == ".")
                continue;

            try
            {
                munger.entries[entry][rule].enabled =
                    branch.getBoolPref("munger." + rule);
            }
            catch (ex)
            {
                // nada
            }
        }
    }
}

function insertLink(matchText, containerTag, data, mungerEntry)
{
    var href;
    var linkText;

    var trailing;
    ary = matchText.match(/([.,?\)]+)$/);
    if (ary)
    {
        linkText = RegExp.leftContext;
        trailing = ary[1];
        
        // We special-case links that end with (something), often found on wikis
        // if "trailing" starts with ) and there's an unclosed ( in the
        // "linkText"; then we put the final ) back in
        if ((trailing.indexOf(")") == 0) && (linkText.match(/\([^\)]*$/)))
        {
            
            linkText += ")";
            trailing = trailing.substr(1);
        }
    }
    else
    {
        linkText = matchText;
    }

    var ary = linkText.match(/^(\w[\w-]+):/);
    if (ary)
    {
        if (!("schemes" in client))
        {
            var pfx = "@mozilla.org/network/protocol;1?name=";
            var len = pfx.length;

            client.schemes = new Object();
            for (var c in Components.classes)
            {
                if (c.indexOf(pfx) == 0)
                    client.schemes[c.substr(len)] = true;
            }
        }

        if (!(ary[1] in client.schemes))
        {
            mungerEntry.enabled = false;
            client.munger.munge(matchText, containerTag, data);
            mungerEntry.enabled = true;
            return;
        }

        href = linkText;
    }
    else
    {
        href = "http://" + linkText;
    }

    /* This gives callers to the munger control over URLs being logged; the
     * channel topic munger uses this, as well as the "is important" checker.
     * If either of |dontLogURLs| or |noStateChange| is present and true, we
     * don't log.
     */
    if ((!("dontLogURLs" in data) || !data.dontLogURLs) &&
        (!("noStateChange" in data) || !data.noStateChange))
    {
        var max = client.prefs["urls.store.max"];
        if (client.prefs["urls.list"].unshift(href) > max)
            client.prefs["urls.list"].pop();
        client.prefs["urls.list"].update();
    }

    var anchor = document.createElementNS(XHTML_NS, "html:a");
    var mircRE = /\x1f|\x02|\x0f|\x16|\x03([0-9]{1,2}(,[0-9]{1,2})?)?/g;
    anchor.setAttribute("href", href.replace(mircRE, ""));

    // Carry over formatting.
    var otherFormatting = calcClass(data);
    if (otherFormatting)
        anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
    else
        anchor.setAttribute("class", "chatzilla-link");

    anchor.setAttribute("target", "_content");
    mungerEntry.enabled = false;
    data.inLink = true;
    client.munger.munge(linkText, anchor, data);
    mungerEntry.enabled = true;
    delete data.inLink;
    containerTag.appendChild(anchor);
    if (trailing)
        insertHyphenatedWord(trailing, containerTag, data);

}

function insertMailToLink(matchText, containerTag, eventData, mungerEntry)
{
    if (("inLink" in eventData) && eventData.inLink)
    {
        mungerEntry.enabled = false;
        client.munger.munge(matchText, containerTag, eventData);
        mungerEntry.enabled = true;
        return;
    }

    var href;

    if (matchText.indexOf("mailto:") != 0)
        href = "mailto:" + matchText;
    else
        href = matchText;

    var anchor = document.createElementNS(XHTML_NS, "html:a");
    var mircRE = /\x1f|\x02|\x0f|\x16|\x03([0-9]{1,2}(,[0-9]{1,2})?)?/g;
    anchor.setAttribute("href", href.replace(mircRE, ""));

    // Carry over formatting.
    var otherFormatting = calcClass(eventData);
    if (otherFormatting)
        anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
    else
        anchor.setAttribute("class", "chatzilla-link");

    //anchor.setAttribute ("target", "_content");
    mungerEntry.enabled = false;
    eventData.inLink = true;
    client.munger.munge(matchText, anchor, eventData);
    mungerEntry.enabled = true;
    delete eventData.inLink;
    containerTag.appendChild(anchor);

}

function insertChannelLink(matchText, containerTag, eventData, mungerEntry)
{
    if (("inLink" in eventData) && eventData.inLink)
    {
        mungerEntry.enabled = false;
        client.munger.munge(matchText, containerTag, eventData);
        mungerEntry.enabled = true;
        return;
    }

    var bogusChannels =
        /^#(include|error|define|if|ifdef|else|elsif|endif)$/i;

    if (!("network" in eventData) || !eventData.network ||
        matchText.search(bogusChannels) != -1)
    {
        containerTag.appendChild(document.createTextNode(matchText));
        return;
    }

    var linkText = removeColorCodes(matchText);
    var encodedLinkText = fromUnicode(linkText, eventData.sourceObject);
    var anchor = document.createElementNS(XHTML_NS, "html:a");
    anchor.setAttribute("href", eventData.network.getURL(encodedLinkText));

    // Carry over formatting.
    var otherFormatting = calcClass(eventData);
    if (otherFormatting)
        anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
    else
        anchor.setAttribute("class", "chatzilla-link");

    mungerEntry.enabled = false;
    eventData.inLink = true;
    client.munger.munge(matchText, anchor, eventData);
    mungerEntry.enabled = true;
    delete eventData.inLink;
    containerTag.appendChild(anchor);
}

function insertTalkbackLink(matchText, containerTag, eventData, mungerEntry)
{
    if (("inLink" in eventData) && eventData.inLink)
    {
        mungerEntry.enabled = false;
        client.munger.munge(matchText, containerTag, eventData);
        mungerEntry.enabled = true;
        return;
    }

    var anchor = document.createElementNS(XHTML_NS, "html:a");

    anchor.setAttribute("href", "http://talkback-public.mozilla.org/" +
                        "search/start.jsp?search=2&type=iid&id=" + matchText);

    // Carry over formatting.
    var otherFormatting = calcClass(eventData);
    if (otherFormatting)
        anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
    else
        anchor.setAttribute("class", "chatzilla-link");

    mungerEntry.enabled = false;
    client.munger.munge(matchText, anchor, eventData);
    mungerEntry.enabled = true;
    containerTag.appendChild(anchor);
}

function insertBugzillaLink (matchText, containerTag, eventData, mungerEntry)
{
    if (("inLink" in eventData) && eventData.inLink)
    {
        mungerEntry.enabled = false;
        client.munger.munge(matchText, containerTag, eventData);
        mungerEntry.enabled = true;
        return;
    }

    var prefs = client.prefs;
    if (eventData.channel)
        prefs = eventData.channel.prefs;
    else if (eventData.network)
        prefs = eventData.network.prefs;

    var bugURL = prefs["bugURL"];
    var bugURLcomment = prefs["bugURL.comment"];

    if (bugURL.length > 0)
    {
        var idOrAlias = matchText.match(/bug\s+#?(\d+|[^\s,]{1,20})/i)[1];
        bugURL = bugURL.replace("%s", idOrAlias);

        if (matchText.indexOf("comment") != -1)
        {
            var commentNum = matchText.match(/comment\s+#?(\d+)/i)[1];
            /* If the comment is a complete URL, use only that, replacing %1$s
             * and %2$s with the bug number and comment number, respectively.
             * Otherwise, append the comment preference to the main one,
             * replacing just %s in each.
             */
            if (bugURLcomment.match(/^\w+:/))
            {
                bugURL = bugURLcomment;
                bugURL = bugURL.replace("%1$s", idOrAlias);
                bugURL = bugURL.replace("%2$s", commentNum);
            }
            else
            {
                bugURL += bugURLcomment.replace("%s", commentNum);
            }
        }

        var anchor = document.createElementNS(XHTML_NS, "html:a");
        anchor.setAttribute("href", bugURL);
        // Carry over formatting.
        var otherFormatting = calcClass(eventData);
        if (otherFormatting)
            anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
        else
            anchor.setAttribute("class", "chatzilla-link");

        anchor.setAttribute("target", "_content");
        mungerEntry.enabled = false;
        eventData.inLink = true;
        client.munger.munge(matchText, anchor, eventData);
        mungerEntry.enabled = true;
        delete eventData.inLink;
        containerTag.appendChild(anchor);
    }
    else
    {
        mungerEntry.enabled = false;
        client.munger.munge(matchText, containerTag, eventData);
        mungerEntry.enabled = true;
    }
}

function insertRheet(matchText, containerTag, eventData, mungerEntry)
{
    if (("inLink" in eventData) && eventData.inLink)
    {
        mungerEntry.enabled = false;
        client.munger.munge(matchText, containerTag, eventData);
        mungerEntry.enabled = true;
        return;
    }

    var anchor = document.createElementNS(XHTML_NS, "html:a");
    anchor.setAttribute("href",
                        "http://ftp.mozilla.org/pub/mozilla.org/mozilla/libraries/bonus-tracks/rheet.wav");
    anchor.setAttribute("class", "chatzilla-rheet chatzilla-link");
    //anchor.setAttribute ("target", "_content");
    insertHyphenatedWord(matchText, anchor, eventData);
    containerTag.appendChild(anchor);
}

function insertQuote (matchText, containerTag)
{
    if (matchText == "``")
        containerTag.appendChild(document.createTextNode("\u201c"));
    else
        containerTag.appendChild(document.createTextNode("\u201d"));
    containerTag.appendChild(document.createElementNS(XHTML_NS, "html:wbr"));
}

function insertSmiley(emoticon, containerTag)
{
    var type = "error";

    if (emoticon.search(/\>[-^v]?\)/) != -1)
        type = "face-alien";
    else if (emoticon.search(/\>[=:;][-^v]?[(|]/) != -1)
        type = "face-angry";
    else if (emoticon.search(/[=:;][-^v]?[Ss\\\/]/) != -1)
        type = "face-confused";
    else if (emoticon.search(/[B8][-^v]?[)\]]/) != -1)
        type = "face-cool";
    else if (emoticon.search(/[=:;][~'][-^v]?\(/) != -1)
        type = "face-cry";
    else if (emoticon.search(/o[._]O/) != -1)
        type = "face-dizzy";
    else if (emoticon.search(/O[._]o/) != -1)
        type = "face-dizzy-back";
    else if (emoticon.search(/o[._]o|O[._]O/) != -1)
        type = "face-eek";
    else if (emoticon.search(/\>[=:;][-^v]?D/) != -1)
        type = "face-evil";
    else if (emoticon.search(/[=:;][-^v]?DD/) != -1)
        type = "face-lol";
    else if (emoticon.search(/[=:;][-^v]?D/) != -1)
        type = "face-laugh";
    else if (emoticon.search(/\([-^v]?D|[xX][-^v]?D/) != -1)
        type = "face-rofl";
    else if (emoticon.search(/[=:;][-^v]?\|/) != -1)
        type = "face-normal";
    else if (emoticon.search(/[=:;][-^v]?\?/) != -1)
        type = "face-question";
    else if (emoticon.search(/[=:;]"[)\]]/) != -1)
        type = "face-red";
    else if (emoticon.search(/9[._]9/) != -1)
        type = "face-rolleyes";
    else if (emoticon.search(/[=:;][-^v]?[(\[]/) != -1)
        type = "face-sad";
    else if (emoticon.search(/[=:][-^v]?[)\]]/) != -1)
        type = "face-smile";
    else if (emoticon.search(/[=:;][-^v]?[0oO]/) != -1)
        type = "face-surprised";
    else if (emoticon.search(/[=:;][-^v]?[pP]/) != -1)
        type = "face-tongue";
    else if (emoticon.search(/;[-^v]?[)\]]/) != -1)
        type = "face-wink";

    if (type == "error")
    {
        // We didn't actually match anything, so it'll be a too-generic match
        // from the munger RegExp.
        containerTag.appendChild(document.createTextNode(emoticon));
        return;
    }

    var span = document.createElementNS(XHTML_NS, "html:span");

    /* create a span to hold the emoticon text */
    span.setAttribute ("class", "chatzilla-emote-txt");
    span.setAttribute ("type", type);
    span.appendChild (document.createTextNode (emoticon));
    containerTag.appendChild (span);

    /* create an empty span after the text.  this span will have an image added
     * after it with a chatzilla-emote:after css rule. using
     * chatzilla-emote-txt:after is not good enough because it does not allow us
     * to turn off the emoticon text, but keep the image.  ie.
     * chatzilla-emote-txt { display: none; } turns off
     * chatzilla-emote-txt:after as well.*/
    span = document.createElementNS(XHTML_NS, "html:span");
    span.setAttribute("class", "chatzilla-emote");
    span.setAttribute("type", type);
    span.setAttribute("title", emoticon);
    span.setAttribute("role", "image");
    containerTag.appendChild(span);

}

function mircChangeColor (colorInfo, containerTag, data)
{
    /* If colors are disabled, the caller doesn't want colors specifically, or
     * the caller doesn't want any state-changing effects, we drop out.
     */
    if (!client.enableColors ||
        (("noMircColors" in data) && data.noMircColors) ||
        (("noStateChange" in data) && data.noStateChange))
    {
        return;
    }

    var ary = colorInfo.match (/.(\d{1,2}|)(,(\d{1,2})|)/);

    // Do we have a BG color specified...?
    if (!arrayHasElementAt(ary, 1) || !ary[1])
    {
        // Oops, no colors.
        delete data.currFgColor;
        delete data.currBgColor;
        return;
    }

    var fgColor = String(Number(ary[1]) % 16);

    if (fgColor.length == 1)
        data.currFgColor = "0" + fgColor;
    else
        data.currFgColor = fgColor;

    // Do we have a BG color specified...?
    if (arrayHasElementAt(ary, 3) && ary[3])
    {
        var bgColor = String(Number(ary[3]) % 16);

        if (bgColor.length == 1)
            data.currBgColor = "0" + bgColor;
        else
            data.currBgColor = bgColor;
    }

    data.hasColorInfo = true;
}

function mircToggleBold (colorInfo, containerTag, data)
{
    if (!client.enableColors ||
        (("noMircColors" in data) && data.noMircColors) ||
        (("noStateChange" in data) && data.noStateChange))
    {
        return;
    }

    if ("isBold" in data)
        delete data.isBold;
    else
        data.isBold = true;
    data.hasColorInfo = true;
}

function mircToggleUnder (colorInfo, containerTag, data)
{
    if (!client.enableColors ||
        (("noMircColors" in data) && data.noMircColors) ||
        (("noStateChange" in data) && data.noStateChange))
    {
        return;
    }

    if ("isUnderline" in data)
        delete data.isUnderline;
    else
        data.isUnderline = true;
    data.hasColorInfo = true;
}

function mircResetColor (text, containerTag, data)
{
    if (!client.enableColors ||
        (("noMircColors" in data) && data.noMircColors) ||
        (("noStateChange" in data) && data.noStateChange) ||
        !("hasColorInfo" in data))
    {
        return;
    }

    delete data.currFgColor;
    delete data.currBgColor;
    delete data.isBold;
    delete data.isUnderline;
    delete data.hasColorInfo;
}

function mircReverseColor (text, containerTag, data)
{
    if (!client.enableColors ||
        (("noMircColors" in data) && data.noMircColors) ||
        (("noStateChange" in data) && data.noStateChange))
    {
        return;
    }

    var tempColor = ("currFgColor" in data ? data.currFgColor : "");

    if ("currBgColor" in data)
        data.currFgColor = data.currBgColor;
    else
        delete data.currFgColor;
    if (tempColor)
        data.currBgColor = tempColor;
    else
        delete data.currBgColor;
    data.hasColorInfo = true;
}

function showCtrlChar(c, containerTag)
{
    var span = document.createElementNS(XHTML_NS, "html:span");
    span.setAttribute("class", "chatzilla-control-char");
    if (c == "\t")
    {
        containerTag.appendChild(document.createTextNode(c));
        return;
    }

    var ctrlStr = c.charCodeAt(0).toString(16);
    if (ctrlStr.length < 2)
        ctrlStr = "0" + ctrlStr;
    span.appendChild(document.createTextNode("0x" + ctrlStr));
    containerTag.appendChild(span);
    containerTag.appendChild(document.createElementNS(XHTML_NS, "html:wbr"));
}

function insertHyphenatedWord(longWord, containerTag, data)
{
    var wordParts = splitLongWord(longWord, client.MAX_WORD_DISPLAY);
    var newClass = "";
    if (data && ("hasColorInfo" in data))
        newClass = calcClass(data);

    var wbr = document.createElementNS(XHTML_NS, "html:wbr");
    for (var i = 0; i < wordParts.length; ++i)
    {
        if (newClass)
        {
            var newTag = document.createElementNS(XHTML_NS, "html:span");
            newTag.setAttribute("class", newClass);
            newTag.appendChild(document.createTextNode(wordParts[i]));
            containerTag.appendChild(newTag);
        }
        else
        {
            delete data.hasColorInfo;
            containerTag.appendChild(document.createTextNode(wordParts[i]));
        }
        containerTag.appendChild(wbr.cloneNode(true));
    }
}

function insertInlineButton(text, containerTag, data)
{
    var ary = text.match(/\[\[([^\]]+)\]\[([^\]]+)\]\[([^\]]+)\]\]/);

    if (!ary)
    {
        containerTag.appendChild(document.createTextNode(text));
        return;
    }

    var label = ary[1];
    var title = ary[2];
    var command = ary[3];

    var link = document.createElementNS(XHTML_NS, "html:a");
    link.setAttribute("href", "x-cz-command:" + encodeURI(command));
    link.setAttribute("title", title);
    link.setAttribute("class", "chatzilla-link");
    link.appendChild(document.createTextNode(label));

    containerTag.appendChild(document.createTextNode("["));
    containerTag.appendChild(link);
    containerTag.appendChild(document.createTextNode("]"));
}

function calcClass(data)
{
    var className = "";
    if ("hasColorInfo" in data)
    {
        if ("currFgColor" in data)
            className += " chatzilla-fg" + data.currFgColor;
        if ("currBgColor" in data)
            className += " chatzilla-bg" + data.currBgColor;
        if ("isBold" in data)
            className += " chatzilla-bold";
        if ("isUnderline" in data)
            className += " chatzilla-underline";
    }
    return className;
}

