// ---------------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------------

// Default options
var options = {};
options.chkRegExp = false;
options.chkCaseSens = false;
options.txtUserName = "YourName";

// Starting up
function main()
{
    loadOptionsCookie();
    setupOptionsPanel();
    setupRegexp();
    hideMessage();
    refreshAll();
    var start = getTiddlerText("DefaultTiddlers");
    if(window.location.hash)
        displayTiddlers(null,convertUTF8ToUnicode(decodeURI(window.location.hash.substr(1))),1,null,null);
    else if(start)
        displayTiddlers(null,start,1,null,null);
}

// ---------------------------------------------------------------------------------
// Tiddler functions
// ---------------------------------------------------------------------------------

// Display several tiddlers from a list of space separated titles
function displayTiddlers(src,titles,state,highlightText,highlightCaseSensitive,slowly)
{
    var tiddlers = titles.split(" ");
    for(var t=tiddlers.length-1;t>=0;t--)
        displayTiddler(src,tiddlers[t],state,highlightText,highlightCaseSensitive,slowly);
}

// Display a tiddler with animation and scrolling, as though a link to it has been clicked on
//  src = source element object (eg link) for animation effects and positioning
//  title = title of tiddler to display
//  state = 0 is default or current state, 1 is read only and 2 is edittable
//  highlightText = text to highlight in the displayed tiddler
//  highlightCaseSensitive = flag for whether the highlight text is case sensitive
function displayTiddler(src,title,state,highlightText,highlightCaseSensitive,slowly)
{
    var place = document.getElementById("tiddlerDisplay");
    var after = findContainingTiddler(src); // Which tiddler this one will be positioned after
    var before;
    if(after == null)
        before = place.firstChild;
    else if(after.nextSibling)
        before = after.nextSibling;
    else
        before = null;
    var theTiddler = createTiddler(place,before,title,state,highlightText,highlightCaseSensitive);
    if(src)
        {
        theTiddler.style.opacity = 0;
        var floater = document.getElementById("floater");
        var floaterTitle = document.createTextNode(title);
        if(floater.firstChild)
            floater.replaceChild(floaterTitle,floater.firstChild);
        startZoomer(floater,src,theTiddler,slowly);
        }
}

// Create a tiddler if it doesn't exist (with no fancy animating)
//  place = parent element
//  before = node before which to create/move the tiddler
//  title = title of tiddler to display
//  state = 0 is default or current state, 1 is read only and 2 is edittable
//  highlightText = text to highlight in the displayed tiddler
//  highlightCaseSensitive = flag for whether the highlight text is case sensitive
function createTiddler(place,before,title,state,highlightText,highlightCaseSensitive)
{
    var theTiddler = createTiddlerSkeleton(place,before,title);
    createTiddlerTitle(title,highlightText,highlightCaseSensitive);
    var theViewer = document.getElementById("viewer" + title);
    var theEditor = document.getElementById("editor" + title);
    switch(state)
        {
        case 0:
            if(!theViewer && !theEditor)
                {
                createTiddlerToolbar(title,false);
                createTiddlerViewer(title,highlightText,highlightCaseSensitive);
                }
            break;
        case 1: // Viewer
            if(theViewer)
                theViewer.parentNode.removeChild(theViewer);
            if(theEditor)
                theEditor.parentNode.removeChild(theEditor);
            createTiddlerToolbar(title,false);
            createTiddlerViewer(title,highlightText,highlightCaseSensitive);
            break;
        case 2: // Editor
            if(!theEditor)
                {
                if(theViewer)
                    theViewer.parentNode.removeChild(theViewer);
                createTiddlerToolbar(title,true);
                createTiddlerEditor(title);
                }
            break;
        }
    return(theTiddler);
}

function createTiddlerSkeleton(place,before,title)
{
    var theTiddler = document.getElementById("tiddler" + title);
    if(!theTiddler)
        {
        theTiddler = createTiddlyElement(null,"div","tiddler" + title,"tiddler",null);
        theTiddler.onmouseover = onMouseOverTiddler;
        theTiddler.onmouseout = onMouseOutTiddler;
        theTiddler.ondblclick = onDblClickTiddler;
        var theInnerTiddler = createTiddlyElement(theTiddler,"div",null,"innerTiddler",null);
        var theTitle = createTiddlyElement(theInnerTiddler,"div","title" + title,"title",null);
        var theToolbar = createTiddlyElement(theInnerTiddler,"div","toolbar" + title,"toolbar", null);
        var theBody = createTiddlyElement(theInnerTiddler,"div","body" + title,"body",null);
        place.insertBefore(theTiddler,before);
        }
    return(theTiddler);
}

function createTiddlerTitle(title,highlightText,highlightCaseSensitive)
{
    var theTitle = document.getElementById("title" + title);
    if(theTitle)
        {
        removeChildren(theTitle);
        if(highlightText == "")
            highlightText = null;
        var highlightRegExp,highlightMatch;
        if(highlightText)
            {
            highlightRegExp = new RegExp(highlightText,highlightCaseSensitive ? "mg" : "img");
            highlightMatch = highlightRegExp.exec(title);
            }
        highlightMatch = subWikify(theTitle,title,0,title.length,highlightRegExp,highlightMatch);
        var subtitle = getTiddlerSubtitle(title);
        theTitle.title = subtitle;
        }
}

// Create a tiddler toolbar according to whether it's an editor or not
function createTiddlerToolbar(title,editor)
{
    var theToolbar = document.getElementById("toolbar" + title);
    if(theToolbar)
        {
        removeChildren(theToolbar);
        insertSpacer(theToolbar);
        if(!editor)
            {
            // Non-editor toolbar
            createTiddlyButton(theToolbar,"close","Close this tiddler",onClickToolbarClose);
            // insertSpacer(theToolbar);
            // createTiddlyButton(theToolbar,"edit","Edit this tiddler",onClickToolbarEdit);
            insertSpacer(theToolbar);
            createTiddlyButton(theToolbar,"permalink","Permalink for this tiddler",onClickToolbarPermaLink);
            insertSpacer(theToolbar);
            // createTiddlyButton(theToolbar,"references","Show tiddlers that link to this one",onClickToolbarBackLink);
            }
        else
            {
            // Editor toolbar
            createTiddlyButton(theToolbar,"done","Save changes to this tiddler",onClickToolbarSave);
            insertSpacer(theToolbar);
            createTiddlyButton(theToolbar,"cancel","Undo changes to this tiddler",onClickToolbarUndo);
            insertSpacer(theToolbar);
            createTiddlyButton(theToolbar,"delete","Delete this tiddler",onClickToolbarDelete);
            }
        insertSpacer(theToolbar);
        }
}

// Create the body section of a read-only tiddler
function createTiddlerViewer(title,highlightText,highlightCaseSensitive)
{
    var theBody = document.getElementById("body" + title);
    if(theBody)
        {
        var tiddlerText = getTiddlerText(title);
        var tiddlerExists = (tiddlerText != null);
        if(!tiddlerExists)
            tiddlerText = "I haven't written this part yet.";
        var theViewer = createTiddlyElement(theBody,"div","viewer" + title,"viewer",null);
        if(!tiddlerExists)
            theViewer.style.fontStyle = "italic";
        wikify(tiddlerText,theViewer,highlightText,highlightCaseSensitive);
        }
}

// Create the body section of an edittable tiddler
function createTiddlerEditor(title)
{
    var theBody = document.getElementById("body" + title);
    if(theBody)
        {
        var tiddlerText = getTiddlerText(title);
        var tiddlerExists = (tiddlerText != null);
        if(!tiddlerExists)
            tiddlerText = "Type the text for '" + title + "' here.";
        var theEditor = createTiddlyElement(theBody,"div","editor" + title,"editor",null);
        theEditor.onkeypress = onEditKey;
        var theTitleBox = createTiddlyElement(theEditor,"input","editorTitle" + title,null,null);
        theTitleBox.setAttribute("type","text");
        theTitleBox.value = title;
        theTitleBox.setAttribute("size","40");
        var theBodyBox = createTiddlyElement(theEditor,"textarea","editorBody" + title,null,null);
        theBodyBox.value = tiddlerText;
        theBodyBox.setAttribute("rows","10");
        theBodyBox.style.width = "100%";
        //theBodyBox.setAttribute("cols","80");
        theBodyBox.focus();
        }
}

