# Source: view/Name.js

    /**
     * Represents the name of a XML element or attribute.
     */
    export class Name {
        /**
         * Constructs an XML Name having specified namespace URI
         * and specified local part.
         *
         * @param {string} namespace - the namespace of the name.
         * Use the empty string not <code>null</code> to specify the
         * absence of namespace.
         * @param {string} localPart - the local part of the name.
         */
        constructor(namespace, localPart) {
            if (namespace === null) {
                throw new Error("null namespace");
            }
            if (localPart === null) {
                throw new Error("null localPart");
            }

            this._namespace = namespace.trim(); // Just in case.
            this._localPart = localPart;
        }

        /**
         * Get the <code>namespace</code> property of this name.
         * Never <code>null</code>.
         *
         * @type {string}
         */
        get namespace() {
            return this._namespace;
        }

        /**
         * Get the <code>localPart</code> property of this name.
         *
         * @type {string}
         */
        get localPart() {
            return this._localPart;
        }

        /**
         * Returns a name parsed from specified string representation.
         * <p>The grammar of the string representation is:
         * <pre>string_presentation -> Name | '{}' Name |
         *                        'xml:' NCName |
         *                        '{' anyURI '}' NCName</pre>
         *
         * @param {string} spec - the string representation of the name.
         * @return {Name} parsed name or <code>null</code>.
         */
        static fromString(spec) {
            if (spec === null || (spec = spec.trim()).length === 0) {
                return null;
            }

            let pos = -1;
            if (spec.charAt(0) === '{' && (pos = spec.lastIndexOf('}')) > 0) {
                if (pos+1 === length) {
                    return null;
                }

                let ns = spec.substring(1, pos);
                let localPart = spec.substring(pos+1);

                if (ns.length === 0) {
                    if (!Name.isName(localPart)) {
                        return null;
                    }

                    return new Name(Name.NS_NONE, localPart);
                } else {
                    if (ns.trim().length === 0 ||
                        !Name.isNCName(localPart)) {
                        return null;
                    }

                    return new Name(ns, localPart);
                }
            } else {
                if (spec.startsWith("xml:")) {
                    let localPart = spec.substring(4);
                    if (!Name.isNCName(localPart)) {
                        return null;
                    }

                    return new Name(Name.NS_XML, localPart);
                } else {
                    if (!Name.isName(spec)) {
                        return null;
                    }

                    return new Name(Name.NS_NONE, spec);
                }
            }
        }

        /**
         * Tests whether specified string is an XML Name.
         *
         * @param {string} spec - the string to be tested.
         */
        static isName(spec) {
            // A simplification of XML Names.
            // (\u00B7 is MIDDLE DOT.)
            return (spec !== null &&
                    spec.length > 0 &&
                    spec.match(/^[:_\p{L}][:_\p{L}0-9\u00B7.-]*$/u) !== null);
        }

        /**
         * Tests whether specified string is an XML NCName.
         *
         * @param {string} spec - the string to be tested.
         */
        static isNCName(spec) {
            // A simplification of XML NCNames.
            return (spec !== null &&
                    spec.length > 0 &&
                    spec.match(/^[_\p{L}][_\p{L}0-9\u00B7.-]*$/u) !== null);
        }

        /**
         * Inverse method of {@link Name#fromString}.
         */
        toString() {
            if (this._namespace === Name.NS_NONE) {
                return this._localPart;
            } else if (this._namespace === Name.NS_XML) {
                return "xml:" + this._localPart;
            }

            return '{' + this._namespace + '}' + this._localPart;
        }

        /**
         * Returns the name parsed from specified prefixed representation
         * using specified prefix to namespace assocations.
         * <p>If <code>prefixToNS</code> is not specified, this function
         * only knows about names without a namespace and names in the
         * "<code>http://www.w3.org/XML/1998/namespace</code>" namespace
         * (example: <code>xml:lang</code>).
         *
         * @param {string} qName - "prefixed" representation
         * (examples: <code>bar</code> or <code>foo:bar</code> ).
         * @param {boolean} isAttribute - <code>true</code> if qName is
         * the name of an attribute; <code>false</code> if qName is the name
         * of an element. More generally specifies whether the default
         * namespace may be used to parse the qualified name.
         * @param {array nsPrefixes - prefix to namespace associations:
         * an array containing <code>[<i>prefix</i>, <i>namespace</i>]</code>
         * pairs; may be <code>null</code>.
         * @return {Name} parsed name or <code>null</code>
         * (if qName is malformed or if its prefix is unknown).
         */
        static parse(qName, isAttribute, nsPrefixes) {
            let prefix = null, localPart = null;

            let colon = qName.indexOf(':');
            if (colon < 0) {
                prefix = "";
                localPart = qName;

                if (!Name.isNCName(localPart)) {
                    // Malformed qName.
                    return null;
                }
            } else {
                if (colon === 0 || colon === qName.length-1) {
                    // Malformed qName.
                    return null;
                }

                prefix = qName.substring(0, colon);
                localPart = qName.substring(colon+1);

                if (!Name.isNCName(prefix) ||
                    !Name.isNCName(localPart)) {
                    // Malformed qName.
                    return null;
                }
            }

            // ---
            let namespace = null;

            if (prefix.length === 0) {
                if (isAttribute) {
                    namespace = Name.NS_NONE;
                } else {
                    // Use default namespace if any, otherwise use NONE.
                    namespace = Name.findNS(nsPrefixes, prefix);
                    if (namespace === null) {
                        namespace = Name.NS_NONE;
                    }
                }
            } else if ("xml" === prefix) {
                namespace = Name.NS_XML;
            } else {
                namespace = Name.findNS(nsPrefixes, prefix);
                if (namespace === null) {
                    // Unknown prefix.
                    return null;
                }
            }

            return new Name(namespace, localPart);
        }

        static findNS(nsPrefixes, prefix) {
            if (nsPrefixes !== null && nsPrefixes.length > 0) {
                for (let nsPrefix of nsPrefixes) {
                    let [pre, ns] = nsPrefix;
                    if (pre === prefix) {
                        return ns;
                    }
                }
            }

            return null;
        }

        /**
         * Returns a prefixed representation of this name using
         * specified namespace to prefixe assocations.
         * <p>If <code>nsToPrefixes</code> is not specified, this function can only
         * format names without a namespace and names in the
         * "<code>http://www.w3.org/XML/1998/namespace</code>" namespace
         * (example: <code>xml:lang</code>).
         * For other names, it fallbacks to {@link #toString}.
         *
         * @param {boolean} isAttribute - <code>true</code> if this name is
         * the name of an attribute; <code>false</code> if this name is the name
         * of an element. More generally specifies whether the default
         * namespace may be used to format the name.
         * @param {array nsPrefixes - prefix to namespace associations:
         * an array containing <code>[<i>prefix</i>, <i>namespace</i>]</code>
         * pairs; may be <code>null</code>.
         * @return {string} "prefixed" representation if <code>nsToPrefixes</code>
         * is specified and suitable namespace is found in these assocations;
         * the "non-prefixed" representation of {@link Name#toString} otherwise.
         */
        format(isAttribute, nsPrefixes) {
            if (this._namespace === Name.NS_NONE) {
                return this._localPart;
            }

            let prefix = null;
            if (this._namespace === Name.NS_XML) {
                prefix = "xml";
            } else {
                let count = 0;
                if (nsPrefixes !== null && (count = nsPrefixes.length) > 0) {
                    if (!isAttribute) {
                        // Preferably use default NS (empty prefix) for elements.
                        for (let i =  count-1; i >= 0; --i) {
                            let [pre, ns] = nsPrefixes[i];
                            if (ns === this._namespace && pre.length === 0) {
                                prefix = pre;
                                break;
                            }
                        }
                    }

                    if (prefix === null) {
                        // Default NS (empty prefix) not allowed for attributes.
                        for (let i = 0; i < count; ++i) {
                            let [pre, ns] = nsPrefixes[i];
                            if (ns === this._namespace && pre.length > 0) {
                                prefix = pre;
                                break;
                            }
                        }
                    }
                }

                if (prefix === null) {
                    // No namespaces or prefix not declared.
                    return this.toString();
                }
            }

            if (prefix.length > 0) {
                return prefix + ':' + this._localPart;
            } else {
                return this._localPart;
            }
        }

        // -----------------------------------------------------------------------

        /*TEST|
        static test_Name(logger) {
            let testIndex = 0;

            let nsPrefixesList = [ null, [] ];
            for (let nsPrefixes of nsPrefixesList) {
                Name.nameTest(null, false, nsPrefixes, 1);
                Name.nameTest("", false, nsPrefixes, 1);
                Name.nameTest("foo bar", false, nsPrefixes, 1);

                Name.nameTest("para", false, nsPrefixes, 0);
                Name.nameTest("{}para", false, nsPrefixes, 0);
                Name.nameTest("id", true, nsPrefixes, 0);
                Name.nameTest("xml:lang", true, nsPrefixes, 0);
                Name.nameTest("{http://www.w3.org/XML/1998/namespace}lang", true,
                         nsPrefixes, 0);

                Name.nameTest(":foo", false, nsPrefixes, 2);
                Name.nameTest("foo:", false, nsPrefixes, 2);
                Name.nameTest("foo:bar:gee", false, nsPrefixes, 2);
                Name.nameTest("{http://www.w3.org/1999/xhtml}p", false,
                              nsPrefixes, 2);
                Name.nameTest("{http://www.w3.org/1999/xlink}href", true,
                              nsPrefixes, 2);
            }

            // ---

            const nsPrefixes1 = [
                ["mml", "http://www.w3.org/1998/Math/MathML"],
                ["math", "http://www.w3.org/1998/Math/MathML"],
                ["svg", "http://www.w3.org/2000/svg"],
                ["xlink", "http://www.w3.org/1999/xlink"],
                ["htm", "http://www.w3.org/1999/xhtml"]
            ];
            const nsPrefixes2 = [
                ...nsPrefixes1,
                ["", "http://www.w3.org/1999/xhtml"]
            ];
            nsPrefixesList = [ nsPrefixes1, nsPrefixes2 ];
            for (let nsPrefixes of nsPrefixesList) {
                Name.nameTest("pre", false, nsPrefixes,
                              (Object.is(nsPrefixes, nsPrefixes2))? 3 : 0);

                Name.nameTest("{http://www.w3.org/1999/xhtml}p", false,
                              nsPrefixes, 0);
                Name.nameTest("lang", true, nsPrefixes, 0);
                Name.nameTest("xml:id", true, nsPrefixes, 0);
                Name.nameTest("{http://www.w3.org/1999/xlink}href", true,
                              nsPrefixes, 0);
                Name.nameTest("{http://www.w3.org/2000/svg}svg", false,
                              nsPrefixes, 0);
                Name.nameTest("{http://www.w3.org/1998/Math/MathML}math", false,
                              nsPrefixes, 0);

                Name.nameTest("{DAV:}lockscope", false, nsPrefixes, 2);
            }
        }

        static nameTest(stringForm, isAttribute, nsPrefixes, expectedResult) {
            let name = Name.fromString(stringForm);
            if (name === null) {
                Name.checkNameTest(1, expectedResult,
                                   `cannot parse string form "${stringForm}"`);
                return 1;
            }

            let qName = name.format(isAttribute, nsPrefixes);

            let name2 = Name.parse(qName, isAttribute, nsPrefixes);
            if (name2 === null) {
                Name.checkNameTest(2, expectedResult,
                                   `cannot parse qualified name "${qName}"`);
                return 2;
            }
            if (name2.namespace !== name.namespace ||
                name2.localPart !== name.localPart) {
                Name.checkNameTest(3, expectedResult,
                                   `expected "${name.toString()}",
    found "${name2.toString()}"`);
                return 3;
            }

            return 0;
        }

        static checkNameTest(got, expected, msg) {
            if (got !== expected) {
                throw new Error(msg);
            }
        }
        |TEST*/
    }

    Name.NS_NONE = "";
    Name.NS_XML = "http://www.w3.org/XML/1998/namespace";