Source: xxe/view/DOMUtil.js

const TRAVERSAL_LEAVE_ELEMENT = "__TRAVERSAL__LEAVE__ELEMENT__";

/**
 * Generic DOM utilities.
 */
/*TEST|export|TEST*/ class DOMUtil {
    // ------------------------------------
    // Document traversal
    // ------------------------------------

    static traverse(node, handler) {
        switch (node.nodeType) {
        case Node.TEXT_NODE:
        case Node.PROCESSING_INSTRUCTION_NODE:
        case Node.COMMENT_NODE:
            return handler(node, /*enterElement*/ undefined);

        case Node.ELEMENT_NODE:
            {
                let result = handler(node, /*enterElement*/ true);
                if (result !== null) {
                    return (result === TRAVERSAL_LEAVE_ELEMENT)? null : result;
                }

                let child = node.firstChild;
                while (child !== null) {
                    result = DOMUtil.traverse(child, handler);
                    if (result !== null) {
                        return result;
                    }

                    child = child.nextSibling;
                }

                result = handler(node, /*enterElement*/ false);
                if (result !== null) {
                    return result;
                }
            }
            return null;

        default:
            return null;
        }
    }

    static traverseBackwards(node, handler) {
        switch (node.nodeType) {
        case Node.TEXT_NODE:
        case Node.PROCESSING_INSTRUCTION_NODE:
        case Node.COMMENT_NODE:
            return handler(node, /*enterElement*/ undefined);

        case Node.ELEMENT_NODE:
            {
                let result = handler(node, /*enterElement*/ true);
                if (result !== null) {
                    return (result === TRAVERSAL_LEAVE_ELEMENT)? null : result;
                }

                let child = node.lastChild;
                while (child !== null) {
                    result = DOMUtil.traverseBackwards(child, handler);
                    if (result !== null) {
                        return result;
                    }

                    child = child.previousSibling;
                }

                result = handler(node, /*enterElement*/ false);
                if (result !== null) {
                    return result;
                }
            }
            return null;

        default:
            return null;
        }
    }
    
    static traverseFrom(node, handler) {
        let result = DOMUtil.traverse(node, handler);
        if (result !== null) {
            return result;
        }

        return DOMUtil.traverseAfter(node, handler);
    }

    static traverseAfter(node, handler) {
        let parent = DOMUtil.parentElementWithinView(node);
        if (parent === null) {
            return null;
        }

        let sibling = node.nextSibling;
        while (sibling !== null) {
            let result = DOMUtil.traverse(sibling, handler);
            if (result !== null) {
                return result;
            }

            sibling = sibling.nextSibling;
        }

        for (;;) {
            // Do not visit the siblings of root element.
            let ancestor = DOMUtil.parentElementWithinView(parent);
            if (ancestor === null) {
                return null;
            }

            let from = parent.nextSibling;
            if (from !== null) {
                return DOMUtil.traverseFrom(from, handler);
            }

            parent = ancestor;
        }
    }

    static traverseBackwardsFrom(node, handler) {
        // This utility corresponds to XXE's Traversal.traverseBackwardsFromEx
        // (which generates extra leave parent events
        // compared to Traversal.traverseBackwardsFrom).
        
        let result = DOMUtil.traverseBackwards(node, handler);
        if (result !== null) {
            return result;
        }

        return DOMUtil.traverseBefore(node, handler);
    }

    static traverseBefore(node, handler) {
        // This utility corresponds to XXE's Traversal.traverseBeforeEx
        // (which generates extra leave parent events
        // compared to Traversal.traverseBefore).
        
        let parent = DOMUtil.parentElementWithinView(node);
        if (parent === null) {
            return null;
        }

        let sibling = node.previousSibling;
        while (sibling !== null) {
            let result = DOMUtil.traverseBackwards(sibling, handler);
            if (result !== null) {
                return result;
            }

            sibling = sibling.previousSibling;
        }

        for (;;) {
            let result = handler(parent, /*enterElement*/ false);
            if (result !== null) {
                return result;
            }

            // Do not visit the siblings of root element.
            let ancestor = DOMUtil.parentElementWithinView(parent);
            if (ancestor === null) {
                return null;
            }

            let from = parent.previousSibling;
            if (from !== null) {
                return DOMUtil.traverseBackwardsFrom(from, handler);
            }

            parent = ancestor;
        }
    }

    static parentElementWithinView(node) {
        let parent = node.parentElement;
        if (parent !== null && (parent instanceof DocumentView)) {
            parent = null;
        }
        return parent;
    }
    
    // ------------------------------------
    // lookupAncestorByTag
    // ------------------------------------

    static lookupAncestorByTag(node, ancestorName) {
        let found = null;
        let ancestor = node.parentElement;
        while (ancestor !== null) {
            if (ancestor.localName === ancestorName) {
                found = ancestor;
                break;
            }
            ancestor = ancestor.parentElement;
        }

        return found;
    }

    // ------------------------------------
    // textContentLength
    // ------------------------------------

    static textContentLength(node) {
        let count = 0;

        if (node !== null) {
            switch (node.nodeType) {
            case Node.TEXT_NODE:
                count += node.length;
                break;
            case Node.ELEMENT_NODE:
                {
                    let child = node.firstChild;
                    while (child !== null) {
                        count += DOMUtil.textContentLength(child);
                        child = child.nextSibling;
                    }
                }
                break;
            }
        }
        
        return count;
    }

    // ------------------------------------
    // getAllTextNodes
    // ------------------------------------

    static getAllTextNodes(node) {
        let list = [];
        if (node !== null) {
            DOMUtil.doGetAllTextNodes(node, list);
        }
        return list;
    }
    
    static doGetAllTextNodes(node, list) {
        switch (node.nodeType) {
        case Node.TEXT_NODE:
            list.push(node)
            break;
        case Node.ELEMENT_NODE:
            {
                let child = node.firstChild;
                while (child !== null) {
                    DOMUtil.doGetAllTextNodes(child, list);
                    child = child.nextSibling;
                }
            }
            break;
        }
    }
    
    // ------------------------------------
    // createElementFromHTML
    // ------------------------------------

    static createElementFromHTML(html) {
        // Unlike table, template has no content restriction.
        let template = document.createElement('template');
        template.innerHTML = html;
        return template.content.firstElementChild;
    }
    
    // ------------------------------------
    // isDisplayedNode
    // ------------------------------------
    
    static isDisplayedNode(node, root) {
        let elem = node;
        if (node !== null &&
            node.nodeType !== Node.ELEMENT_NODE) {
            elem = node.parentElement;
        }

        while (elem !== null && elem !== root) {
            const display =
                window.getComputedStyle(elem).getPropertyValue("display");
            if (display === "none") {
                return false;
            }
            
            elem = elem.parentElement;
        }

        return true;
    }
    
    // ------------------------------------
    // dumpNode
    // ------------------------------------

    static dumpNode(node, dump="", indent=0) {
        if (node !== null) {
            dump = DOMUtil.indentDumpNode(dump, indent);

            switch (node.nodeType) {
            case Node.TEXT_NODE:
                dump += '"' + node.data + '" (' + node.length + ' chars)\n';
                break;
            case Node.ELEMENT_NODE:
                {
                    dump += '<' + node.localName;
                    const attrs = node.attributes;
                    for (let i = attrs.length-1; i >= 0; --i) {
                        dump += " " + attrs[i].name +
                            "='" + attrs[i].value + "'";
                    }

                    let child = node.firstChild;
                    if (child === null) {
                        dump += ' />\n';
                    } else {
                        dump += '>\n';

                        while (child !== null) {
                            dump = DOMUtil.dumpNode(child, dump, indent+2);
                            child = child.nextSibling;
                        }

                        dump = DOMUtil.indentDumpNode(dump, indent);
                        dump += '</' + node.localName + '>\n';
                    }
                }
                break;
            }
        }
        
        return dump;
    }

    static indentDumpNode(dump, indent) {
        while (indent > 0) {
            dump += " ";
            --indent;
        }
        return dump;
    }

    // ------------------------------------
    // escapeXML/unescapeXML
    // ------------------------------------

    /**
     * Escapes specified string (that is, 
     * <code>'&lt;'</code> is replaced by "<code>&amp;#60</code>;",
     * <code>'&amp;'</code> is replaced by "<code>&amp;#38;</code>", etc).
     * 
     * @param {string} text - string to be escaped.
     * @param {number} [maxCode=0xFFFF] maxCode - characters with 
     * code &gt; maxCode are escaped as <code>&amp;#<i>code</i>;</code>.
     * Pass 127 for US-ASCII, 255 for ISO-8859-1, otherwise pass
     * <code>0xFFFF</code> (Unicode Basic Multilingual Plane).
     * @return {string} escaped string.
     */
    static escapeXML(text, maxCode=0xFFFF) {
        let escaped = "";

        const count = text.length;
        for (let i = 0; i < count; ++i) {
            let c = text.charAt(i);

            switch (c) {
            case "'":
                escaped += "&#39;";
                break;
            case '"':
                escaped += "&#34;";
                break;
            case '<':
                escaped += "&#60;";
                break;
            case '>':
                escaped += "&#62;";
                break;
            case '&':
                escaped += "&#38;";
                break;
            default:
                {
                    const code = c.charCodeAt(0);
                    if (code > maxCode) {
                        escaped += "&#";
                        escaped += String(code);
                        escaped += ';';
                    } else {
                        escaped += c;
                    }
                }
            }
        }

        return escaped;
    }

    /**
     * Unescapes specified string. Inverse operation of <code>escapeXML</code>.
     * 
     * @param {string}  text string to be unescaped.
     * @return {string}  unescaped string.
     */
    static unescapeXML(text) {
        const parseCharRef = (charRef) => {
            if (charRef.length >= 2 && charRef.charAt(0) === '#') {
                let i;
                if (charRef.charAt(1) === 'x') {
                    i = parseInt(charRef.substring(2), 16);
                } else {
                    i = parseInt(charRef.substring(1));
                }
                if (isNaN(i) || i <= 0 || i > 0xFFFF) {
                    return '?';
                } else {
                    return String.fromCharCode(i);
                }
            } if (charRef === "amp") {
                return '&';
            } if (charRef === "apos") {
                return "'";
            } if (charRef === "quot") {
                return '"';
            } if (charRef === "lt") {
                return '<';
            } if (charRef === "gt") {
                return '>';
            } else {
                return '?';
            }
        }

        // ---

        let unescaped = "";

        const count = text.length;
        for (let i = 0; i < count; ++i) {
            let c = text.charAt(i);

            if (c === '&') {
                let charRef = "";

                ++i;
                while (i < count) {
                    c = text.charAt(i);
                    if (c === ';') {
                        break;
                    }
                    charRef += c;
                    ++i;
                }

                c = parseCharRef(charRef);
            } 

            unescaped += c;
        }

        return unescaped;
    }
    
    // -----------------------------------------------------------------------

    /*TEST|
    static test_escapeXML(logger) {
        const texts = [
            "\"Black & Decker\" (<BLACK+DECKER\u2122>) une 'bonne' marque?",
            "\u00ABOh le bel \u00E9t\u00E9 que voil\u00E0!\u00BB",
            "\u00C7a c\u2019est bien vrai!",
        ];
        for (let text of texts) {
            let escaped = DOMUtil.escapeXML(text, 127); // US-ASCII
            let unescaped = DOMUtil.unescapeXML(escaped);
            logger(`${text} --escape--> ${escaped} --unescape--> ${unescaped}`);
        }
    }
    |TEST*/
}