function saveTiddler(title)
{
    var theNewTitle = document.getElementById("editorTitle" + title).value;
    var theNewBody = document.getElementById("editorBody" + title).value;
    var theExisting = document.getElementById("store" + title);
    if(theExisting)
        theExisting.parentNode.removeChild(theExisting);
    if(title != theNewTitle)
       {
       theExisting = document.getElementById("store" + theNewTitle);
       if(theExisting)
            theExisting.parentNode.removeChild(theExisting);
        }
    var place = document.getElementById("storeArea");
    var storeItem = createTiddlyElement(place,"div","store" + theNewTitle,null,escapeTiddler(theNewBody));
    var now = new Date();
    storeItem.setAttribute("modified",ConvertToYYYYMMDDHHMM(now));
    storeItem.setAttribute("modifier",options.txtUserName);
    displayTiddler(null,theNewTitle,1,null,null,null,false);
    // Close the old tiddler if this is a rename
    if(title != theNewTitle)
        {
        var oldTiddler = document.getElementById("tiddler" + title);
        oldTiddler.parentNode.removeChild(oldTiddler);
        }
    refreshAll();
}

function searchTiddlers(text,caseSensitive,useRegExp)
{
    var searchText;
    closeAllTiddlers();
    if (useRegExp)
        searchText = text;
    else
        searchText = escapeRegExp(text);
    if(document.getElementById("store" + text))
        displayTiddler(null,text,1,text,caseSensitive,false); // Special case of searching for a tiddler title
    var theTiddler = document.getElementById("tiddler" + text);
    var store = document.getElementById("storeArea").childNodes;
    var c = 0;
    var regExp = new RegExp(searchText,caseSensitive ? "m" : "im");
    for(var t = 0; t < store.length; t++)
        {
        var e = store[t];
        if(e.id)
            if(e.id.substr(0,5) == "store")
                {
                var tiddlerText = "";
                if(e.firstChild)
                    tiddlerText = unescapeTiddler(e.firstChild.nodeValue);
                if(regExp.exec(e.id.substr(5)) || regExp.exec(tiddlerText))
                    {
                    displayTiddler(null,e.id.substr(5),1,searchText,caseSensitive,false);
                    c++;
                    }
                }
        }
    var q = useRegExp ? "/" : "'";
    displayMessage(c + " tiddlers found matching " + q + text + q);
}

function selectTiddler(title)
{
    var e = document.getElementById("toolbar" + title);
    if(e != null)
        e.style.visibility = "visible";
}

function deselectTiddler(title)
{
    var e = document.getElementById("toolbar" + title);
    if(e != null)
        e.style.visibility = "hidden";
}

function deleteTiddler(title)
{
    closeTiddler(title,false);
    var tiddler = document.getElementById("store" + title);
    if(tiddler)
        tiddler.parentNode.removeChild(tiddler);
    refreshAll();
}

function closeTiddler(title,slowly)
{
    var tiddler = document.getElementById("tiddler" + title);
    if(tiddler != null)
        {
        scrubIds(tiddler);
        startSlider(tiddler,false,slowly,true);
        }
}

function scrubIds(e)
{
    if(e.id)
        e.id = null;
    var children = e.childNodes;
    for(var t=0; t<children.length; t++)
        {
        var c = children[t];
        if(c.id)
            c.id = null;
        }
}

function closeAllTiddlers()
{
    hideMessage();
    var place = document.getElementById("tiddlerDisplay");
    var tiddler = place.firstChild;
    var nextTiddler;
    while(tiddler)
        {
        nextTiddler = tiddler.nextSibling;
        if(tiddler.id)
            if(tiddler.id.substr(0,7) == "tiddler")
                {
                var title = tiddler.id.substr(7);
                if(!document.getElementById("editor" + title))
                    place.removeChild(tiddler);
                }
        tiddler = nextTiddler;
        }
}

// ---------------------------------------------------------------------------------
// Regular expression stuff
// ---------------------------------------------------------------------------------

var upperLetter = "[A-ZÀ-ß]";
var lowerLetter = "[a-zà-ÿ_0-9\\-]";
var anyLetter = "[A-Za-zÀ-ßà-ÿ_0-9\\-]";
var anyDigit = "[0-9]";
var anyNumberChar = "[0-9\\.E]";

var wikiNameRegExp;
var structurePatterns;
var stylePatterns;
var tableRegExp;
var tableRowColRegExp;
var invalidPreWikiNamePattern;

function setupRegexp()
{
    // Table rows pattern
    var rowPattern = "^\\|([^\\n]*\\|)([fhc]?)$";
    tableRegExp = new RegExp(rowPattern,"mg");
    // Table columns pattern
    var elementPattern = "(?:(?:BGCOLOR|bgcolor)\\(([^\\)]+)\\):)?([^\\|]*)\\|";
    tableRowColRegExp = new RegExp(elementPattern,"g");
    // Link patterns
    var wikiNamePattern = "(?:" + upperLetter + "+" + lowerLetter + "+" + upperLetter + anyLetter + "*)|(?:" + upperLetter + "{2,}" + lowerLetter + "+)";
    var urlPattern = "(?:http|https|mailto|ftp):[^\\s\"']*";
    var explicitLinkPattern = "\\[\\[([^\\[\\]\\|]+)\\|([^\\[\\]\\|]+)\\]\\]";
    var bracketNamePattern = "\\[\\[([^\\]]+)\\]\\]";
    var patterns = "(" + wikiNamePattern + 
        ")|(" + urlPattern + 
        ")|(?:" + explicitLinkPattern + 
        ")|(?:" + bracketNamePattern + 
        ")";
    wikiNameRegExp = new RegExp(patterns,"mg");
    invalidPreWikiNamePattern = anyLetter;
    // Structural patterns
    var breakPattern = "\\n";
    var horizontalRulePattern = "^----$\\n?";
    var headerPattern = "^!{1,5}";
    var bulletListItemPattern = "^\\*+";
    var numberedListItemPattern = "^#+";
    var tablePattern = "(?:^\\|[^\\n]*$\\n?)+";
    var blockquotePattern = "(?:^>[^\\n]*$\\n?)+";
    var blockquotePattern2 = "^<<<\\n((?:^[^\\n]*\\n)+)(^<<<$\\n?)";
    var imagePattern = "\\[[Ii][Mm][Gg]\\[(?:([^\\|]+)\\|)?([^\\[\\]\\|]+)\\]\\]";
    structurePatterns = "(" + breakPattern + 
        ")|(" + horizontalRulePattern + 
        ")|(" + headerPattern + 
        ")|(" + bulletListItemPattern + 
        ")|(" + numberedListItemPattern + 
        ")|(" + tablePattern + 
        ")|(" + blockquotePattern + 
        ")|(?:" + blockquotePattern2 + 
        ")|(?:" + imagePattern + 
        ")";
    // Style patterns
    var boldPattern = "''([^']+)''";
    var strikePattern = "==([^=]+)==";
    var underlinePattern = "__([^_]+)__";
    var italicPattern = "//([^/]+)//";
    var supPattern = "\\^\\^([^\\^]+)\\^\\^";
    var subPattern = "~~([^~]+)~~";
    var colorPattern = "@@(?:color\\(([^\\)]+)\\):|bgcolor\\(([^\\)]+)\\):){0,2}([^@]+)@@";
    stylePatterns = "(?:" + boldPattern + 
        ")|(?:" + strikePattern + 
        ")|(?:" + underlinePattern + 
        ")|(?:" + italicPattern + 
        ")|(?:" + supPattern + 
        ")|(?:" + subPattern + 
        ")|(?:" + colorPattern + 
        ")";
}

// Create child text nodes and link elements to represent a wiki-fied version of some text
function wikify(text,parent,highlightText,highlightCaseSensitive)
{
    // Prepare the regexp for the highlighted selection
    if(highlightText == "")
        highlightText = null;
    var highlightRegExp,highlightMatch;
    if(highlightText)
        {
        highlightRegExp = new RegExp(highlightText,highlightCaseSensitive ? "mg" : "img");
        highlightMatch = highlightRegExp.exec(text);
        }
    wikifyStructures(parent,text,text,0,text.length,highlightRegExp,highlightMatch);
}


