toloframework
Version: 
Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.
625 lines (596 loc) • 16.3 kB
JavaScript
/**
 * A HtmlNode has the following attributes:
 * * __type__:
 *   * __VOID__, __EMPTY__: this is a fake node, it will just aggregate nodes in the __children__ attribute.
 *   * __TAG__, __ELEMENT__: an element of the DOM.
 *   * __TEXT__: a text without any tag arround it.
 * * __name__: the name of the element if this node is a DOM element.
 * * __attribs__: an object of all the element attributes, if this node is a DOM element.
 * * __text__: the content of the text element if this node is a text element.
 * * __children__: an array of the children nodes.
 * * __extra__: an object for free information addition.
 *
 * @module node
 */
var id = 1;
/**
 * Not a real element. Just a list of elements.
 * Must contain `children`.
 */
exports.ROOT = 0;
exports.VOID = 0;
/**
 * DOM element tag.
 * Has an attribute *name*.
 * @const
 */
exports.TAG = 1;
exports.ELEMENT = 1;
/**
 * HTML text node.
 * Has an attribute *text*.
 * @const
 */
exports.TEXT = 2;
/**
 * HTML CDATA section.
 * @example
 * <![CDATA[This a is a CDATA section...]]>
 * @const
 */
exports.CDATA = 3;
/**
 * @example
 * <?xml-stylesheet href="default.css" title="Default style"?>
 */
exports.PROCESSING = 4;
/**
 * HTML comment.
 * @example
 * <-- This is a comment -->
 * @const
 */
exports.COMMENT = 5;
/**
 * @const
 */
exports.DOCTYPE = 6;
/**
 * @const
 */
exports.TYPE = 7;
/**
 * Example: `&`, `<`, ...
 */
exports.ENTITY = 8;
/**
 * Put the `text` attribute verbatim, without any transformation, nor parsing.
 */
exports.VERBATIM = 98;
/**
 * Every children is a diffrent HTML file. Usefull to generate several pages.
 */
exports.PAGES = 99;
/**
 * @return a deep copy of `root`.
 */
exports.clone = function(root) {
    return JSON.parse(JSON.stringify(root));
};
/**
 * Return the next incremental id.
 */
exports.nextId = function() {
    return "W" + (id++);
};
/**
 * @param {object} root root of the tree we want to look in.
 * @param {string} name name of the searched TAG. Must be in lowercase.
 * @return the first TAG node with the name `name`.
 */
exports.getElementByName = function(root, name) {
    if (!root) return null;
    if (root.name && root.name.toLowerCase() === name.toLowerCase()) return root;
    if (Array.isArray(root.children)) {
        var i, node;
        for (i = 0 ; i < root.children.length ; i++) {
            node = exports.getElementByName(root.children[i], name);
            if (node !== null) return node;
        }
    }
    return null;
};
/**
 * Remove `child` from the children's list of `parent`.
 */
exports.removeChild = function(parent, child) {
    if (Array.isArray(parent.children)) {
        var i, node;
        for (i = 0 ; i < parent.children.length ; i++) {
            node = parent.children[i];
            if (node === child) {
                parent.children.splice(i, 1);
                break;
            }
        }
    }
};
exports.trim = function(root) {
    var children = root.children;
    function isEmpty(node) {
        if (node.type != exports.TEXT) return false;
        if (!node.text) return false;
        if (node.text.trim().length == 0) return true;
        return false;
    }
    while (children.length > 0 && isEmpty(children[0])) {
        children.shift();
    }
    while (children.length > 0 && isEmpty(children[children.length - 1])) {
        children.pop();
    }
    if (children.length > 0) {
        if (children[0].type == exports.TEXT) {
            children[0].text = children[0].text.trimLeft();
        }
        if (children[children.length - 1].type == exports.TEXT) {
            children[children.length - 1].text = children[children.length - 1].text.trimRight();
        }
    }
};
exports.trimLeft = function(root) {
    var children = root.children;
    function isEmpty(node) {
        if (node.type != exports.TEXT) return false;
        if (!node.text) return false;
        if (node.text.trim().length == 0) return true;
        return false;
    }
    while (children.length > 0 && isEmpty(children[0])) {
        children.shift();
    }
    if (children.length > 0) {
        if (children[0].type == exports.TEXT) {
            children[0].text = children[0].text.trimLeft();
        }
    }
};
exports.trimRight = function(root) {
    var children = root.children;
    function isEmpty(node) {
        if (node.type != exports.TEXT) return false;
        if (!node.text) return false;
        if (node.text.trim().length == 0) return true;
        return false;
    }
    while (children.length > 0 && isEmpty(children[children.length - 1])) {
        children.pop();
    }
    if (children.length > 0) {
        if (children[children.length - 1].type == exports.TEXT) {
            children[children.length - 1].text = children[children.length - 1].text.trimRight();
        }
    }
};
/**
 * Convert a node in HTML code.
 */
