salve-annos
Version:
A fork with support for documentation of Salve, a Javascript library which implements a validator able to validate an XML document on the basis of a subset of RelaxNG.
516 lines • 17.2 kB
JavaScript
;
/**
* @author Louis-Dominique Dubeau
* @license MPL 2.0
* @copyright Mangalam Research Center for Buddhist Languages
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AnyName = exports.NsName = exports.NameChoice = exports.Name = exports.Base = void 0;
exports.isName = isName;
exports.isNameChoice = isNameChoice;
exports.isNsName = isNsName;
exports.isAnyName = isAnyName;
function escapeString(str) {
return str.replace(/(["\\])/g, "\\$1");
}
/**
* Base class for all name patterns.
*/
class Base {
/**
* Gets the list of namespaces used in the pattern. An ``::except`` entry
* indicates that there are exceptions in the pattern. A ``*`` entry indicates
* that any namespace is allowed.
*
* This method should be used by client code to help determine how to prompt
* the user for a namespace. If the return value is a list without
* ``::except`` or ``*``, the client code knows there is a finite list of
* namespaces expected, and what the possible values are. So it could present
* the user with a choice from the set. If ``::except`` or ``*`` appears in
* the list, then a different strategy must be used.
*
* @returns The list of namespaces.
*/
getNamespaces() {
const namespaces = new Set();
this._recordNamespaces(namespaces, true);
return Array.from(namespaces);
}
/**
* Alias of [[Base.toObject]].
*
* ``toJSON`` is a misnomer, as the data returned is not JSON but a JavaScript
* object. This method exists so that ``JSON.stringify`` can use it.
*/
toJSON() {
return this.toObject();
}
/**
* Stringify the pattern to a JSON string.
*
* @returns The stringified instance.
*/
toString() {
// Profiling showed that caching the string representation helps
// performance.
if (this._asString === undefined) {
this._asString = this.asString();
}
return this._asString;
}
}
exports.Base = Base;
function isName(name) {
return name.kind === "Name";
}
function isNameChoice(name) {
return name.kind === "NameChoice";
}
function isNsName(name) {
return name.kind === "NsName";
}
function isAnyName(name) {
return name.kind === "AnyName";
}
/**
* Models the Relax NG ``<name>`` element.
*
*/
class Name extends Base {
/**
* @param ns The namespace URI for this name. Corresponds to the
* ``ns`` attribute in the simplified Relax NG syntax.
*
* @param name The name. Corresponds to the content of ``<name>``
* in the simplified Relax NG syntax.
*
* @param documentation Attached documentation if available.
*/
constructor(ns, name, documentation) {
super();
this.ns = ns;
this.name = name;
this.documentation = documentation;
this.kind = "Name";
}
match(ns, name) {
return this.name === name && this.ns === ns;
}
intersects(other) {
return isName(other) ? this.match(other.ns, other.name) :
// Delegate to the other classes.
other.intersects(this);
}
intersection(other) {
if (other === 0) {
return 0;
}
if (isName(other)) {
return this.match(other.ns, other.name) ? this : 0;
}
// Delegate to the other classes.
return other.intersection(this);
}
// @ts-ignore
wildcardMatch(ns, name) {
return false; // This is not a wildcard.
}
toObject() {
return {
ns: this.ns,
name: this.name,
documentation: this.documentation,
};
}
asString() {
// We don't need to escape this.name because names cannot contain
// things that need escaping.
const doc = this.documentation ?
`,"documentation":"${this.documentation}"` : "";
return `{"ns":"${escapeString(this.ns)}","name":"${this.name}"${doc}}`;
}
simple() {
return true;
}
toArray() {
return [this];
}
_recordNamespaces(namespaces, recordEmpty) {
if (this.ns === "" && !recordEmpty) {
return;
}
namespaces.add(this.ns);
}
}
exports.Name = Name;
/**
* Models the Relax NG ``<choice>`` element when it appears in a name
* class.
*/
class NameChoice extends Base {
/**
* @param a The first choice.
*
* @param b The second choice.
*/
constructor(a, b) {
super();
this.a = a;
this.b = b;
this.kind = "NameChoice";
}
/**
* Makes a tree of NameChoice objects out of a list of names.
*
* @param names The names from which to build a tree.
*
* @return If the list is a single name, then just that name. Otherwise,
* the names from the list in a tree of [[NameChoice]].
*/
static makeTree(names) {
if (names.length === 0) {
throw new Error("trying to make a tree out of nothing");
}
let ret;
if (names.length > 1) {
// More than one name left. Convert them to a tree.
let top = new NameChoice(names[0], names[1]);
for (let ix = 2; ix < names.length; ix++) {
top = new NameChoice(top, names[ix]);
}
ret = top;
}
else {
// Only one name: we can use it as-is.
ret = names[0];
}
return ret;
}
match(ns, name) {
return this.a.match(ns, name) || this.b.match(ns, name);
}
intersects(other) {
return this.a.intersects(other) || this.b.intersects(other);
}
intersection(other) {
if (other === 0) {
return 0;
}
const a = this.a.intersection(other);
const b = this.b.intersection(other);
return a === 0 ? b : (b === 0 ? a : new NameChoice(a, b));
}
/**
* Recursively apply a transformation to a NameChoice tree.
*
* @param fn The transformation to apply. It may return 0 to indicate that the
* child has been transformed to the empty set.
*
* @returns The transformed tree, or 0 if the tree has been transformed to
* nothing.
*/
applyRecursively(fn) {
const { a, b } = this;
const newA = isNameChoice(a) ? a.applyRecursively(fn) : fn(a);
const newB = isNameChoice(b) ? b.applyRecursively(fn) : fn(b);
return newA === 0 ? newB :
(newB === 0 ? newA : new NameChoice(newA, newB));
}
wildcardMatch(ns, name) {
return this.a.wildcardMatch(ns, name) || this.b.wildcardMatch(ns, name);
}
toObject() {
return {
a: this.a.toObject(),
b: this.b.toObject(),
};
}
asString() {
return `{"a":${this.a.toString()},"b":${this.b.toString()}}`;
}
simple() {
return this.a.simple() && this.b.simple();
}
toArray() {
const aArr = this.a.toArray();
if (aArr === null) {
return null;
}
const bArr = this.b.toArray();
return bArr === null ? null : aArr.concat(bArr);
}
_recordNamespaces(namespaces, recordEmpty) {
this.a._recordNamespaces(namespaces, recordEmpty);
this.b._recordNamespaces(namespaces, recordEmpty);
}
}
exports.NameChoice = NameChoice;
/**
* Models the Relax NG ``<nsName>`` element.
*/
class NsName extends Base {
/**
* @param ns The namespace URI for this name. Corresponds to the ``ns``
* attribute in the simplified Relax NG syntax.
*
* @param except Corresponds to an ``<except>`` element appearing as a child
* of the ``<nsName>`` element in the Relax NG schema.
*/
constructor(ns, except) {
super();
this.ns = ns;
this.except = except;
this.kind = "NsName";
}
match(ns, name) {
return this.ns === ns && !(this.except !== undefined &&
this.except.match(ns, name));
}
intersects(other) {
if (isName(other)) {
return this.ns === other.ns &&
(this.except === undefined || !other.intersects(this.except));
}
return isNsName(other) ? this.ns === other.ns :
// Delegate the logic to the other classes.
other.intersects(this);
}
intersection(other) {
if (other === 0) {
return 0;
}
if (isName(other)) {
if (this.ns !== other.ns) {
return 0;
}
if (this.except !== undefined) {
// We're computing other - other ^ this.except
//
// Since other is a single name, there are only two possible values for
// other ^ this.except: other and 0.
//
// Consequently the whole equation can also only resolve to other and 0.
return other.intersection(this.except) === 0 ? other : 0;
}
return other;
}
if (isNsName(other)) {
if (this.ns !== other.ns) {
return 0;
}
if (this.except !== undefined && other.except !== undefined) {
// We have to create a new except that does not duplicate exceptions.
// For instance if this excepts {q}foo and other excepts {q}foo too, we
// don't want to have an exception that has {q}foo twice.
// Due to Relax NG restrictions on NsName, both excepts necessarily
// contain only Name or NameChoice elements.
const theseNames = this.except.toArray();
const otherNames = other.except.toArray();
// And so these cannot be null.
if (theseNames === null || otherNames === null) {
throw new Error("complex pattern found in NsName except");
}
// Find the unique names.
const names = Array.from(new Map(theseNames.concat(otherNames)
.map(name => [`{${name.ns}}${name.name}`, name])).values());
return new NsName(this.ns, NameChoice.makeTree(names));
}
return (other.except !== undefined) ? other : this;
}
// Delegate the logic to the other classes.
return other.intersection(this);
}
/**
* Subtract a [[Name]] or [[NsName]] from this one, or a [[NameChoice]] that
* contains only a mix of these two. We support subtracting only these two
* types because only these two cases are required by Relax NG.
*
* @param other The object to subtract from this one.
*
* @returns An object that represents the subtraction. If the result is the
* empty set, 0 is returned.
*/
subtract(other) {
if (isNameChoice(other)) {
// x - (a U b) = x - a U x - b
return other.applyRecursively(child => {
if (!(isName(child) || isNsName(child))) {
throw new Error("child is not Name or NsName");
}
return this.subtract(child);
});
}
if (this.ns !== other.ns) {
return this;
}
if (isName(other)) {
return new NsName(this.ns, this.except === undefined ?
other :
new NameChoice(this.except, other));
}
if (other.except === undefined) {
return 0;
}
if (this.except === undefined) {
return other.except;
}
// Otherwise, return other.except - this.except. Yes, the order is
// correct.
// Due to Relax NG restrictions on NsName, both excepts may contain only
// Name or NameChoice, so only simple patterns that can be converted to
// arrays.
const theseNames = this.except.toArray();
const otherNames = other.except.toArray();
if (theseNames === null || otherNames === null) {
throw new Error("NsName contains an except pattern which is not simple.");
}
const result = otherNames
.filter(name => !theseNames.some(thisName => name.ns === thisName.ns &&
name.name === thisName.name));
return result.length === 0 ? 0 : NameChoice.makeTree(result);
}
wildcardMatch(ns, name) {
return this.match(ns, name);
}
toObject() {
const ret = {
ns: this.ns,
};
if (this.except !== undefined) {
ret.except = this.except.toObject();
}
return ret;
}
asString() {
const except = this.except === undefined ? "" :
`,"except":${this.except.toString()}`;
return `{"ns":"${escapeString(this.ns)}"${except}}`;
}
simple() {
return false;
}
toArray() {
return null;
}
_recordNamespaces(namespaces, recordEmpty) {
if (this.ns !== "" || recordEmpty) {
namespaces.add(this.ns);
}
if (this.except !== undefined) {
namespaces.add("::except");
}
}
}
exports.NsName = NsName;
/**
* Models the Relax NG ``<anyName>`` element.
*/
class AnyName extends Base {
/**
* @param except Corresponds to an ``<except>`` element appearing as a child
* of the ``<anyName>`` element in the Relax NG schema.
*/
constructor(except) {
super();
this.except = except;
this.kind = "AnyName";
}
match(ns, name) {
return (this.except === undefined) || !this.except.match(ns, name);
}
intersects(other) {
if (this.except === undefined || isAnyName(other)) {
return true;
}
if (isName(other)) {
return !this.except.intersects(other);
}
if (isNsName(other)) {
// Reminder: the except can only be one of three things: Name, NsName or
// NameChoice so negation can only be 0, Name, NsName or NameChoice.
const negation = this.except.intersection(other);
//
// We've commented out this test. The simplification/validation/checking
// logic prevents us from failing this test.
//
// if (!(negation === 0 || isName(negation) || isNsName(negation) ||
// isNameChoice(negation))) {
// throw new Error("negation should be 0, Name, NsName or NameChoice");
// }
//
// In the absence of the test above, we must assert the type of negation
// here.
return negation === 0 ||
other.subtract(negation) !== 0;
}
throw new Error("cannot compute intersection!");
}
intersection(other) {
if (other === 0) {
return 0;
}
if (this.except === undefined) {
return other;
}
if (isName(other)) {
return this.except.intersection(other) === 0 ? other : 0;
}
if (isNsName(other)) {
// Reminder: the except can only be one of three things: Name, NsName or
// NameChoice so negation can only be 0, Name, NsName or NameChoice.
const negation = this.except.intersection(other);
//
// We've commented out this test. The simplification/validation/checking
// logic prevents us from failing this test.
//
// if (!(negation === 0 || isName(negation) || isNsName(negation) ||
// isNameChoice(negation))) {
// throw new Error("negation should be 0, Name, NsName or NameChoice");
// }
//
// In the absence of the test above, we must assert the type of negation
// here.
return negation === 0 ? other :
other.subtract(negation);
}
if (isAnyName(other)) {
if (other.except !== undefined && this.except !== undefined) {
return new AnyName(new NameChoice(this.except, other.except));
}
return (other.except !== undefined) ? other : this;
}
throw new Error("cannot compute intersection!");
}
wildcardMatch(ns, name) {
return this.match(ns, name);
}
toObject() {
const ret = {
pattern: "AnyName",
};
if (this.except !== undefined) {
ret.except = this.except.toObject();
}
return ret;
}
asString() {
const except = this.except === undefined ? "" :
`,"except":${this.except.toString()}`;
return `{"pattern":"AnyName"${except}}`;
}
simple() {
return false;
}
toArray() {
return null;
}
_recordNamespaces(namespaces, _recordEmpty) {
namespaces.add("*");
if (this.except !== undefined) {
namespaces.add("::except");
}
}
}
exports.AnyName = AnyName;
// LocalWords: MPL NG Stringify stringified AnyName
//# sourceMappingURL=name_patterns.js.map