function wikifyStructures(parent,text,targetText,startPos,endPos,highlightRegExp,highlightMatch)
{
    var body = parent;
    var structureRegExp = new RegExp(structurePatterns,"mg");
    var theList = new Array();  // theList[0]: don't use
    var isInListMode = false;
    var isInHeaderMode = false;
    var isNewline = false;
    // The start of the fragment of the text being considered
    var nextPos = 0;
    // Loop through the bits of the body text
    do {
        // Get the next formatting match
        var formatMatch = structureRegExp.exec(targetText);
        var matchPos = formatMatch ? formatMatch.index : targetText.length;
        // Subwikify the plain text before the match
        if(nextPos < matchPos)
            {
            isNewline = false;
            highlightMatch = wikifyStyles(body,text,targetText.substring(nextPos,matchPos),startPos+nextPos,startPos+matchPos,highlightRegExp,highlightMatch);
            }
        // Dump out the formatted match
        var level;
        var theBlockquote;
        if(formatMatch)
            {
            // Dump out the link itself in the appropriate format
            if(formatMatch[1])
                {
                if(isNewline && isInListMode)
                    {
                    theList = new Array();
                    body = parent;
                    isInListMode = false;
                    }
                else if(isInHeaderMode)
                    {
                    body = parent;
                    isInHeaderMode = false;
                    }
                else
                    {
                    isNewline = true;
                    body.appendChild(document.createElement("br"));
                    }
                }
            else if(formatMatch[2])
                {
                isNewline = false;
                body.appendChild(document.createElement("hr"));
                }
            else if(formatMatch[3])
                {
                level = formatMatch[3].length + 1;
                isNewline = false;
                isInHeaderMode = true;
                var theHeader = document.createElement("h" + level);
                parent.appendChild(theHeader);
                body = theHeader;
                }
            else if(formatMatch[4])
                {
                level = formatMatch[4].length;
                isNewline = false;
                isInListMode = true;
                if (theList[level] == null)
                    {
                    theList[level] = document.createElement("ul");
                    body.appendChild(theList[level]);
                    }
                theList = theList.slice(0,level + 1);
                body = document.createElement("li");
                theList[level].appendChild(body);
                }
            else if(formatMatch[5])
                {
                level = formatMatch[5].length;
                isNewline = false;
                isInListMode = true;
                if (theList[level] == null)
                    {
                    theList[level] = document.createElement("ol");
                    body.appendChild(theList[level]);
                    }
                theList = theList.slice(0,level + 1);
                body = document.createElement("li");
                theList[level].appendChild(body);
                }
            else if(formatMatch[6])
                {
                isNewline = false;
                highlightMatch = wikifyTable(body,text,formatMatch[6],startPos+matchPos,startPos+structureRegExp.lastIndex,highlightRegExp,highlightMatch);
                }
            else if(formatMatch[7])
                {
                isNewline = false;
                var quotedText = formatMatch[7].replace(new RegExp("^>(>*)","mg"),"$1");
                theBlockquote = document.createElement("blockquote");
                var newHighlightRegExp,newHighlightMatch;
                if (highlightRegExp) {
                    newHighlightRegExp = new RegExp(highlightRegExp.toString(), "img");
                    newHighlightMatch = newHighlightRegExp.exec(quotedText);
                }
                wikifyStructures(theBlockquote,quotedText,quotedText,0,quotedText.length,newHighlightRegExp,newHighlightMatch);
                body.appendChild(theBlockquote);
                }
            else if(formatMatch[8])
                {
                isNewline = false;
                theBlockquote = document.createElement("blockquote");
                highlightMatch = wikifyStructures(theBlockquote,text,formatMatch[8],startPos+matchPos+4,startPos+structureRegExp.lastIndex-formatMatch[9].length,highlightRegExp,highlightMatch);
                body.appendChild(theBlockquote);
                }
            else if(formatMatch[11])
                {
                isNewline = false;
                var theImage = document.createElement("img");
                theImage.alt = formatMatch[10];
                theImage.src = formatMatch[11];
                body.appendChild(theImage);
                }
            }
        // Move the next position past the formatting match
        nextPos = structureRegExp.lastIndex;
    } while(formatMatch);
    return highlightMatch;
}

function wikifyLinks(parent,text,targetText,startPos,endPos,highlightRegExp,highlightMatch)
{
    // The start of the fragment of the text being considered
    var nextPos = 0;
    // Loop through the bits of the body text
    var theLink;
    do {
        // Get the next formatting match
        var formatMatch = wikiNameRegExp.exec(targetText);
        var matchPos = formatMatch ? formatMatch.index : targetText.length;
        // Subwikify the plain text before the match
        if(nextPos < matchPos)
            highlightMatch = subWikify(parent,text,startPos+nextPos,startPos+matchPos,highlightRegExp,highlightMatch);
        // Dump out the formatted match
        if(formatMatch)
            {
            // Dump out the link itself in the appropriate format
            if(formatMatch[1])
                {
                if(matchPos > 0 && new RegExp(invalidPreWikiNamePattern,"").exec(targetText.charAt(matchPos - 1)))
                    {
                    theLink = parent;
                    }
                else
                    {
                     theLink = createTiddlyLink(parent,formatMatch[0],false);
                     }
                 highlightMatch = subWikify(theLink,text,startPos+matchPos,startPos+wikiNameRegExp.lastIndex,highlightRegExp,highlightMatch);
                }
            else if(formatMatch[2])
                {
                theLink = createExternalLink(parent,formatMatch[0]);
                highlightMatch = subWikify(theLink,text,startPos+matchPos,startPos+wikiNameRegExp.lastIndex,highlightRegExp,highlightMatch);
                }
            else if(formatMatch[3])
                {
                theLink = createExternalLink(parent,formatMatch[4]);
                highlightMatch = subWikify(theLink,text,startPos+matchPos+2,startPos+matchPos+2+formatMatch[3].length,highlightRegExp,highlightMatch);
                }
            else if(formatMatch[5])
                {
                theLink = createTiddlyLink(parent,formatMatch[5],false);
                highlightMatch = subWikify(theLink,text,startPos+matchPos+2,startPos+wikiNameRegExp.lastIndex-2,highlightRegExp,highlightMatch);
                }
            }
        // Move the next position past the formatting match
        nextPos = wikiNameRegExp.lastIndex;
    } while(formatMatch);
    return highlightMatch;
}