exports.toString = function(node) {
    var txt = '',
    key, val;
    if (!node) return '';
    if (node.type == exports.TAG) {
        txt += "<" + node.name;
        for (key in node.attribs) {
            val = "" + node.attribs[key];
            txt += " " + key + "=" + JSON.stringify(val) + "";
        }
        if (node.children && node.children.length > 0) {
            txt += ">";
            node.children.forEach(
                function(child) {
                    txt += exports.toString(child);
                }
            );
            txt += "</" + node.name + ">";
        } else {
            if (node.void) txt += ">";
            else if (node.autoclose) txt += "/>";
            else txt += "></" + node.name + ">";
        }
    }
    else if (node.type == exports.ENTITY) {
        txt += node.text;
    }
    else if (node.type == exports.VERBATIM) {
        txt += node.text;
    }
    else if (node.type == exports.TEXT) {
        txt += node.text;
    }
    else if (node.type == exports.PROCESSING) {
        txt += "<?" + node.name;
        for (key in node.attribs) {
            val = "" + node.attribs[key];
            txt += " " + key + "=" + JSON.stringify(val) + "";
        }
        txt += "?>";
    }
    else if (node.children) {
        node.children.forEach(
            function(child) {
                txt += exports.toString(child);
            }
        );
    }
    return txt;
};
/**
 * Put on console a representation of the tree.
 */
exports.debug = function(node, indent) {
    if (typeof indent === 'undefined') indent = '';
    if (!node) {
        return console.log(indent + "UNDEFINED!");
    }
    if (node.type == exports.TEXT) {
        console.log(indent + "\"" + node.text.trim() + "\"");
    } else {
        if (node.children && node.children.length > 0) {
            console.log(
                indent + "<" + (node.name ? node.name : node.type) + "> "
                    + (node.attribs ? JSON.stringify(node.attribs) : '')
            );
            node.children.forEach(
                function(child) {
                    exports.debug(child, indent + '  ');
                }
            );
            console.log(indent + "</" + (node.name ? node.name : node.type) + "> ");
        } else {
            console.log(
                indent + "<" + (node.name ? node.name : node.type) + " "
                    + (node.attribs ? JSON.stringify(node.attribs) : '') + " />"
            );
        }
    }
};
/**
 * Walk through the HTML tree and, eventually, replace branches.
 * The functions used as arguments take only one argument: the current node.
 * @param node {object} root node
 * @param functionBottomUp {function} function to call when traversing the tree bottom-up.
 * @param functionTopDowy {function} function to call when traversing the tree top-down.
 * @param parent {object} parent node or *undefined*.
 */
exports.walk = function(node, functionBotomUp, functionTopDown, parent) {
    if (!node) return;
    var i, child, replacement, children;
    if (typeof functionTopDown === 'function') {
        functionTopDown(node, parent);
    }
    if (node.children) {
        children = [];
        node.children.forEach(
            function(item) {
                children.push(item);
            }
        );
        children.forEach(
            function(child) {
                exports.walk(child, functionBotomUp, functionTopDown, node);
            }
        );
    }
    if (typeof functionBotomUp === 'function') {
        return functionBotomUp(node, parent);
    }
};
/**
 * Get/Set an attribute.
 */
exports.att = function(node, name, value) {
    if (typeof value === 'undefined') {
        if (node.attribs) {
            return node.attribs[name];
        }
        return undefined;
    }
    if (!node.attribs) {
        node.attribs = {};
    }
    node.attribs[name] = value;
};
/**
 * Return the text content of a node.
 */
exports.text = function(node, text) {
    if (typeof text === 'undefined') {
        if (node.type == exports.TEXT) {
            return node.text;
        }
        if (node.children) {
            var txt = "";
            node.children.forEach(
                function(child) {
                    txt += exports.text(child);
                }
            );
            return txt;
        } else {
            return "";
        }
    } else {
        node.children = [
            {
                type: exports.TEXT,
                text: text
            }
        ];
    }
};
/**
 * Return a node representing a viewable error message.
 */
exports.createError = function(msg) {
    return {
        type: exports.TAG,
        name: "div",
        attribs: {
            style: "margin:4px;padding:4px;border:2px solid #fff;color:#fff;background:#f00;"
                + "box-shadow:0 0 2px #000;overflow:auto;font-family:monospace;font-size:1rem;"
        },
        children: [
            {
                type: exports.TEXT,
                text: msg.replace("\n", "<br/>", "g")
            }
        ]
    };
};
/**
 * Add a class to a node of type TAG.
 */
exports.addClass = function(node, className) {
    if (!node.attribs) {
        node.attribs = {};
    }
    var classes = (node.attribs["class"] || "").split(/[ \t\n\r]/g);
    var i;
    for (i = 0 ; i < classes.length ; i++) {
        if (className == classes[i]) return false;
    }
    var txt = "";
    classes.push(className);
    for (i = 0 ; i < classes.length ; i++) {
        if (txt.length > 0) {
            txt += " ";
        }
        txt += classes[i];
    }
    if (txt.length > 0) {
        node.attribs["class"] = txt;
    }
    return true;
};
/**
 * Return a node of type TAG représenting a javascript content.
 */
