// vim:et:sw=4:ts=4
// classes for parsing Netscape bookmark files, 2006.
// by Kevin Ko <kevin.s.ko@gmail.com>
// $Id: bookmarks.js,v 1.7 2006/08/06 18:03:31 scrabbly Exp $
//
// Licensed under the Creative Commons Attribution 2.5 License
//   http://creativecommons.org/licenses/by/2.5/
// (open use, as long as credit is given to the author)
//
function UrlEntry(title, summary, url, note, date, tags, folder)
{
    this.title = title;
    this.summary = summary;
    this.url = url;
    this.note = note;
    this.date = date;
    this.tags = tags;
    this.folder = folder;
}

UrlEntry.prototype = {
    toStr: function() {
        var s = "title: " + this.title + "<br>" +
                "summary: " + this.summary + "<br>" +
                "url: " + this.url + "<br>" +
                "note: " + this.note + "<br>" +
                "date: " + this.date.toDateString() + "<br>";
                "tags:";
        for (var i = 0; i < this.tags.length; i++) {
            s += " " + this.tags[i];
        }
        s += "<br>";
        return s;
    }
}

function BookmarkFolder(parentFolder, baseElem, name, desc)
{
    this.name = name;
    this.desc = desc;
    this.baseElem = baseElem;
    this.entries = new Array();
    this.parent = parentFolder;
    this.children = new Array();
    // build tags from nested folder names
    if (this.parent == null) {
        this.tags = this.name == "" ? [] : [this.name];
    } else {
        this.tags = this.name == "" ? this.parent.tags :
            this.parent.tags.concat([this.name]);
    }
}

BookmarkFolder.prototype = {
    toStr: function() {
        var i;
        var s = "<ul><h3>" + this.name + " - " + this.desc + "</h3>";
        for (i = 0; i < this.entries.length; i++) {
            s += "<li>" + this.entries[i].toStr() + "</li>";
        }
        for (i = 0; i < this.children.length; i++) {
            s += "<li>" + this.children[i].toStr() + "</li>";
        }
        s += "</ul>";
        return s;
    },
    /**
     * @return Array of all UrlEntry's
     */
    toArray: function() {
        var f, i;
        var urls = new Array();
        var q = new Array();
        q.push(this);
        while (q.length) {
            f = q.shift();
            // perhaps the following is too wasteful?
            urls = urls.concat(f.entries);
            q = q.concat(f.children);
        }
        return urls;
    }
}

/**
 * @param useTagCommas determine whether tags should be split by
 *                     commas or white-space
 */
function BookmarkParse(useTagCommas, useFoldersAsTags) 
{
    this.folderQ = new Array();
    this.init();
    this.useTagCommas = useTagCommas;
    this.useFoldersAsTags = useFoldersAsTags;
}

BookmarkParse.TOKEN_NONE = 0;
BookmarkParse.TOKEN_DL = 1;
BookmarkParse.TOKEN_DT = 2;
BookmarkParse.TOKEN_DD = 3;
BookmarkParse.TOKEN_H3 = 4;
BookmarkParse.TOKEN_A = 5;