function wikifyStyles(parent,text,targetText,startPos,endPos,highlightRegExp,highlightMatch)
{
    var formatRegExp = new RegExp(stylePatterns,"mg");
    // The start of the fragment of the text being considered
    var nextPos = 0;
    // Loop through the bits of the body text
    do {
        // Get the next formatting match
        var formatMatch = formatRegExp.exec(targetText);
        var matchPos = formatMatch ? formatMatch.index : targetText.length;
        // Subwikify the plain text before the match
        if(nextPos < matchPos)
            highlightMatch = wikifyLinks(parent,text,targetText.substring(nextPos,matchPos),startPos+nextPos,startPos+matchPos,highlightRegExp,highlightMatch);
        // Dump out the formatted match
        if(formatMatch)
            {
            // Dump out the link itself in the appropriate format
            if(formatMatch[1])
                {
                var theBold = createTiddlyElement(parent,"b",null,null,null);
                highlightMatch = wikifyStyles(theBold,text,formatMatch[1],startPos+matchPos+2,startPos+formatRegExp.lastIndex-2,highlightRegExp,highlightMatch);
                }
            else if(formatMatch[2])
                {
                var theStrike = createTiddlyElement(parent,"strike",null,null,null);
                highlightMatch = wikifyStyles(theStrike,text,formatMatch[2],startPos+matchPos+2,startPos+formatRegExp.lastIndex-2,highlightRegExp,highlightMatch);
                }
            else if(formatMatch[3])
                {
                var theUnderline = createTiddlyElement(parent,"u",null,null,null);
                highlightMatch = wikifyStyles(theUnderline,text,formatMatch[3],startPos+matchPos+2,startPos+formatRegExp.lastIndex-2,highlightRegExp,highlightMatch);
                }
            else if(formatMatch[4])
                {
                var theItalic = createTiddlyElement(parent,"i",null,null,null);
                highlightMatch = wikifyStyles(theItalic,text,formatMatch[4],startPos+matchPos+2,startPos+formatRegExp.lastIndex-2,highlightRegExp,highlightMatch);
                }
            else if(formatMatch[5])
                {
                var theSup = createTiddlyElement(parent,"sup",null,null,null);
                highlightMatch = wikifyStyles(theSup,text,formatMatch[5],startPos+matchPos+2,startPos+formatRegExp.lastIndex-2,highlightRegExp,highlightMatch);
                }
            else if(formatMatch[6])
                {
                var theSub = createTiddlyElement(parent,"sub",null,null,null);
                highlightMatch = wikifyStyles(theSub,text,formatMatch[6],startPos+matchPos+2,startPos+formatRegExp.lastIndex-2,highlightRegExp,highlightMatch);
                }
            else if(formatMatch[9])
                {
                var theSpan;
                if (formatMatch[7] == "" && formatMatch[8] == "" )
                    {
                    theSpan = createTiddlyElement(parent,"span",null,"marked",null);
                    }
                    else
                    {
                    theSpan = createTiddlyElement(parent,"span",null,null,null);
                    if (formatMatch[7] != "") theSpan.style.color = formatMatch[7];
                    if (formatMatch[8] != "") theSpan.style.background = formatMatch[8];
                    }
                highlightMatch = wikifyStyles(theSpan,text,formatMatch[9],startPos+formatRegExp.lastIndex-2-formatMatch[9].length,startPos+formatRegExp.lastIndex-2,highlightRegExp,highlightMatch);
                }
            }
        // Move the next position past the formatting match
        nextPos = formatRegExp.lastIndex;
    } while(formatMatch);
    return highlightMatch;
}

// Create a table 
function wikifyTable(parent,text,targetText,startPos,endPos,highlightRegExp,highlightMatch)
{
    // The start of the fragment of the text being considered
    var nextPos = 0;
    var theTable = document.createElement("table");
    var bodyRowLen = 0;
    var headRowLen = 0;
    var footRowLen = 0;
    var bodyRows = new Array();
    var headRows = new Array();
    var footRows = new Array();
    var theCaption = null;
    // Loop through the bits of the body text
    do {
        // Get the next formatting match
        var formatMatch = tableRegExp.exec(targetText);
        var matchPos = formatMatch ? formatMatch.index : targetText.length;
        // Dump out the formatted match
        if(formatMatch) {
            if (formatMatch[2] == "c") {
                var cap = formatMatch[1].substring(0,formatMatch[1].length-1);
                theCaption = document.createElement("caption");
                highlightMatch = wikifyStyles(theCaption,text,cap,startPos+matchPos+1,startPos+cap.length,highlightRegExp,highlightMatch);
                if (bodyRowLen == 0 && headRowLen == 0 && footRowLen == 0) {
                    theCaption.setAttribute("align", "top");
                } else {
                    theCaption.setAttribute("align", "bottom");
                }
            } else if (formatMatch[2] == "h") {
                highlightMatch = wikifyTableRow(headRows,headRowLen,text,formatMatch[1],startPos+matchPos,startPos+matchPos+formatMatch[1].length,highlightRegExp,highlightMatch);
                headRowLen++;
            } else if (formatMatch[2] == "f") {
                highlightMatch = wikifyTableRow(footRows,footRowLen,text,formatMatch[1],startPos+matchPos,startPos+matchPos+formatMatch[1].length,highlightRegExp,highlightMatch);
                footRowLen++;
            } else {
                highlightMatch = wikifyTableRow(bodyRows,bodyRowLen,text,formatMatch[1],startPos+matchPos,startPos+matchPos+formatMatch[1].length,highlightRegExp,highlightMatch);
                bodyRowLen++;
            }
        }
        nextPos = tableRegExp.lastIndex;
    } while(formatMatch);
    
    if (theCaption != null) {
        theTable.appendChild(theCaption);
    }
    
    if (headRowLen > 0) {
        var theTableHead = document.createElement("thead");
        createTableRows(headRows,theTableHead);
        theTable.appendChild(theTableHead);
    }
    
    if (bodyRowLen > 0) {
        var theTableBody = document.createElement("tbody");
        createTableRows(bodyRows,theTableBody);
        theTable.appendChild(theTableBody);
    }
    
    if (footRowLen > 0) {
        var theTableFoot = document.createElement("tfoot");
        createTableRows(footRows,theTableFoot);
        theTable.appendChild(theTableFoot);
    }
    
    parent.appendChild(theTable);
    return highlightMatch;
}

function wikifyTableRow(rows,rowIndex,text,targetText,startPos,endPos,highlightRegExp,highlightMatch)
{
    // The start of the fragment of the text being considered
    var eIndex = 0;
    var elements = new Array();
    // Loop through the bits of the body text
    do {
        // Get the next formatting match
        var formatMatch = tableRowColRegExp.exec(targetText);
        var matchPos = formatMatch ? formatMatch.index : targetText.length;
        if(formatMatch) {
            var eText = formatMatch[2];
            if (eText == "~" || eText == ">") {
                elements[eIndex] = eText;
            } else {
                var eTextLen = eText.length;
                var align = "";
                if (eTextLen >= 1 && eText.charAt(0) == " ") {
                    if (eTextLen >= 3 && eText.charAt(eTextLen - 1) == " ") {
                        align = "center";
                        eText = eText.substring(1,eTextLen - 1);
                        //eTextLen -= 2;
                        eTextLen--;
                    } else {
                        align = "right";
                        eText = eText.substring(1);
                        eTextLen--;
                    }
                } else if (eTextLen >= 2 && eText.charAt(eTextLen - 1) == " ") {
                    align = "left";
                    eText = eText.substring(0,eTextLen - 1);
                    //eTextLen--;
                }
                
                var theElement;
                if (eTextLen >= 1 && eText.charAt(0) == "!") {
                    theElement = document.createElement("th");
                    eText = eText.substring(1);
                    eTextLen--;
                } else {
                    theElement = document.createElement("td");
                }
                
                if (align != "") {
                    theElement.align = align;
                }
                
                if (formatMatch[1]) {
                    theElement.style.background = formatMatch[1];
                }
                
                highlightMatch = wikifyStyles(theElement,text,eText,startPos+tableRowColRegExp.lastIndex-eTextLen,startPos+tableRowColRegExp.lastIndex-1,highlightRegExp,highlightMatch);
                elements[eIndex] = theElement;
            }
            eIndex++;
        }
    } while(formatMatch);
    rows[rowIndex] = elements;
    return highlightMatch;
}

function createTableRows(rows,parent)
{
    var i, j, k, cols;
    for (i = 0; i < rows.length; i++) {
        cols = rows[i];
        var theRow = document.createElement("tr");
        for (j = 0; j < cols.length; j++) {
            if (cols[j] == "~") continue;
            
            var rowspan = 1;
            for (k = i+1; k < rows.length; k++) {
                if (rows[k][j] != "~") break;
                rowspan++;
            }
            
            var colspan = 1;
            for (; j < cols.length - 1; j++) {
                if (cols[j] != ">") break;
                colspan++;
            }
            
            var theElement = cols[j];
            if (rowspan > 1) {
                theElement.setAttribute("rowspan",rowspan);
                theElement.valign = "center";
            }
            if (colspan > 1) theElement.setAttribute("colspan",colspan);
            theRow.appendChild(theElement);
        }
        parent.appendChild(theRow);
    }
}

// Helper for wikify that handles highlights within runs of text
function subWikify(parent,text,startPos,endPos,highlightRegExp,highlightMatch)
{
    // Check for highlights
    while(highlightMatch && (highlightRegExp.lastIndex > startPos) && (highlightMatch.index < endPos) && (startPos < endPos))
        {
        // Deal with the plain text before the highlight
        if(highlightMatch.index > startPos)
            {
            parent.appendChild(document.createTextNode(text.substring(startPos,highlightMatch.index)));
            startPos = highlightMatch.index;
            }
        // Deal with the highlight
        var highlightEnd = Math.min(highlightRegExp.lastIndex,endPos);
        var theHighlight = createTiddlyElement(parent,"span",null,"highlight",text.substring(startPos,highlightEnd));
        startPos = highlightEnd;
        // Nudge along to the next highlight if we're done with this one
        if(startPos >= highlightRegExp.lastIndex)
            highlightMatch = highlightRegExp.exec(text);
        }
    // Do the unhighlighted text left over
    if(startPos < endPos)
        {
        parent.appendChild(document.createTextNode(text.substring(startPos,endPos)));
        //startPos = endPos;
        }
    return(highlightMatch);
}