exports.createJavascript = function(code) {
    return {
        type: exports.TAG,
        name: "script",
        attribs: {
            type: "text/javascript"
        },
        children: [
            {
                type: exports.TEXT,
                text: "//<![CDATA[\n" + code + "//]]>"
            }
        ]
    };
};
exports.forEachAttrib = function(node, func) {
    var attribs = node.attribs,
    attName, attValue, count = 0;
    if (!attribs) return 0;
    for (attName in attribs) {
        attValue = attribs[attName];
        if (typeof attValue === 'string') {
            func(attName, attValue);
        }
    }
    return count;
};
/**
 * Apply a function on every child of a node.
 */
exports.forEachChild = function(node, func) {
    var children = node.children, i, child;
    if (!children) return false;
    for (i = 0 ; i < children.length ; i++) {
        child = children[i];
        if (typeof child.type === 'undefined' || child.type == exports.VOID) {
            exports.forEachChild(child, func);
        }
        else if (true === func(child, i)) {
            break;
        }
    }
    return i;
};
/**
 * If a node's children is of type VOID, we must remove it and add its
 * children to node's children.
 *
 * Then following tree:
 * ```
 * {
 *   children: [
 *     {
 *       children: [
 *         {type: Tree.TAG, name: "hr"},
 *         {
 *           children: [{type: Tree.TAG, name: "img"}]
 *         }
 *         {type: Tree.TEXT, text: "Hello"},
 *       ]
 *     },
 *     {type: Tree.TEXT, text: "World"},
 *   ],
 *   type: Tree.TAG,
 *   name: "div"
 * }
 * ```
 * will be tranformed in:
 * ```
 * {
 *   children: [
 *     {type: Tree.TAG, name: "hr"},
 *     {type: Tree.TAG, name: "img"},
 *     {type: Tree.TEXT, text: "Hello"},
 *     {type: Tree.TEXT, text: "World"},
 *   ],
 *   type: Tree.TAG,
 *   name: "div"
 * }
 * ```
 */
exports.normalizeChildren = function(node, recurse) {
    if (typeof recurse === 'undefined') recurse = false;
    if (node.children) {
        var children = [];
        extractNonVoidChildren(node, children);
        node.children = children;
        if (recurse) {
            node.children.forEach(
                function(child) {
                    exports.normalizeChildren(child, true);
                }
            );
        }
    }
};
function extractNonVoidChildren(node, target) {
    if (typeof target === 'undefined') target = [];
    if (node.children && node.children.length > 0) {
        node.children.forEach(
            function(child) {
                if (!child.type || child.type == exports.VOID) {
                    extractNonVoidChildren(child, target);
                } else {
                    target.push(child);
                }
            }
        );
    }
    return target;
}
/**
 * @return The first children tag with name `tagname`, or `null` if not found.
 */
exports.findChild = function(root, tagname) {
    if (!Array.isArray(root.children)) return null;
    for (var i = 0 ; i < root.children.length ; i++) {
        var item = root.children[i];
        if (item.type == exports.TAG && item.name == tagname) return item;
    }
    return null;
};
/**
 * If a tag called `tagname` exist among `root`'s children, return it.
 * Otherwise, create a new tag, append it to `root` and return it.
 */
exports.findOrAppendChild = function(root, tagname, attribs, children) {
    var child = exports.findChild(root, tagname);
    if (child) return child;
    child = exports.tag(tagname, attribs, children);
    if (!Array.isArray(root.children)) {
        root.children = [child];
    } else {
        root.children.push(child);
    }
    return child;
};
/**
 * Return a div element.
 */
exports.div = function(attribs, children) {
    return exports.tag("div", attribs, children);
};
/**
 * @example
 * // <span style='color: red'>ERROR</span>
 * htmltree.tag("span", {style: "color: red"}, "ERROR");
 * @example
 * // <b><span class="dirty">OK!</span></b>
 * htmltree.tag("b", {}, [
 *   htmltree.tag("span", "dirty", "OK!")
 * ]);
 */
exports.tag = function(name, attribs, children) {
    if (!attribs) attribs = {};
    if (typeof attribs === 'string') attribs = {"class": attribs};
    if (typeof children === 'undefined') children = [];
    if (!Array.isArray(children)) {
        if (typeof children === 'string') {
            children = {
                type: exports.TEXT,
                text: children
            };
        }
        children = [children];
    }
    return {
        type: exports.TAG,
        name: name,
        attribs: attribs,
        children: children
    };
};
/**
 * Return multi-lingual text.
 */
exports.createText = function(dic) {
    var key, val, children = [];
    for (key in dic) {
        val = dic[key];
        children.push(
            {
                type: exports.TAG,
                name: "span",
                attribs: {
                    lang: key
                },
                children: [
                    {
                        type: exports.TEXT,
                        text: val
                    }
                ]
            }
        );
    }
    return children;
};
/**
 * @description
 * Remove all TEXT or COMMENT children.
 *
 * @param root the htmlnode from which you want to keep only TAG children.
 * @memberof node
 */
exports.keepOnlyTagChildren = function(root) {
    if (!root.children) return root;
    var children = [];
    root.children.forEach(
        function(node) {
            if (node.type == exports.TAG) {
                children.push(node);
            }
        }
    );
    root.children = children;
    return root;
};