/**
* 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";