// ---------------------------------------------------------------------------------
// Tiddler-related utility functions
// ---------------------------------------------------------------------------------

function getTiddlerText(title)
{
    var tiddlerStore = document.getElementById("store" + title);
    if(tiddlerStore == null)
        return(null);
    else if(tiddlerStore.firstChild)
        {
        if(tiddlerStore.firstChild)
            return(unescapeTiddler(tiddlerStore.firstChild.nodeValue));
        else
            return("");
        }
    else
        return("");
}

var regexpBackSlashEn = new RegExp("\\\\n","mg");
var regexpSingleBackSlash = new RegExp("\\\\","mg");
var regexpDoubleBackSlash = new RegExp("\\\\\\\\","mg");
var regexpNewLine = new RegExp("\n","mg");

// Convert "\n" to newlines
function unescapeTiddler(text)
{
    return(text.replace(regexpBackSlashEn,"\n").replace(regexpSingleBackSlash,"\\\\"));
}

// Convert newlines to "\n"
function escapeTiddler(text)
{
    return(text.replace(regexpDoubleBackSlash,"\\").replace(regexpNewLine,"\\n"));
}

function getTiddlerSubtitle(title)
{
    var tiddlerStore = document.getElementById("store" + title);
    if(tiddlerStore != null)
        {
        var theModifier = tiddlerStore.getAttribute("modifier");
        if(!theModifier)
            theModifier = "(unknown)";
        var theModified = tiddlerStore.getAttribute("modified");
        if(theModified)
            theModified = ConvertFromYYYYMMDDHHMM(theModified).toLocaleString();
        else
            theModified = "(unknown)";
        return(theModifier + ", " + theModified);
        }
    else
        return(null);
}

function createTiddlyElement(theParent,theElement,theID,theClass,theText)
{
    var e = document.createElement(theElement);
    if(theClass != null)
        e.className = theClass;
    if(theID != null)
        e.setAttribute("id",theID);
    if(theText != null)
        e.appendChild(document.createTextNode(theText));
    if(theParent != null)
        theParent.appendChild(e);
    return(e);
}

function createTiddlyButton(theParent,theText,theTooltip,theAction)
{
    var theButton = document.createElement("a");
    if(theAction)
        {
        theButton.onclick = theAction;
        theButton.setAttribute("href","JavaScript:;");
        }
    theButton.setAttribute("title",theTooltip);
    if(theText)
        {
        theButton.appendChild(document.createTextNode(theText));
        }
    theParent.appendChild(theButton);
    return(theButton);
}

function createTiddlyLink(place,title,includeText)
{
    var text = includeText ? title : null;
    var subTitle = getTiddlerSubtitle(title);
    var theClass = subTitle ? "tiddlyLinkExisting" : "tiddlyLinkNonExisting";
    if(!subTitle)
        subTitle = title + " doesn't yet exist";
    var btn = createTiddlyButton(place,text,subTitle,onClickTiddlerLink);
    btn.className = theClass;
    btn.setAttribute("tiddlyLink",title);
    return(btn);
}

function createExternalLink(place,url)
{
    var theLink = document.createElement("a");
    theLink.className = "externalLink";
    theLink.href = url;
    theLink.title = "Go to " + url;
    theLink.target = "_top";
    place.appendChild(theLink);
    return(theLink);
}

// Find the tiddler instance (if any) containing a specified element
function findContainingTiddler(e)
{
    if(e == null)
        return(null);
    do {
        if(e != document)
            {
            if(e.id)
                if(e.id.substr(0,7) == "tiddler")
                    return(e);
            }
        e = e.parentNode;
    } while(e != document);
    return(null);
}

function displayMessage(text)
{
    var msgArea = document.getElementById("messageArea");
    var msg = document.createTextNode(text);
    msgArea.replaceChild(msg,msgArea.firstChild);
    msgArea.style.display = "block";
}

function hideMessage()
{
    var msgArea = document.getElementById("messageArea");
    msgArea.style.display = "none";
}

// ---------------------------------------------------------------------------------
// Menu and sidebar functions
// ---------------------------------------------------------------------------------

var currentTab; // The id of the currently selected tab

function refreshAll()
{
    refreshHeader();
    refreshMenu();
    refreshSidebar();
}

// Refresh all parts of the header
function refreshHeader()
{
    var theTitle = getTiddlerText("SiteTitle");
    if(!theTitle) theTitle = "SiteTitle";
    var theSubtitle = getTiddlerText("SiteSubtitle");
    if(!theSubtitle) theSubtitle = "SiteSubtitle";
    document.title = theTitle + " - " + theSubtitle;
    var place = document.getElementById("siteTitle");
    while(place.firstChild != null)
        place.removeChild(place.firstChild);
    wikify(theTitle,place,null,null);
    place = document.getElementById("siteSubtitle");
    while(place.firstChild != null)
        place.removeChild(place.firstChild);
    wikify(theSubtitle,place,null,null);
}

function refreshMenu()
{
    var place = document.getElementById("mainMenu");
    while(place.firstChild != null)
        place.removeChild(place.firstChild);
    var menu = getTiddlerText("MainMenu");
    if(!menu) menu = "MainMenu";
    wikify(menu,place,null,null);
}

function refreshSidebar()
{
    var tabContent = document.getElementById("sidebarContent");
    switch(currentTab)
        {
            case "tabTimeline":
                tabContent.className = "tabContentTimeline";
                refreshTabTimeline();
                break;
            case "tabAll":
                tabContent.className = "tabContentAll";
                refreshTabAll();
                break;
            default:
                tabContent.className = "tabContentTimeline";
                refreshTabTimeline();
                break;
        }
}

function refreshTabTimeline()
{
    var allTiddlers = new Array(); // An array of 2-entry arrays, where entry 0 = name, 1 = date
    var storeNodes = document.getElementById("storeArea").childNodes;
    for (var t = 0; t < storeNodes.length; t++)
        {
        var n = storeNodes[t];
        if(n.id)
            if(n.id.substr(0,5) == "store")
                allTiddlers.push(new Array(n.id.substr(5),n.getAttribute("modified")));
        }
    allTiddlers.sort(function (a,b) { if(a[1] == b[1]) return(1); else return (a[1] < b[1]) ? +1 : -1; }); // Sort by date
    var place = document.getElementById("sidebarContent");
    while(place.firstChild != null)
        place.removeChild(place.firstChild);
    var lastDay = "";
    for (t = 0; t < allTiddlers.length; t++)
        {
        var theDay = allTiddlers[t][1].substr(0,8);
        if(theDay != lastDay)
            {
            var theDateElement = document.createElement("span");
            var theDateCaption = ConvertFromYYYYMMDDHHMM(allTiddlers[t][1]).toLocaleDateString();
            theDateElement.appendChild(document.createTextNode(theDateCaption));
            theDateElement.className = "sidebarSubHeading";
            place.appendChild(theDateElement);
            place.appendChild(document.createElement("br")); 
            lastDay = theDay;
            }
        place.appendChild(document.createTextNode(String.fromCharCode(160)));
        place.appendChild(document.createTextNode(String.fromCharCode(160)));
        createTiddlyLink(place,allTiddlers[t][0],true);
        place.appendChild(document.createElement("br"));
        }
}

