+ * A `Tag` constructor function.
+ *
+ * This class extends the identity function, meaning that the object
+ * provided as the constructor is used verbatim (with new private
+ * fields added).
+ *
+ * ※ The instance methods of this class are provided as static
+ * methods on the superclass which all `Tag` constructors inherit
+ * from.
+ *
+ * ※ This class is not exposed.
+ */
+ TagConstructor,
+
+ /**
+ * The exposed constructor function from which all `Tag` constructors
+ * inherit.
+ *
+ * ☡ This constructor always throws.
+ */
+ TagSuper,
+} = (() => {
+ const tagConstructorBehaviours = Object.create(null);
+ return {
+ TagConstructor: class extends identity {
+ /**
+ * The `TagSystem` used for `Tag`s constructed by this
+ * constructor.
+ */
+ #system;
+
+ /** The `Storage` managed by this constructor’s `TagSystem`. */
+ #storage;
+
+ /** The schema in use for this constructor. */
+ #schema;
+
+ /**
+ * Constructs a new `Tag` constructor by adding the appropriate
+ * private fields to the provided constructor, setting its
+ * prototype, and then returning it.
+ *
+ * ※ This constructor does not modify the `name` or `prototype`
+ * properties of the provided constructor.
+ *
+ * ※ See `Tag.For`, where this constructor is used.
+ */
+ constructor(constructor, system, storage, schema) {
+ super(constructor);
+ Object.setPrototypeOf(this, TagSuper);
+ this.#system = system;
+ this.#storage = storage;
+ this.#schema = schema;
+ }
+
+ static {
+ // Define the superclass constructor which all `Tag`
+ // constructors will inherit from.
+ const superclass = tagConstructorBehaviours.TagSuper =
+ function Tag() {
+ throw new TypeError("Tags must belong to a System.");
+ };
+ const { prototype: staticFeatures } = this;
+ delete staticFeatures.constructor;
+ Object.defineProperty(superclass, "prototype", {
+ configurable: false,
+ enumerable: false,
+ value: Tag.prototype,
+ writable: false,
+ });
+ Object.defineProperties(
+ superclass,
+ Object.getOwnPropertyDescriptors(staticFeatures),
+ );
+ }
+
+ /**
+ * Yields the tags in the `TagSystem` associated with this
+ * constructor.
+ */
+ *all() {
+ const system = this.#system;
+ const storage = this.#storage;
+ for (const instance of storage.values()) {
+ // Iterate over the entries and yield the ones which are
+ // `Tag`s in this `TagSystem`.
+ if (Tag.getSystem(instance) == system) {
+ // The current instance is a `Tag` in this `TagSystem`.
+ yield instance;
+ } else {
+ // The current instance is not a `Tag` in this
+ // `TagSystem`.
+ /* do nothing */
+ }
+ }
+ }
+
+ /**
+ * Returns a new `Tag` resolved from the provided I·R·I.
+ *
+ * ☡ This function throws if the I·R·I is not in the `.iriSpace`
+ * of the `TagSystem` associated with this constructor.
+ *
+ * ※ If the I·R·I is not recognized, this function returns
+ * `undefined`.
+ */
+ fromIRI(iri) {
+ const system = this.#system;
+ const storage = this.#storage;
+ const name = `${iri}`;
+ const prefix = `${system.iriSpace}`;
+ if (!name.startsWith(prefix)) {
+ // The I·R·I does not begin with the expected prefix.
+ throw new RangeError(
+ `I·R·I did not begin with the expected prefix: ${iri}`,
+ );
+ } else {
+ // The I·R·I begins with the expected prefix.
+ const identifier = name.substring(prefix.length);
+ try {
+ // Attempt to resolve the identifier.
+ const instance = storage.get(identifier);
+ return Tag.getSystem(instance) == system
+ ? instance
+ : undefined;
+ } catch {
+ // Do not throw for bad identifiers.
+ return undefined;
+ }
+ }
+ }
+
+ /**
+ * Returns a new `Tag` resolved from the provided identifier.
+ *
+ * ☡ This function throws if the identifier is invalid.
+ *
+ * ※ If the identifier is valid but not recognized, this
+ * function returns `undefined`.
+ */
+ fromIdentifier(identifier) {
+ const system = this.#system;
+ const storage = this.#storage;
+ const instance = storage.get(identifier);
+ return Tag.getSystem(instance) == system
+ ? instance
+ : undefined;
+ }
+
+ /**
+ * Returns a new `Tag` resolved from the provided Tag U·R·I.
+ *
+ * ☡ This function throws if the provided Tag U·R·I does not
+ * match the tagging entity of this constructor’s `TagSystem`.
+ *
+ * ※ If the specific component of the Tag U·R·I is not
+ * recognized, this function returns `undefined`.
+ */
+ fromTagURI(tagURI) {
+ const system = this.#system;
+ const storage = this.#storage;
+ const tagName = `${tagURI}`;
+ const tagPrefix = `tag:${system.taggingEntity}:`;
+ if (!tagName.startsWith(tagPrefix)) {
+ // The Tag U·R·I does not begin with the expected prefix.
+ throw new RangeError(
+ `Tag U·R·I did not begin with the expected prefix: ${tagURI}`,
+ );
+ } else {
+ // The I·R·I begins with the expected prefix.
+ const identifier = tagName.substring(tagPrefix.length);
+ try {
+ // Attempt to resolve the identifier.
+ const instance = storage.get(identifier);
+ return Tag.getSystem(instance) == system
+ ? instance
+ : undefined;
+ } catch {
+ // Do not throw for bad identifiers.
+ return undefined;
+ }
+ }
+ }
+
+ /**
+ * Yields the tag identifiers in the `TagSystem` associated with
+ * this constructor.
+ */
+ *identifiers() {
+ const system = this.#system;
+ const storage = this.#storage;
+ for (const [identifier, instance] of storage.entries()) {
+ // Iterate over the entries and yield the ones which are
+ // `Tag`s in this `TagSystem`.
+ if (Tag.getSystem(instance) == system) {
+ // The current instance is a `Tag` in this `TagSystem`.
+ yield identifier;
+ } else {
+ // The current instance is not a `Tag` in this `TagSystem`.
+ /* do nothing */
+ }
+ }
+ }
+
+ /** Returns the `TagSystem` for this `Tag` constructor. */
+ get system() {
+ return this.#system;
+ }
+
+ /**
+ * Returns a new `Tag` constructed from the provided data and
+ * with the provided identifier.
+ *
+ * ※ This function is not really intended for public usage.
+ */
+ [Storage.toInstance](data, identifier) {
+ const tag = new this(data.kind);
+ return Tag.assignData(tag, data, identifier);
+ }
+ },
+ TagSuper: tagConstructorBehaviours.TagSuper,
+ };
+})();
+
+const {
+ /**
+ * Returns whether the provided schema, subject class, object
+ * property, and object class are consistent.
+ *
+ * This is hardly a full reasoner; it is tuned to the abilites and
+ * needs of this module.
+ */
+ isObjectPredicateOK,
+} = (() => {
+ const cachedClassAndSuperclasses = new WeakMap();
+ const cachedClassRestrictions = new WeakMap();
+ const cachedPredicateRestrictions = new WeakMap();
+
+ const classAndSuperclasses = function* (
+ classes,
+ baseClass,
+ touched = new Set(),
+ ) {
+ if (baseClass == "Thing" || touched.has(baseClass)) {
+ /* do nothing */
+ } else {
+ yield baseClass;
+ touched.add(baseClass);
+ const subClassOf = classes[baseClass]?.subClassOf ?? "Thing";
+ for (
+ const superclass of (
+ typeof subClassOf == "string"
+ ? [subClassOf]
+ : Array.from(subClassOf)
+ ).filter(($) => typeof $ == "string")
+ ) {
+ yield* classAndSuperclasses(classes, superclass, touched);
+ }
+ }
+ };
+
+ const getClassAndSuperclasses = (schema, baseClass) => {
+ const schemaCache = cachedClassAndSuperclasses.get(schema);
+ const cached = schemaCache?.[baseClass];
+ if (cached != null) {
+ return cached;
+ } else {
+ const { classes } = schema;
+ const result = [...classAndSuperclasses(classes, baseClass)];
+ if (schemaCache) {
+ schemaCache[baseClass] = result;
+ } else {
+ cachedClassRestrictions.set(
+ schema,
+ Object.assign(Object.create(null), { [baseClass]: result }),
+ );
+ }
+ return result;
+ }
+ };
+
+ const getClassRestrictions = (schema, domain) => {
+ const schemaCache = cachedClassRestrictions.get(schema);
+ const cached = schemaCache?.[domain];
+ if (cached != null) {
+ return cached;
+ } else {
+ const { classes } = schema;
+ const restrictions = Object.create(null);
+ const subClassOf = classes[domain]?.subClassOf ?? "Thing";
+ for (
+ const superclass of (
+ typeof subClassOf == "string"
+ ? [subClassOf]
+ : Array.from(subClassOf)
+ ).filter(($) => Object($) === $)
+ ) {
+ const { onProperty, allValuesFrom } = superclass;
+ restrictions[onProperty] = processSpace(allValuesFrom);
+ }
+ if (schemaCache) {
+ schemaCache[domain] = restrictions;
+ } else {
+ cachedClassRestrictions.set(
+ schema,
+ Object.assign(Object.create(null), {
+ [domain]: restrictions,
+ }),
+ );
+ }
+ return restrictions;
+ }
+ };
+
+ const getPredicateRestrictions = (schema, predicate) => {
+ const schemaCache = cachedPredicateRestrictions.get(schema);
+ const cached = schemaCache?.[predicate];
+ if (cached != null) {
+ return cached;
+ } else {
+ const { objectProperties } = schema;
+ const restrictions = [
+ ...predicateRestrictions(objectProperties, predicate),
+ ].reduce(
+ (result, { domainIntersection, rangeIntersection }) => {
+ result.domainIntersection.push(...domainIntersection);
+ result.rangeIntersection.push(...rangeIntersection);
+ return result;
+ },
+ Object.assign(Object.create(null), {
+ domainIntersection: [],
+ rangeIntersection: [],
+ }),
+ );
+ if (schemaCache) {
+ schemaCache[predicate] = restrictions;
+ } else {
+ cachedPredicateRestrictions.set(
+ schema,
+ Object.assign(Object.create(null), {
+ [predicate]: restrictions,
+ }),
+ );
+ }
+ return restrictions;
+ }
+ };
+
+ const processSpace = (space) =>
+ Object(space) === space
+ ? "length" in space
+ ? Array.from(
+ space,
+ (subspace) =>
+ Object(subspace) === subspace
+ ? Array.from(subspace.unionOf)
+ : [subspace],
+ )
+ : [Array.from(space.unionOf)]
+ : [[space]];
+
+ const predicateRestrictions = function* (
+ objectProperties,
+ predicate,
+ touched = new Set(),
+ ) {
+ if (predicate == "Property" || touched.has(predicate)) {
+ /* do nothing */
+ } else {
+ const { domain, range, subPropertyOf } =
+ objectProperties[predicate];
+ yield Object.assign(Object.create(null), {
+ domainIntersection: processSpace(domain ?? "Thing"),
+ rangeIntersection: processSpace(range ?? "Thing"),
+ });
+ touched.add(predicate);
+ for (
+ const superproperty of (
+ subPropertyOf == null
+ ? ["Property"]
+ : typeof subPropertyOf == "string"
+ ? [subPropertyOf]
+ : Array.from(subPropertyOf)
+ )
+ ) {
+ yield* predicateRestrictions(
+ objectProperties,
+ superproperty,
+ touched,
+ );
+ }
+ }
+ };
+
+ return {
+ isObjectPredicateOK: (
+ schema,
+ subjectClass,
+ predicate,
+ objectClass,
+ ) => {
+ const { objectProperties } = schema;
+ const predicateDefinition = objectProperties[predicate];
+ const isInverse = "inverseOf" in predicateDefinition;
+ const usedPredicate = isInverse
+ ? predicateDefinition.inverseOf
+ : predicate;
+ const domain = isInverse ? objectClass : subjectClass;
+ const domains = new Set(getClassAndSuperclasses(schema, domain));
+ const ranges = new Set(getClassAndSuperclasses(
+ schema,
+ isInverse ? subjectClass : objectClass,
+ ));
+ const predicateRestrictions = getPredicateRestrictions(
+ schema,
+ usedPredicate,
+ );
+ const { domainIntersection } = predicateRestrictions;
+ const rangeIntersection = [
+ ...predicateRestrictions.rangeIntersection,
+ ...function* () {
+ for (const domain of domains) {
+ const classRestrictionOnPredicate =
+ getClassRestrictions(schema, domain)[usedPredicate];
+ if (classRestrictionOnPredicate != null) {
+ yield* classRestrictionOnPredicate;
+ } else {
+ /* do nothing */
+ }
+ }
+ }(),
+ ];
+ return domainIntersection.every((domainUnion) =>
+ domainUnion.some((domain) =>
+ domain == "Thing" || domains.has(domain)
+ )
+ ) &&
+ rangeIntersection.every((rangeUnion) =>
+ rangeUnion.some((range) =>
+ range == "Thing" || ranges.has(range)
+ )
+ );
+ },
+ };
+})();
+
+const {
+ /**
+ * Returns the provided value converted into a `String` object with
+ * `.["@value"]` and `.["@language"]` properties.
+ *
+ * The same object will be returned for every call with an equivalent
+ * value.