BookmarkParse.prototype = {
    init: function() {
        this.topFolder = new BookmarkFolder(null, null, "", "");
        this.currFolder = this.topFolder;
    },
    /**
     * @private
     * @param base the DOM element to evaluate; assumed not null.
     */
    toToken: function(base) {
        if (base.nodeType == Node.ELEMENT_NODE) {
            switch (base.nodeName) {
                case "DL":
                    return BookmarkParse.TOKEN_DL;
                case "DT":
                    return BookmarkParse.TOKEN_DT;
                case "DD":
                    return BookmarkParse.TOKEN_DD;
                case "H3":
                    return BookmarkParse.TOKEN_H3;
                case "A":
                    return BookmarkParse.TOKEN_A;
                default:
                    break;
            }
        }
        return BookmarkParse.TOKEN_NONE;
    },
    /**
     * Find next bookmark-related tag at the current depth, base inclusive.
     * Returns BookmarkParse.TOKEN_NONE only if it cannot find a 
     * bookmark-related token. 
     *
     * @private
     * @param base starting DOM element
     */
    nextToken: function(base) {
        var i, t;
        for (i = base; i; i = i.nextSibling) {
            t = this.toToken(i);
            if (t != BookmarkParse.TOKEN_NONE) {
                return {elem: i, token: t};
            }
        }
        return {elem: null, token: BookmarkParse.TOKEN_NONE};
    },
    parse: function(base) {
        this.init();
        var curr, f;
        // find start of bookmark (ie. <DL> tag)
        var t = this.nextToken(base);
        if (t.token == BookmarkParse.TOKEN_DL) {
            this.parseFolder(t.elem);
            // handle folders
            while (true) {
                f = this.folderQ.shift();
                if (!f) {
                    break;
                }
                this.currFolder = f;
                this.parseFolder(f.baseElem);
                f.parent.children.push(f);  // link
            }
        }
        return this.topFolder;
    },
    /**
     * @private
     * @param base starting <DL> tag for the folder (no checking)
     */
    parseFolder: function(base) {
        var curr, t;
        curr = base.firstChild;
        while (true) {
            // prep next tag for parseEntry
            t = this.nextToken(curr);
            if (t.token == BookmarkParse.TOKEN_NONE) {
                break;
            } else if (t.token == BookmarkParse.TOKEN_DT) { 
                curr = t.elem;
                curr = this.parseEntry(curr);
                if (!curr) {
                    break;
                }
            } else {
                throw new Error("unexpected data");
            }
        }
    },
    /**
     * @private
     * @param base <DT> element holding an <A> tag
     * @return next unseen DOM element at base's level or null
     */
    parseDTLink: function(base) {
        var curr = base;
        var c = curr.firstChild;
        var t = this.nextToken(c);
        if (t.token != BookmarkParse.TOKEN_A) {
            throw new Error("unexpected entry data; expected a link");
        }
        c = t.elem;
        var title = c.innerHTML;
        var date = c.getAttribute('add_date');
        if (date != "") {
            date = new Date(date*1000);
        } else {
            date = new Date();
        }
        var url = c.getAttribute('href');
        var tags = new Array();
        if (c.getAttribute('tags')) {
            tags = c.getAttribute('tags').split(",");
        }
        var note = "";
        var summary = "";
        // description should be in DT level
        t = this.nextToken(curr.nextSibling);
        if (t.token == BookmarkParse.TOKEN_DD) {
            var s = t.elem.innerHTML.replace(/\n/g, " ");
            var tagRE = /^(.*)\(tags\:\s*(.*)\s*\).*$/;
            var m = tagRE.exec(s);
            if (m) {
                if (m.length == 3) {
                    // handle tags and elipses
                    if (this.useTagCommas) {
                        tags = m[2].split(",");
                    } else {
                        // split by spaces
                        tags = m[2].split(/\s+/);
                    }
                    note = m[1].replace(/^\s*(.*)\s*\.\.\.\s*$/, "$1");
                } else {
                    note = m[1];
                }
                note = note.replace(/^\s*(.*)\s*$/, "$1");
            } else {
                note = t.elem.innerHTML;
            }
            curr = t.elem;
        }
        if (tags.length == 0 && this.useFoldersAsTags) {
            // build from folders
            tags = this.currFolder.tags;
        }
        var ue = new UrlEntry(title, summary, url, note, date, tags,
                this.currFolder);
        this.currFolder.entries.push(ue);
        return curr.nextSibling;
    },
    /**
     * one needs to be careful with the <DD> tag under Mozilla, since <DD> 
     * followed by <DL> places <DL> as a child of <DD>.
     * @private
     * @param base element starting a folder (<H3>, <DL> combination after the
     * <DT>)
     */
    parseDTFolder: function(base) {
        var curr = base;
        // possibly a folder
        var t = this.nextToken(curr);
        if (t.token != BookmarkParse.TOKEN_H3) {
            throw new Error("unexpected entry data; expected a folder");
        }
        curr = t.elem;
        // folder
        var name = curr.innerHTML;
        var desc = "";
        t = this.nextToken(curr.nextSibling);
        if (t.token == BookmarkParse.TOKEN_DL) {
            this.folderQ.push(new BookmarkFolder(this.currFolder, 
                        t.elem, name, desc));
            curr = t.elem;
        } else if (t.token == BookmarkParse.TOKEN_DD) {
            curr = t.elem;
            var fc = curr.firstChild;
            if (fc && fc.nodeType == Node.TEXT_NODE) {
                desc = fc.nodeValue;
                fc = fc.nextSibling;
            }
            // <DL> becomes a child of <DD>
            t = this.nextToken(fc);
            if (t.token == BookmarkParse.TOKEN_DL) {
                this.folderQ.push(new BookmarkFolder(this.currFolder, 
                            t.elem, name, desc));
            }
        }
        return curr.nextSibling;
    },
    /**
     * new folders are allocated and pushed onto a queue for later exploration.
     *
     * @private
     * @param base <DT> tag (no checking)
     * @return next unseen DOM element in level or null
     */
    parseEntry: function(base) {
        // first determine type of DT element (folder or link)
        if (base.firstChild) {
            return this.parseDTLink(base);
        } else {
            return this.parseDTFolder(base.nextSibling);
        }
    }
};

function BookmarkParseIE(useTagCommas, useFoldersAsTags)
{
    this.folderQ = new Array();
    this.init();
    this.useTagCommas = useTagCommas;
    this.useFoldersAsTags = useFoldersAsTags;
}

BookmarkParseIE.prototype = new BookmarkParse();
BookmarkParseIE.prototype.constructor = BookmarkParseIE;

/**
 * @param base <DT> level for folder
 */
BookmarkParseIE.prototype.parseDTFolder = function(base) 
{
    var curr = base;
    var cc = base.firstChild;
    // possibly a folder
    var t = this.nextToken(cc);
    if (t.token != BookmarkParse.TOKEN_H3) {
        throw new Error("unexpected entry data; expected a folder");
    }
    cc = t.elem;
    // this is folder
    var name = cc.innerHTML;
    var desc = "";

    // check for folder description at <DT> level
    t = this.nextToken(curr.nextSibling);
    if (t.token == BookmarkParse.TOKEN_DD) {
        curr = t.elem;
        if (curr.firstChild && curr.firstChild.nodeType == Node.TEXT_NODE) {
            desc = curr.firstChild.nodeValue;
        }
        // <DL> becomes a child of <DD>
        cc = curr.firstChild;
    }

    // get folder's <DL> element as either child of <DT> or <DD>
    t = this.nextToken(cc.nextSibling);
    if (t.token == BookmarkParse.TOKEN_DL) {
        this.folderQ.push(new BookmarkFolder(this.currFolder, 
                    t.elem, name, desc));
    }
    return curr.nextSibling;
}

BookmarkParseIE.prototype.parseEntry = function(base) 
{
    // first determine type of DT element (folder or link)
    if (base.firstChild) {
        var t = this.nextToken(base.firstChild);
        if (t.token == BookmarkParse.TOKEN_H3) {
            return this.parseDTFolder(base);
        } else if (t.token == BookmarkParse.TOKEN_A) {
            return this.parseDTLink(base);
        } 
    }
    throw new Error("unexpected entry: neither folder nor link");
}