function refreshTabAll()
{
    var allTiddlers = new Array(); // An array of 2-entry arrays, where entry 0 = name, 1 = date
    var storeNodes = document.getElementById("storeArea").childNodes;
    for (var t = 0; t < storeNodes.length; t++)
        {
        var n = storeNodes[t];
        if(n.id)
            if(n.id.substr(0,5) == "store")
                allTiddlers.push(new Array(n.id.substr(5),n.getAttribute("modified")));
        }
    allTiddlers.sort(function (a,b) { if(a[0] == b[0]) return(0); else return (a[0] > b[0]) ? +1 : -1; }); // Sort by name
    var place = document.getElementById("sidebarContent");
    while(place.firstChild != null)
        place.removeChild(place.firstChild);
    for (t = 0; t < allTiddlers.length; t++)
        {
        createTiddlyLink(place,allTiddlers[t][0],true);
        place.appendChild(document.createElement("br"));
        }
}

// ---------------------------------------------------------------------------------
// Options stuff
// ---------------------------------------------------------------------------------

function setupOptionsPanel()
{
    for(var opt in options)
        {
        var e = document.getElementById(opt);
        switch(opt.substr(0,3))
            {
            case "txt":
                e.value = options[opt];
                break;
            case "chk":
                e.checked = options[opt];
                break;
            }
        }
}

function loadOptionsCookie()
{
    var cookies = document.cookie;
    if(cookies.length > 0)
        for(var opt in options)
            {
            var n = opt + "=";
            var p = cookies.indexOf(n);
            if(p > -1)
                {
                p += n.length;
                var e = cookies.indexOf(";",p);
                if(e == -1)
                    e = cookies.length;
                var v = cookies.substr(p,e-p);
                switch(opt.substr(0,3))
                    {
                    case "txt":
                        options[opt] = v;
                        break;
                    case "chk":
                        options[opt] = v == "true";
                        break;
                    }
                }
            }
}

function saveOptionCookie(name)
{
    var c = name + "=";
    switch(name.substr(0,3))
        {
        case "txt":
            c += escape(options[name].toString());
            break;
        case "chk":
            c += options[name] ? "true" : "false";
            break;
        }
    c += "; expires=Fri, 1 Jan 2038 12:00:00 UTC; path=/";
    document.cookie = c;
}

function onChangeOption(e)
{
    if (!e) var e = window.event;
    var opt = resolveTarget(e);
    if(opt.id)
        {
        switch(opt.id.substr(0,3))
            {
            case "txt":
                options[opt.id] = opt.value;
                break;
            case "chk":
                options[opt.id] = opt.checked;
                break;
            }
        saveOptionCookie(opt.id);
        changeOption(opt.id);
        }
    return(true);
}

// React to an option having been changed
function changeOption(name)
{
    switch(name)
        {
        case "chkRegExp":
            break;
        case "chkCaseSens":
            break;
        case "txtUserName":
            break;
        }
}

// ---------------------------------------------------------------------------------
// Saving
// ---------------------------------------------------------------------------------

var startSaveArea = '<div id="' + 'storeArea">'; // Split up into two so that indexOf() of this source doesn't find it
var endSaveArea = '</d' + 'iv>';

// Save this tiddlywiki with the pending changes
function saveChanges()
{
    // Get the URL of the document
    var originalPath = document.location.toString();
    // Check we were loaded from a file URL
    if(originalPath.substr(0,5) != "file:")
        {
        alert("You need to save this TiddlyWiki to a file before you can save changes");
        displayTiddler(null,"SaveChanges",0,null,null,null);
        return;
        }
    // Remove any location part of the URL
    var hashPos = originalPath.indexOf("#");
    if(hashPos != -1)
        originalPath = originalPath.substr(0,hashPos);
    // Convert to a native file format assuming
    // "file:///x:/path/path/path..." - pc local file --> "x:\path\path\path..."
    // "file://server/share/path/path/path..." - pc network file --> "\\server\share\path\path\path..."
    // "file:///path/path/path..." - mac/unix local file --> "/path/path/path..."
    var localPath;
    if(originalPath.charAt(9) == ":") // pc local file
        localPath = unescape(originalPath.substr(8)).replace(new RegExp("/","g"),"\\");
    else if(originalPath.charAt(7) == "/") // mac/unix local file
        localPath = unescape(originalPath.substr(7));
    else // pc network file
        localPath = "\\\\" + unescape(originalPath.substr(7)).replace(new RegExp("/","g"),"\\");
    // Create backup filename
    var backupPath = localPath.substr(0,localPath.lastIndexOf(".")) + "." + ConvertToYYYYMMDDHHMMSSMMM(new Date()) + ".html";
    var original = loadFile(localPath);
    if(original == null)
        {
        alert("It's not possible to save changes using this browser. Use FireFox if you can");
        displayTiddler(null,"SaveChanges",0,null,null,null);
        return;
        }
    else
        {
        var posOpeningDiv = original.indexOf(startSaveArea);
        var posClosingDiv = original.lastIndexOf(endSaveArea);
        if((posOpeningDiv == -1) || (posClosingDiv == -1))
            alert("Doesn't appear to be a TiddlyWiki");
        else
            {
            var backup = saveFile(backupPath,original);
            var revised = original.substr(0,posOpeningDiv + startSaveArea.length) + convertUnicodeToUTF8(allTiddlersAsHtml()) + original.substr(posClosingDiv);
            var save = saveFile(localPath,revised)
            if(save)
                alert("Saved successfully as '" + localPath + "'");
            else
                alert("Error while saving");
            }
        }
}

function allTiddlersAsHtml()
{
    var storeNodes = document.getElementById("storeArea").childNodes;
    var savedTiddlers = new Array();
    for (var t = 0; t < storeNodes.length; t++)
        {
        var n = storeNodes[t];
        if(n.id)
            if(n.id.substr(0,5) == "store")
                {
                var title = n.id.substr(5);
                var text = "";
                if(n.firstChild)
                    text = n.firstChild.nodeValue;
                savedTiddlers.push('<div id="store' + title + '" modified="' +
                                    n.getAttribute("modified") + '" modifier="' + n.getAttribute("modifier") + '">' +
                                    text + '</div>');
                }
        }
    return savedTiddlers.join("\n");
}

// UTF-8 encoding rules:
// 0x0000 - 0x007F:	0xxxxxxx
// 0x0080 - 0x07FF:	110xxxxx 10xxxxxx
// 0x0800 - 0xFFFF:	1110xxxx 10xxxxxx 10xxxxxx

function convertUTF8ToUnicode(u)
{
    var s = "";
    var t = 0;
    var b1, b2, b3;
    while(t < u.length)
        {
        b1 = u.charCodeAt(t++);
        if(b1 < 0x80)
            s += String.fromCharCode(b1);
        else if(b1 < 0xE0)
            {
            b2 = u.charCodeAt(t++);
            s += String.fromCharCode(((b1 & 0x1F) << 6) | (b2 & 0x3F));
            }
        else
            {
            b2 = u.charCodeAt(t++);
            b3 = u.charCodeAt(t++);
            s += String.fromCharCode(((b1 & 0xF) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F));
            }
    }
    return(s);
}

function convertUnicodeToUTF8(s)
{
    var u = "";
    for(var t=0;t<s.length;t++)
        {
        var c = s.charCodeAt(t);
        if(c <= 0x7F) 
            u += String.fromCharCode(c);
        else if(c <= 0x7FF)
            {
            u += String.fromCharCode((c >> 6) | 0xC0);
            u += String.fromCharCode((c & 0x3F) | 0x80);
            }
        else
            {
            u += String.fromCharCode((c >> 12) | 0xE0);
            u += String.fromCharCode(((c >> 6) & 0x3F) | 0x80);
            u += String.fromCharCode((c & 0x3F) | 0x80);
            }
        }
    return(u);
}

function saveFile(fileUrl, content)
{
    var r = mozillaSaveFile(fileUrl, content);
    if((r == null) || (r == false))
        r = ieSaveFile(fileUrl, content);
    return(r);
}

function loadFile(fileUrl)
{
    var r = mozillaLoadFile(fileUrl);
    if((r == null) || (r == false))
        r = ieLoadFile(fileUrl);
    return(r);
}

// Returns null if it can't do it, false if there's an error
function ieSaveFile(filePath, content)
{
    try
        {
        var fso = new ActiveXObject("Scripting.FileSystemObject");
        }
    catch(e)
        {
        //alert("Exception while attempting to save\n\n" + e.toString());
        return(null);
        }
    var file = fso.OpenTextFile(filePath,2,-1,0);
    file.Write(content);
    file.Close();
    return(true);
}

