Source: xxe/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";