# Source: view/DOMUtil.js

    const TRAVERSAL_LEAVE_ELEMENT = "__TRAVERSAL__LEAVE__ELEMENT__";

    /**
     * Generic DOM utilities.
     */
    export 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*/
    }