// Returns null if it can't do it, false if there's an error, or a string of the content if successful
function ieLoadFile(filePath)
{
    try
        {
        var fso = new ActiveXObject("Scripting.FileSystemObject");
        }
    catch(e)
        {
        //alert("Exception while attempting to load\n\n" + e.toString());
        return(null);
        }
    var file = fso.OpenTextFile(filePath,1);
    var content = file.ReadAll();
    file.Close();
    return(content);
}

// Returns null if it can't do it, false if there's an error
function mozillaSaveFile(filePath, content)
{
    if(window.Components)
        try
            {
            netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
            var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
            file.initWithPath(filePath);
            if (!file.exists())
                file.create(0, 0664);
            var out = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream);
            out.init(file, 0x20 | 0x02, 00004,null);
            out.write(content, content.length);
            out.flush();
            out.close();
            return(true);
            }
        catch(e)
            {
            //alert("Exception while attempting to save\n\n" + e);
            return(false);
            }
    return(null);
}
    
// Returns null if it can't do it, false if there's an error, or a string of the content if successful
function mozillaLoadFile(filePath)
{
    if(window.Components)
        try 
            {
            netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
            var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
            file.initWithPath(filePath);
            if (!file.exists())
                return(null);
            var inputStream = Components.classes["@mozilla.org/network/file-input-stream;1"].createInstance(Components.interfaces.nsIFileInputStream);
            inputStream.init(file, 0x01, 00004, null);
            var sInputStream = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream);
            sInputStream.init(inputStream);
            return(sInputStream.read(sInputStream.available()));
            }
        catch(e)
            {
            //alert("Exception while attempting to load\n\n" + e);
            return(false);
            }
    return(null);
}

// ---------------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------------

function onEditKey(e)
{
    if (!e) var e = window.event;
    hideMessage();
    var consume = false;
    switch(e.keyCode)
        {
        case 13: // Ctrl-Enter
        case 10: // Ctrl-Enter on IE PC
        case 77: // Ctrl-Enter is "M" on some platforms
            if(e.ctrlKey && this.id && this.id.substr(0,6) == "editor")
                {
                saveTiddler(this.id.substr(6));
                consume = true;
                }
            break;
        case 27: // Escape
            if(this.id && this.id.substr(0,6) == "editor")
                {
                displayTiddler(null,this.id.substr(6),1,null,null,false);
                consume = true;
                }
            break;
        }
    e.cancelBubble = consume;
    if(consume)
        if (e.stopPropagation) e.stopPropagation();
    return(!consume);

}

// Event handler for clicking on a tiddly link
function onClickTiddlerLink(e)
{
    if (!e) var e = window.event;
    var theTarget = resolveTarget(e);
    var theLink = theTarget;
    var title = null;
    do {
        title = theLink.getAttribute("tiddlyLink");
        theLink = theLink.parentNode;
    } while(title == null && theLink != null);
    if(title)
        displayTiddler(theTarget,title,0,null,null,e.shiftKey || e.altKey);
    hideMessage();
}

// Event handler for mouse over a tiddler
function onMouseOverTiddler(e)
{
    var tiddler;
    if(this.id.substr(0,7) == "tiddler")
        tiddler = this.id.substr(7);
    if(tiddler)
        selectTiddler(tiddler);
}

// Event handler for mouse out of a tiddler
function onMouseOutTiddler(e)
{
    var tiddler;
    if(this.id.substr(0,7) == "tiddler")
        tiddler = this.id.substr(7);
    if(tiddler)
        deselectTiddler(tiddler);
}

// Event handler for double click on a tiddler
function onDblClickTiddler(e)
{
    hideMessage();
    if(document.selection)
        document.selection.empty();
    var tiddler;
    if(this.id.substr(0,7) == "tiddler")
        tiddler = this.id.substr(7);
    if(tiddler)
        displayTiddler(null,tiddler,2,null,null,false);
}

// Event handler for clicking on toolbar close
function onClickToolbarClose(e)
{
    if (!e) var e = window.event;
    hideMessage();
    if(this.parentNode.id)
        closeTiddler(this.parentNode.id.substr(7),e.shiftKey || e.altKey);
    e.cancelBubble = true;
    if (e.stopPropagation) e.stopPropagation();
    return(false);
}

// Event handler for clicking on toolbar permalink
function onClickToolbarPermaLink(e)
{
    if(this.parentNode.id)
        window.location.hash = encodeURIComponent(this.parentNode.id.substr(7));
}

// Event handler for clicking on toolbar close
function onClickToolbarDelete(e)
{
    hideMessage();
    if(this.parentNode.id)
        deleteTiddler(this.parentNode.id.substr(7));
}

// Event handler for clicking on the toolbar backlink
function onClickToolbarBackLink(e)
{
    hideMessage();
    if(this.parentNode.id)
        searchTiddlers(this.parentNode.id.substr(7),true,false);
}

// Event handler for clicking on toolbar close
function onClickToolbarEdit(e)
{
    hideMessage();
    if(this.parentNode.id)
        displayTiddler(null,this.parentNode.id.substr(7),2,null,null,false);
}

// Event handler for clicking on toolbar save
function onClickToolbarSave(e)
{
    if(this.parentNode.id)
        saveTiddler(this.parentNode.id.substr(7));
}

// Event handler for clicking on toolbar save
function onClickToolbarUndo(e)
{
    if(this.parentNode.id)
        displayTiddler(null,this.parentNode.id.substr(7),1,null,null,false);
}

var optionsOpen = false;

// Options button
function onClickOptions(e)
{
    if (!e) var e = window.event;
    var thePanel = document.getElementById("optionsPanel");
    optionsOpen = !optionsOpen;
    startSlider(thePanel,optionsOpen,e.shiftKey || e.altKey,false);
    e.cancelBubble = true;
    if (e.stopPropagation) e.stopPropagation();
    return(false);
}

// Event handler for clicking on a tab
function onClickTab(e)
{
    if (!e) var e = window.event;
    var theTab = resolveTarget(e);
    currentTab = theTab.id;
    refreshSidebar();
}

var lastSearchText = ""; // For keeping track of when the search text actually changes

function onSearch(e)
{
    var text = document.getElementById("searchText").value;
    if((text.length > 2) && (text != lastSearchText))
        {
        searchTiddlers(text,options.chkCaseSens,options.chkRegExp);
        lastSearchText = text;
        }
}

function onClearSearch()
{
    var text = document.getElementById("searchText");
    text.value = "";
    hideMessage();
}

// Eek... it's bad that this is done via a function rather than a normal, copy-able href
function onClickPermaView()
{
    var tiddlerDisplay = document.getElementById("tiddlerDisplay");
    var openTiddlers = "";
    var separator = "";
    for(var t=0;t<tiddlerDisplay.childNodes.length;t++)
        {
        openTiddlers = openTiddlers + separator + tiddlerDisplay.childNodes[t].id.substr(7);
        separator = " ";
        }
    window.location.hash = encodeURIComponent(openTiddlers);
}

// ---------------------------------------------------------------------------------
// Animation engine
// ---------------------------------------------------------------------------------

// Animation housekeeping
var animating = 0; // Incremented at start of each animation, decremented afterwards. If zero, the interval timer is disabled
var animaterID; // ID of the timer used for animating
// 'zoomer' module of the animation engine smoothly moves an element from the position/size of the start element to the target element
var zoomerElement = null; // Element being shifted; null if none
var zoomerStart; // Where we're shifting from
var zoomerTarget; // Where we're shifting to
var zoomerStartScroll; // Where we're scrolling from
var zoomerTargetScroll; // Where we're scrolling to
var zoomerProgress; // 0..1 of how far we are
var zoomerStep; // 0..1 of how much to shift each step
// 'slider' module of the animation engine slides an element smoothly open or closed
var sliderElement = null; // Element being slid open or closed; null if none
var sliderDeleteAfterwards;
var sliderOpening; // True if the element is being opened
var sliderRealHeight; // Starting height
var sliderProgress; // 0..1 of how far we are
var sliderStep; // 0..1 of how much to shift each step

// Start animation engine
function startAnimating()
{
    if(animating++ == 0)
        animaterID = window.setInterval("doAnimate();",25);
}

// Stop animation engine
function stopAnimating()
{
    if(--animating == 0)
        window.clearInterval(animaterID)
}

// Perform an animation engine tick, calling each of the known animation modules
function doAnimate()
{
    if(zoomerElement)
        doZoomer();
    if(sliderElement)
        doSlider();
}

// Start moving the element 'e' from the position of the element 'start' to the position of the element 'target'
function startZoomer(e,start,target,slowly)
{
    stopZoomer();
    zoomerElement = e;
    zoomerStart = start;
    zoomerStartScroll = findScrollY();
    zoomerTargetScroll = ensureVisible(target);
    zoomerTarget = target;
    zoomerProgress = 0;
    zoomerStep = slowly ? 0.01 : 0.12;
    startAnimating();
}

// Stop any ongoing zoomer animation
function stopZoomer()
{
    if(zoomerElement)
        {
        stopAnimating();
        zoomerElement.style.visibility = "hidden";
        zoomerTarget.style.opacity = 1;
        window.scrollTo(0,zoomerTargetScroll);
        zoomerElement = null;
        }
}

// Perform a tick of the zoomer animation
function doZoomer()
{
    zoomerProgress += zoomerStep;
    if(zoomerProgress >= 1.0)
        stopZoomer();
    else
        {
        var f = slowInSlowOut(zoomerProgress);
        var zoomerStartLeft = findPosX(zoomerStart);
        var zoomerStartTop = findPosY(zoomerStart);
        var zoomerStartWidth = zoomerStart.offsetWidth;
        var zoomerStartHeight = zoomerStart.offsetHeight;
        var zoomerTargetLeft = findPosX(zoomerTarget);
        var zoomerTargetTop = findPosY(zoomerTarget);
        var zoomerTargetWidth = zoomerTarget.offsetWidth;
        var zoomerTargetHeight = zoomerTarget.offsetHeight;
        zoomerElement.style.left = zoomerStartLeft + (zoomerTargetLeft-zoomerStartLeft) * f;
        zoomerElement.style.top = zoomerStartTop + (zoomerTargetTop-zoomerStartTop) * f;
        zoomerElement.style.width = zoomerStartWidth + (zoomerTargetWidth-zoomerStartWidth) * f;
        zoomerElement.style.height = zoomerStartHeight + (zoomerTargetHeight-zoomerStartHeight) * f;
        zoomerElement.style.visibility = "visible";
        zoomerTarget.style.opacity = zoomerProgress;
        window.scrollTo(0,zoomerStartScroll + (zoomerTargetScroll-zoomerStartScroll) * f);
        }
}

// Start sliding an element
function startSlider(e,opening,slowly,deleteAfterwards)
{
    stopSlider();
    sliderDeleteAfterwards = deleteAfterwards;
    sliderElement = e;
        sliderElement.style.display = "block";
    sliderElement.style.height = "auto";
    sliderRealHeight = sliderElement.offsetHeight;
    sliderOpening = opening;
    sliderStep = slowly ? 0.01 : 0.16;
    if(opening)
        {
        sliderProgress = 0;
        sliderElement.style.height = "2px";
        sliderElement.style.display = "block";
        }
    else
        {
        sliderStep = -sliderStep;
        sliderProgress = 1;
        }
    sliderElement.style.overflow = "hidden";
    startAnimating();
}

// Stop sliding the current element
function stopSlider()
{
    if(sliderElement)
        {
        stopAnimating();
        if(sliderOpening)
            sliderElement.style.height = "auto";
        else
            {
            if(sliderDeleteAfterwards)
                sliderElement.parentNode.removeChild(sliderElement);
            else
                sliderElement.style.display = "none";
            }
        sliderElement = null;
        }
}

// Perform a tick of the slider animation
function doSlider()
{
    sliderProgress += sliderStep;
    if((sliderProgress < 0) || (sliderProgress > 1.0))
        stopSlider();
    else
        {
        var f = slowInSlowOut(sliderProgress);
        var h = sliderRealHeight*f;
        sliderElement.style.height = (h <= 2) ? 2 : h;
        sliderElement.style.opacity = f;
        }
}

// ---------------------------------------------------------------------------------
// Standalone utility functions
// ---------------------------------------------------------------------------------

// Escape a string to ensure it doesn't have any RegExp special characters
function escapeRegExp(s)
{
    // Escape any special characters with that character preceded by a backslash
    return(s.replace(new RegExp("[\\\\\\^\\$\\*\\+\\?\\(\\)\\=\\!\\|\\,\\{\\}\\[\\]\\.]","g"),"\\$0"));
}

// Return a date in UTC YYYYMMDDHHMM format
function ConvertToYYYYMMDDHHMM(d)
{
    return(zeroPad(d.getFullYear(),4) + zeroPad(d.getMonth()+1,2) + zeroPad(d.getDate(),2) + zeroPad(d.getHours(),2) + zeroPad(d.getMinutes(),2));
}

// Left-pad a string with 0s to a certain width
function zeroPad(n,d)
{
    var s = n.toString();
    if(s.length < d)
        s = "000000000000000000000000000".substr(0,d-s.length) + s;
    return(s);
}

// Return a date in UTC YYYYMMDD.HHMMSSMMM format
function ConvertToYYYYMMDDHHMMSSMMM(d)
{
    return(zeroPad(d.getFullYear(),4) + zeroPad(d.getMonth()+1,2) + zeroPad(d.getDate(),2) + "." + zeroPad(d.getHours(),2) + zeroPad(d.getMinutes(),2) + zeroPad(d.getSeconds(),2) + zeroPad(d.getMilliseconds(),4));
}

// Convert a date in UTC YYYYMMDDHHMM format to date type
function ConvertFromYYYYMMDDHHMM(d)
{
    var theDate = new Date(parseInt(d.substr(0,4),10),
                            parseInt(d.substr(4,2),10)-1,
                            parseInt(d.substr(6,2),10),
                            parseInt(d.substr(8,2),10),
                            parseInt(d.substr(10,2),10),0,0);
    return(theDate);
}

// Map a 0..1 value to 0..1, but slow down at the start and end
function slowInSlowOut(progress)
{
    return(1-((Math.cos(progress * Math.PI)+1)/2));
}

// Resolve the target object of an event
function resolveTarget(e)
{
    var obj;
    if (e.target)
        obj = e.target;
    else if (e.srcElement)
        obj = e.srcElement;
    if (obj.nodeType == 3) // defeat Safari bug
        obj = obj.parentNode;
    return(obj);
}

// Get the scroll position for window.scrollTo necessary to scroll a given element into view
function ensureVisible(e)
{
    var posTop = findPosY(e);
    var posBot = posTop + e.offsetHeight;
    var winTop = findScrollY();
    var winHeight = findWindowHeight();
    var winBot = winTop + winHeight;
    if(posTop < winTop)
        return(posTop);
    else if(posBot > winBot)
        {
        if(e.offsetHeight < winHeight)
            return(posTop - (winHeight - e.offsetHeight));
        else
            return(posTop);
        }
    else
        return(winTop);
}

function findWindowHeight()
{
    return(window.innerHeight ? window.innerHeight : document.body.clientHeight);
}

function findScrollY()
{
    return(window.scrollY ? window.scrollY : document.body.scrollTop);
}

// From QuirksMode.com
function findPosX(obj)
{
    var curleft = 0;
    if (obj.offsetParent)
    {
        while (obj.offsetParent)
        {
            curleft += obj.offsetLeft;
            obj = obj.offsetParent;
        }
    }
    else if (obj.x)
        curleft += obj.x;
    return curleft;
}

// From QuirksMode.com
function findPosY(obj)
{
    var curtop = 0;
    if (obj.offsetParent)
    {
        while (obj.offsetParent)
        {
            curtop += obj.offsetTop;
            obj = obj.offsetParent;
        }
    }
    else if (obj.y)
        curtop += obj.y;
    return curtop;
}

// Create a non-breaking space
function insertSpacer(place)
{
    place.appendChild(document.createTextNode(String.fromCharCode(160)));
}

function removeChildren(e)
{
    while(e.hasChildNodes())
        e.removeChild(e.firstChild);
}

// ---------------------------------------------------------------------------------
// End of scripts
// ---------------------------------------------------------------------------------