// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+import { identity } from "./deps.js";
import { Storage } from "./memory.js";
import { taggingDiscoveryContext } from "./names.js";
-
-// TODO: Move these somewhere else and allow for modification before
-// they are used, freezing them only once tags actually start being
-// constructed (probably on first call to the `TagSystem` initializer
-// for convenience).
-//
-// Or else make them properties of the tag system itself and ∼fully
-// modifiable.
-
-/**
- * Tag kinds which denote entity tags.
- *
- * ※ This object is not exposed.
- */
-const ENTITY_TAG_KINDS = new Set([
- "EntityTag",
- "CharacterTag",
- "InanimateEntityTag",
-]);
-
-/**
- * Tag kinds which denote relationship tags.
- *
- * ※ This object is not exposed.
- */
-const RELATIONSHIP_TAG_KINDS = new Set([
- "RelationshipTag",
- "FamilialRelationship Tag",
- "FriendshipTag",
- "RivalryTag",
- "RomanticRelationshipTag",
- "SexualRelationshipTag",
-]);
-
-/**
- * Tag kinds which denote setting tags.
- *
- * ※ This object is not exposed.
- */
-const SETTING_TAG_KINDS = new Set([
- "SettingTag",
- "LocationTag",
- "TimePeriodTag",
- "UniverseTag",
-]);
-
-/**
- * Tag kinds which denote conceptual tags.
- *
- * ※ This object is not exposed.
- */
-const CONCEPTUAL_TAG_KINDS = new Set(function* () {
- yield "ConceptualTag";
- yield* RELATIONSHIP_TAG_KINDS;
-}());
-
-/**
- * All recognized tag kinds.
- *
- * ※ This object is not exposed.
- */
-const TAG_KINDS = new Set(function* () {
- yield "Tag";
- yield "CanonTag";
- yield* CONCEPTUAL_TAG_KINDS;
- yield* ENTITY_TAG_KINDS;
- yield "GenreTag";
- yield* SETTING_TAG_KINDS;
-}());
-
-/**
- * Tag kinds which can be in canon.
- *
- * ※ This object is not exposed.
- */
-const HAS_IN_CANON = new Set(function* () {
- yield* ENTITY_TAG_KINDS;
- yield* SETTING_TAG_KINDS;
-}());
-
-/**
- * Tag kinds which can be involved in relationship tags.
- *
- * ※ This object is not exposed.
- */
-const INVOLVABLE_IN_RELATIONSHIP = new Set(function* () {
- yield "CharacterTag";
- yield* RELATIONSHIP_TAG_KINDS;
-}());
-
-/**
- * Properties which take literal values instead of identifiers.
- *
- * These are the label terms.
- */
-const LITERAL_TERMS = new Set([
- "prefLabel",
- "altLabel",
- "hiddenLabel",
-]);
-
-/**
- * Properties to skip when diffing.
- *
- * These are all inverses of properties included in diffs and cannot be
- * changed manually.
- */
-const SKIP_IN_DIFF = new Set([
- "hasInCanon",
- "isIncludedIn",
- "narrower",
-]);
+import schema from "./schema.js";
/**
* A tag.
/** The `Storage` managed by this `Tag`’s `TagSystem`. */
#storage;
+ /** The schema in use for this `Tag`. */
+ #schema;
+
/**
* The 30‐bit W·R·M·G base32 identifier with leading checksum which
* has been assigned to this `Tag`.
#data = tagData();
/**
- * Returns whether or not the provided value is a tag which shares a
- * storage with this tag.
- *
- * Sharing a storage also implies sharing a `TagSystem`.
- */
- #isTagInStorage($) {
- try {
- // Try to compare the provided value’s internal store with
- // the provided storage.
- return $.#storage == this.#storage;
- } catch {
- // The provided value was not a `Tag`.
- return false;
- }
- }
-
- /**
- * Constructs a new `Tag` of the provided kind and with the provided
- * preferred label.
- *
- * ※ The first two arguments of this constructor are bound when
- * generating the value of `TagSystem::Tag`. It isn’t possible to
- * access this constructor in its unbound form from outside this
- * module.
- *
- * ☡ This constructor throws if the provided kind is not supported.
+ * Adds the provided label(s) to this `Tag` as the provided
+ * predicate, then returns this `Tag`.
*/
- constructor(system, storage, kind = "Tag", prefLabel = "") {
- const kindString = `${kind}`;
- if (TAG_KINDS.has(kindString)) {
- // The provided kind is one of the recognized tag kinds.
- this.#system = system;
- this.#storage = storage;
- this.#kind = kindString;
- this.#data.prefLabel = prefLabel;
- } else {
- // The provided kind is not supported.
- throw new RangeError(
- `Cannot construct Tag: Unrecognized kind: ${kind}.`,
- );
- }
- }
-
- /**
- * Yields the tags in the `TagSystem` associated with this
- * constructor.
- *
- * ※ The first two arguments of this function are bound when
- * generating the value of `TagSystem::Tag`. It isn’t possible to
- * access this function in its unbound form from outside this module.
- */
- static *all(system, 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.
- *
- * ※ The first two arguments of this function are bound when
- * generating the value of `TagSystem::Tag`. It isn’t possible to
- * access this function in its unbound form from outside this module.
- *
- * ☡ 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 `null`.
- */
- static fromIRI(system, storage, iri) {
- 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 : null;
- } catch {
- // Do not throw for bad identifiers.
- return null;
- }
- }
- }
-
- /**
- * Returns a new `Tag` resolved from the provided identifier.
- *
- * ※ The first two arguments of this function are bound when
- * generating the value of `TagSystem::Tag`. It isn’t possible to
- * access this function in its unbound form from outside this module.
- *
- * ☡ This function throws if the identifier is invalid.
- *
- * ※ If the identifier is valid but not recognized, this function
- * returns `null`.
- */
- static fromIdentifier(system, storage, identifier) {
- const instance = storage.get(identifier);
- return Tag.getSystem(instance) == system ? instance : null;
- }
-
- /**
- * Returns a new `Tag` resolved from the provided Tag U·R·I.
- *
- * ※ The first two arguments of this function are bound when
- * generating the value of `TagSystem::Tag`. It isn’t possible to
- * access this function in its unbound form from outside this module.
- *
- * ☡ 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 `null`.
- */
- static fromTagURI(system, storage, tagURI) {
- 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 : null;
- } catch {
- // Do not throw for bad identifiers.
- return null;
- }
- }
- }
-
- /**
- * Returns the `TagSystem` that the provided value belongs to.
- *
- * ※ This function can be used to check if the provided value has
- * private tag features.
- *
- * ※ This function is not exposed.
- */
- static getSystem($) {
- return !(#system in Object($)) ? null : $.#system;
- }
-
- /**
- * Yields the tag identifiers in the `TagSystem` associated with this
- * constructor.
- *
- * ※ The first two arguments of this function are bound when
- * generating the value of `TagSystem::Tag`. It isn’t possible to
- * access this function in its unbound form from outside this module.
- */
- static *identifiers(system, 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 a new `Tag` constructed from the provided data and with
- * the provided identifier.
- *
- * ※ This function will not work if called directly from `Tag` (and
- * nor is it available *to* be called as such from outside this
- * module). It must be called from a `TagSystem::Tag` bound
- * constructor.
- *
- * ※ This function is not really intended for public usage.
- */
- static [Storage.toInstance](_system, _storage, data, identifier) {
- const tag = new this(data.kind);
- tag.#identifier = `${identifier}`;
- tag.#persistedData = tagData(data);
- tag.#data = tagData(data);
- return tag;
- }
-
- static {
- // Overwrite the default `::constructor` method to instead give the
- // actual (bound) constructor which was used to generate a given
- // `Tag`.
- Object.defineProperties(this.prototype, {
- constructor: {
- configurable: true,
- enumerable: false,
- get() {
- // All `Tag`s are constructed via the `.Tag` constructor
- // available in their `TagSystem`; return it.
- return this.#system.Tag;
- },
- set: undefined,
- },
- });
- }
-
- /**
- * Adds the provided label(s) to this `Tag` as alternate labels, then
- * returns this `Tag`.
- */
- addAltLabel(...labels) {
- const altLabels = this.#data.altLabel;
- let objectLabels = null; // initialized on first use
+ #addLabel(predicate, ...labels) {
+ const values = this.#data[predicate];
for (const $ of labels) {
// Iterate over each provided label and attempt to add it.
const literal = langString($);
- if (Object(literal) === literal) {
- // The current label is a language‐tagged string.
- objectLabels ??= [...function* () {
- for (const altLabel of altLabels) {
- // Iterate over the existing labels and yield the
- // language‐tagged strings.
- if (Object(altLabel) === altLabel) {
- // The current existing label is a language‐tagged
- // string.
- yield altLabel;
- } else {
- // The current existing label is not a language‐tagged
- // string.
- /* do nothing */
- }
- }
- }()];
- if (
- objectLabels.some((objectLabel) =>
- objectLabel["@value"] == literal["@value"] &&
- objectLabel["@language"] == literal["@language"]
- )
- ) {
- // There is a match with the current label in the existing
- // labels.
- /* do nothing */
- } else {
- // There is no match and this label must be added.
- altLabels.add(literal);
- objectLabels.push(literal);
- }
- } else {
- // The current label is a simple string.
- altLabels.add(literal);
- }
+ values.add(literal);
}
return this;
}
/**
* Adds the provided tags to the list of tags that this `Tag` is
- * narrower than, then returns this `Tag`.
+ * related to by the provided predicate, then returns this `Tag`.
*
* Arguments may be string identifiers or objects with an
* `.identifier` property.
*/
- addBroaderTag(...tags) {
+ #addTag(predicate, ...tags) {
const storage = this.#storage;
- const broader = this.#data.broader;
+ const values = this.#data[predicate];
for (const $ of tags) {
- // Iterate over each tag and attempt to set it as broader than
- // this `Tag`.
+ // Iterate over each tag and attempt to state the predicate.
const identifier = toIdentifier($);
if (identifier == null) {
// ☡ The current tag has no identifier.
throw new TypeError(
- "Cannot assign broader to Tag: Identifier must not be nullish.",
+ `Cannot state ${predicate} of Tag: Identifier must not be nullish.`,
);
- } else if (broader.has(identifier)) {
- // Short‐circuit: The identifier is already something this
- // `Tag` is narrower than.
+ } else if (values.has(identifier)) {
+ // Short‐circuit: The identifier has already been stated with
+ // this predicate.
/* do nothing */
} else {
- // The current tag has an identifier.
+ // The current tag has an identifier, but it hasn’t been stated
+ // with this predicate yet.
const tag = storage.get(identifier);
if (tag == null) {
// ☡ The current tag has not been persisted to this `Tag`’s
// storage.
throw new RangeError(
- `Cannot assign broader to Tag: Identifier is not persisted: ${identifier}.`,
+ `Cannot state ${predicate} of Tag: Identifier is not persisted: ${identifier}.`,
);
} else if (!this.#isTagInStorage(tag)) {
// ☡ The current tag is not a tag in the correct tag system.
throw new TypeError(
- `Cannot assign broader to Tag: Tags must be from the same Tag System, but got: ${identifier}.`,
+ `Cannot state ${predicate} of Tag: Tags must be from the same Tag System, but got: ${identifier}.`,
+ );
+ } else if (
+ !isObjectPredicateOK(
+ this.#schema,
+ this.#kind,
+ predicate,
+ tag.#kind,
+ )
+ ) {
+ // ☡ This tag and the current tag form an invalid pair for
+ // this predicate.
+ throw new TypeError(
+ `Cannot state ${predicate} of Tag: Not valid for domain and range: ${this.#kind}, ${tag.#kind}.`,
);
} else {
// The current tag is a tag in the correct tag system; add
// its identifier.
- broader.add(identifier);
+ values.add(identifier);
}
}
}
}
/**
- * Adds the provided label(s) to this `Tag` as hidden labels, then
- * returns this `Tag`.
+ * Removes the provided string label(s) from this `Tag` as the
+ * provided predicate, then returns this `Tag`.
*/
- addHiddenLabel(...labels) {
- const hiddenLabels = this.#data.hiddenLabel;
- let objectLabels = null; // initialized on first use
+ #deleteLabel(predicate, ...labels) {
+ const values = this.#data[predicate];
for (const $ of labels) {
- // Iterate over each provided label and attempt to add it.
+ // Iterate over each provided label and attempt to remove it.
const literal = langString($);
- if (Object(literal) === literal) {
- // The current label is a language‐tagged string.
- objectLabels ??= [...function* () {
- for (const hiddenLabel of hiddenLabels) {
- // Iterate over the existing labels and yield the
- // language‐tagged strings.
- if (Object(hiddenLabel) === hiddenLabel) {
- // The current existing label is a language‐tagged
- // string.
- yield hiddenLabel;
- } else {
- // The current existing label is not a language‐tagged
- // string.
- /* do nothing */
- }
- }
- }()];
- if (
- objectLabels.some((objectLabel) =>
- objectLabel["@value"] == literal["@value"] &&
- objectLabel["@language"] == literal["@language"]
- )
- ) {
- // There is a match with the current label in the existing
- // labels.
- /* do nothing */
- } else {
- // There is no match and this label must be added.
- hiddenLabels.add(literal);
- objectLabels.push(literal);
- }
- } else {
- // The current label is a simple string.
- hiddenLabels.add(literal);
- }
+ values.delete(literal);
}
return this;
}
/**
- * Adds the provided tags to the list of tags that this `Tag` is in
- * canon with, then returns this `Tag`.
+ * Removes the provided tags from the list of tags that this `Tag` is
+ * related to by the provided predicate, then returns this `Tag`.
*
* Arguments may be string identifiers or objects with an
* `.identifier` property.
- *
- * ☡ This method will throw if a provided argument does not indicate
- * a canon tag, or if this `Tag` is not of a kind which can be placed
- * in canon.
*/
- addInCanonTag(...tags) {
- const storage = this.#storage;
- const kind = this.#kind;
- const inCanon = this.#data.inCanon;
- if (!HAS_IN_CANON.has(kind)) {
- // ☡ This is not an entity tag, setting tag, or recognized
- // subclass.
- throw new TypeError(
- `Cannot put Tag in canon: Incorrect Tag type: ${kind}.`,
- );
- } else {
- // This has a kind which can be placed in canon.
- for (const $ of tags) {
- // Iterate over each tag and attempt to set this `Tag` in canon
- // of it.
- const identifier = toIdentifier($);
- if (identifier == null) {
- // ☡ The current tag has no identifier.
- throw new TypeError(
- "Cannot put Tag in canon: Identifier must not be nullish.",
- );
- } else if (inCanon.has(identifier)) {
- // Short‐circuit: The identifier is already something this
- // `Tag` is in canon of.
- /* do nothing */
- } else {
- // The current tag has an identifier.
- const tag = storage.get(identifier);
- if (tag == null) {
- // ☡ The current tag has not been persisted to this `Tag`’s
- // storage.
- throw new RangeError(
- `Cannot put Tag in canon: Identifier is not persisted: ${identifier}.`,
- );
- } else if (
- // ※ If the first check succeeds, then the current tag
- // must have `Tag` private class features.
- !this.#isTagInStorage(tag) || tag.#kind != "CanonTag"
- ) {
- // ☡ The current tag is not a canon tag in the correct
- // tag system.
- throw new TypeError(
- `Cannot put Tag in canon: Tags can only be in Canon Tags from the same Tag System, but got: ${identifier}.`,
- );
- } else {
- // The current tag is a canon tag in the correct tag
- // system; add its identifier.
- inCanon.add(identifier);
- }
- }
- }
+ #deleteTag(predicate, ...tags) {
+ const values = this.#data[predicate];
+ for (const $ of tags) {
+ // Iterate over the provided tags and delete them.
+ values.delete(toIdentifier($));
}
return this;
}
/**
- * Adds the provided tags to the list of tags that this `Tag`
- * involves, then returns this `Tag`.
- *
- * Arguments may be string identifiers or objects with an
- * `.identifier` property.
+ * Returns whether or not the provided value is a tag which shares a
+ * storage with this tag.
*
- * ☡ This method will throw if this `Tag` is not a conceptual tag, or
- * if this `Tag` is a relationship tag and a provided argument does
- * not indicate a character or relationship tag.
+ * Sharing a storage also implies sharing a `TagSystem`.
*/
- addInvolvesTag(...tags) {
- const storage = this.#storage;
- const kind = this.#kind;
- const involves = this.#data.involves;
- if (!CONCEPTUAL_TAG_KINDS.has(kind)) {
- // ☡ This is not a conceptual tag or recognized subclass.
- throw new TypeError(
- `Cannot involve Tag: Incorrect Tag type: ${kind}.`,
- );
- } else {
- // This is a conceptual tag.
- for (const $ of tags) {
- // Iterate over each tag and attempt to set this `Tag` as
- // involving it.
- const identifier = toIdentifier($);
- if (identifier == null) {
- // ☡ The current tag has no identifier.
- throw new TypeError(
- "Cannot involve Tag: Identifier must not be nullish.",
- );
- } else if (involves.has(identifier)) {
- // Short‐circuit: The identifier is already something this
- // `Tag` involves.
- /* do nothing */
- } else {
- // The current tag has an identifier.
- const tag = storage.get(identifier);
- if (tag == null) {
- // ☡ The current tag has not been persisted to this `Tag`’s
- // storage.
- throw new RangeError(
- `Cannot involve Tag: Identifier is not persisted: ${identifier}.`,
- );
- } else if (
- // ※ If the first check succeeds, then the current tag
- // must have `Tag` private class features.
- !this.#isTagInStorage(tag) ||
- RELATIONSHIP_TAG_KINDS.has(kind) &&
- !INVOLVABLE_IN_RELATIONSHIP.has(tag.#kind)
- ) {
- // ☡ The current tag is in the correct tag system and
- // includable.
- throw new TypeError(
- `Cannot involve Tag: Tags must be the same Tag System and involvable, but got: ${identifier}.`,
- );
- } else {
- // The current tag is an involvable tag in the correct tag
- // system; add its identifier.
- involves.add(identifier);
- }
- }
- }
+ #isTagInStorage($) {
+ try {
+ // Try to compare the provided value’s internal store with
+ // the provided storage.
+ return $.#storage == this.#storage;
+ } catch {
+ // The provided value was not a `Tag`.
+ return false;
}
- return this;
}
- /** Yields the alternative labels of this `Tag`. */
- *altLabels() {
- yield* this.#data.altLabel;
- }
-
- /** Returns the authority (domain) name for this `Tag`. */
- get authorityName() {
- return this.#system.authorityName;
+ /**
+ * Yields the labels of this `Tag` according to the provided
+ * predicate.
+ */
+ *#yieldLabels(predicate) {
+ yield* this.#data[predicate];
}
- /** Yields `Tag`s which are broader than this `Tag`. */
- *broaderTags() {
+ /**
+ * Yields the tags that this `Tag` is related to by the provided
+ * predicate.
+ */
+ *#yieldTags(predicate) {
const storage = this.#storage;
- for (const identifier of this.#data.broader) {
- // Iterate over the broader tags and yield them if possible.
+ for (const identifier of this.#data[predicate]) {
+ // Iterate over the tags in this predicate and yield them if
+ // possible.
const tag = storage.get(identifier);
- if (!this.#isTagInStorage(tag)) {
- // The broader tag no longer appears in storage; perhaps it was
- // deleted.
+ if (
+ !this.#isTagInStorage(tag) || !isObjectPredicateOK(
+ this.#schema,
+ this.#kind,
+ predicate,
+ tag.#kind,
+ )
+ ) {
+ // The tag no longer appears in storage or is not compatible;
+ // perhaps it was deleted.
/* do nothing */
} else {
- // The broader tag exists and is constructable from storage.
+ // The tag exists and is constructable from storage.
yield tag;
}
}
}
- /** Yields `Tag`s which are broader than this `Tag`, transitively. */
- *broaderTransitiveTags() {
+ /**
+ * Yields the tags that this `Tag` is related to by the provided
+ * predicate, figured transitively.
+ */
+ *#yieldTransitiveTags(transitivePredicate, basePredicate) {
const storage = this.#storage;
const encountered = new Set();
- let pending = new Set(this.#data.broader);
+ let pending = new Set(this.#data[basePredicate]);
while (pending.size > 0) {
- // Loop until all broader tags have been encountered.
+ // Loop until all tags of the predicate have been encountered.
const processing = pending;
pending = new Set();
for (const identifier of processing) {
- // Iterate over the broader tags and yield them if possible.
+ // Iterate over the tags and yield them if possible.
if (!encountered.has(identifier)) {
- // The broader tag has not been encountered before.
+ // The tag has not been encountered before.
encountered.add(identifier);
const tag = storage.get(identifier);
- if (!this.#isTagInStorage(tag)) {
- // The broader tag no longer appears in storage; perhaps it
- // was deleted.
+ if (
+ !this.#isTagInStorage(tag) || !isObjectPredicateOK(
+ this.#schema,
+ this.#kind,
+ transitivePredicate,
+ tag.#kind,
+ )
+ ) {
+ // The tag no longer appears in storage or is not
+ // compatible; perhaps it was deleted.
/* do nothing */
} else {
- // The broader tag exists and is constructable from
- // storage.
+ // The tag exists and is constructable from storage.
yield tag;
- for (const transitive of tag.#data.broader) {
- // Iterate over the broader tags of the current broader
- // tag and add them to pending as needed.
+ for (const transitive of tag.#data[basePredicate]) {
+ // Iterate over the nested tags of the current tag and
+ // add them to pending as needed.
if (!encountered.has(transitive)) {
- // The broader broader tag has not been encountered
- // yet.
+ // The nested tag has not been encountered yet.
pending.add(transitive);
} else {
- // The broader broader tag has already been
- // encountered.
+ // The nested tag has already been encountered.
/* do nothing */
}
}
}
} else {
- // The broader tag has already been encountered.
+ // The tag has already been encountered.
/* do nothing */
}
}
}
/**
- * Removes the provided string label(s) from this `Tag` as alternate
- * labels, then returns this `Tag`.
- */
- deleteAltLabel(...labels) {
- const altLabels = this.#data.altLabel;
- let objectLabels = null; // initialized on first use
- for (const $ of labels) {
- // Iterate over each provided label and attempt to remove it.
- const literal = langString($);
- if (Object(literal) === literal) {
- // The current label is a language‐tagged string.
- objectLabels ??= [...function* () {
- for (const altLabel of altLabels) {
- // Iterate over the existing labels and yield the
- // language‐tagged strings.
- if (Object(altLabel) === altLabel) {
- // The current existing label is a language‐tagged
- // string.
- yield altLabel;
- } else {
- // The current existing label is not a language‐tagged
- // string.
- /* do nothing */
- }
- }
- }()];
- const existing = objectLabels.find((objectLabel) =>
- objectLabel["@value"] == literal["@value"] &&
- objectLabel["@language"] == literal["@language"]
- );
- altLabels.delete(existing);
- } else {
- // The current label is a simple string.
- altLabels.delete(literal);
- }
- }
- return this;
- }
-
- /**
- * Removes the provided tags from the list of tags that this `Tag` is
- * narrower than, then returns this `Tag`.
+ * Constructs a new `Tag` of the provided kind and with the provided
+ * preferred label.
*
- * Arguments may be string identifiers or objects with an
- * `.identifier` property.
+ * ※ The first two arguments of this constructor are bound when
+ * generating the value of `TagSystem::Tag`. It isn’t possible to
+ * access this constructor in its unbound form from outside this
+ * module.
+ *
+ * ☡ This constructor throws if the provided kind is not supported.
*/
- deleteBroaderTag(...tags) {
- const broader = this.#data.broader;
- for (const $ of tags) {
- // Iterate over the provided tags and delete them.
- broader.delete(toIdentifier($));
+ constructor(system, storage, schema, kind = "Tag", prefLabel = "") {
+ this.#system = system;
+ this.#storage = storage;
+ this.#schema = schema;
+ const kindString = `${kind}`;
+ if (!(kindString in schema.classes)) {
+ // The provided kind is not supported.
+ throw new RangeError(
+ `Cannot construct Tag: Unrecognized kind: ${kind}.`,
+ );
+ } else {
+ // The provided kind is one of the recognized tag kinds.
+ this.#kind = kindString;
+ this.#data.prefLabel = prefLabel;
}
- return this;
}
/**
- * Removes the provided string label(s) from this `Tag` as hidden
- * labels, then returns this `Tag`.
+ * Returns a new `Tag` constructor for the provided system, storage,
+ * schema, created with an appropriate prototype for the properties
+ * so defined.
+ *
+ * ※ This function is not exposed.
*/
- deleteHiddenLabel(...labels) {
- const hiddenLabels = this.#data.hiddenLabel;
- let objectLabels = null; // initialized on first use
- for (const $ of labels) {
- // Iterate over each provided label and attempt to remove it.
- const literal = langString($);
- if (Object(literal) === literal) {
- // The current label is a language‐tagged string.
- objectLabels ??= [...function* () {
- for (const hiddenLabel of hiddenLabels) {
- // Iterate over the existing labels and yield the
- // language‐tagged strings.
- if (Object(hiddenLabel) === hiddenLabel) {
- // The current existing label is a language‐tagged
- // string.
- yield hiddenLabel;
- } else {
- // The current existing label is not a language‐tagged
- // string.
- /* do nothing */
- }
- }
- }()];
- const existing = objectLabels.find((objectLabel) =>
- objectLabel["@value"] == literal["@value"] &&
- objectLabel["@language"] == literal["@language"]
- );
- hiddenLabels.delete(existing);
- } else {
- // The current label is a simple string.
- hiddenLabels.delete(literal);
- }
- }
- return this;
+ static For(system, storage, schema) {
+ const {
+ objectProperties,
+ transitiveProperties,
+ dataProperties,
+ } = schema;
+ const constructor = function (...$s) {
+ return Reflect.construct(
+ Tag,
+ [system, storage, schema, ...$s],
+ new.target,
+ );
+ };
+ Object.defineProperties(constructor, {
+ name: { value: "TagSystem::Tag" },
+ prototype: {
+ configurable: false,
+ enumerable: false,
+ value: Object.create(
+ Tag.prototype,
+ Object.fromEntries(Array.from(
+ function* () {
+ for (const key in objectProperties) {
+ // Iterate over each object property and yield any
+ // necessary method definitions.
+ const {
+ inverseOf,
+ subPropertyOf,
+ } = objectProperties[key];
+ if (key in transitiveProperties) {
+ // The current key indicates a transitive property.
+ //
+ // Transitive property methods are added by their
+ // nontransitive subproperties.
+ /* do nothing */
+ } else {
+ // The current key does not indicate a transitive
+ // property.
+ yield [`${key}Tags`, function* () {
+ yield* this.#yieldTags(key);
+ }];
+ if (inverseOf == null) {
+ // The current key does not indicate an inverse
+ // property, so add and delete methods are also
+ // added.
+ const cased = key[0].toUpperCase() +
+ key.substring(1);
+ yield [`add${cased}Tag`, function (...tags) {
+ return this.#addTag(key, ...tags);
+ }];
+ yield [`delete${cased}Tag`, function (...tags) {
+ return this.#deleteTag(key, ...tags);
+ }];
+ } else {
+ // The current key indicates an inverse property,
+ // so no add and delete methods are necessary.
+ /* do nothing */
+ }
+ if (
+ subPropertyOf != null &&
+ subPropertyOf in transitiveProperties
+ ) {
+ // The current key indicates a subproperty of a
+ // transitive property; its method is also added.
+ yield [`${subPropertyOf}Tags`, function* () {
+ yield* this.#yieldTransitiveTags(
+ subPropertyOf,
+ key,
+ );
+ }];
+ } else {
+ // The current key does not indicate a subproperty
+ // of a transitive property.
+ /* do nothing */
+ }
+ }
+ }
+ for (const key in dataProperties) {
+ // Iterate over each data property and yield any
+ // necessary method definitions.
+ if (key != "prefLabel") {
+ // The current key is not `"prefLabel"`.
+ const cased = key[0].toUpperCase() +
+ key.substring(1);
+ yield [`${key}s`, function* () {
+ yield* this.#yieldLabels(key);
+ }];
+ yield [`add${cased}`, function (...labels) {
+ return this.#addLabel(key, ...labels);
+ }];
+ yield [`delete${cased}`, function (...labels) {
+ return this.#deleteLabel(key, ...labels);
+ }];
+ } else {
+ // The current key is `"prefLabel"`. This is a
+ // special case which is not handled by the schema.
+ /* do nothing */
+ }
+ }
+ }(),
+ ([key, value]) => [key, {
+ configurable: true,
+ enumerable: false,
+ value: Object.defineProperty(value, "name", {
+ value: key,
+ }),
+ writable: true,
+ }],
+ )),
+ ),
+ writable: false,
+ },
+ });
+ return new TagConstructor(constructor, system, storage, schema);
}
/**
- * Removes the provided tags from the list of tags that this `Tag` is
- * in canon with, then returns this `Tag`.
+ * Assigns the provided data and identifier to the provided tag.
*
- * Arguments may be string identifiers or objects with an
- * `.identifier` property.
+ * ☡ This function throws if the provided tag is not a `Tag`.
+ *
+ * ※ This function is not exposed.
*/
- deleteInCanonTag(...tags) {
- const inCanon = this.#data.inCanon;
- for (const $ of tags) {
- // Iterate over the provided tags and delete them.
- inCanon.delete(toIdentifier($));
- }
- return this;
+ static assignData(tag, data, identifier) {
+ tag.#identifier = `${identifier}`;
+ tag.#persistedData = tagData(data);
+ tag.#data = tagData(data);
+ return tag;
}
/**
- * Removes the provided tags from the list of tags that this `Tag`
- * involves, then returns this `Tag`.
+ * Returns the `TagSystem` that the provided value belongs to.
*
- * Arguments may be string identifiers or objects with an
- * `.identifier` property.
+ * ※ This function can be used to check if the provided value has
+ * private tag features.
+ *
+ * ※ This function is not exposed.
*/
- deleteInvolvesTag(...tags) {
- const involves = this.#data.involves;
- for (const $ of tags) {
- // Iterate over the provided tags and delete them.
- involves.delete(toIdentifier($));
- }
- return this;
+ static getSystem($) {
+ return !(#system in Object($)) ? null : $.#system;
}
- /** Yields `Tag`s that are in canon of this `Tag`. */
- *hasInCanonTags() {
- const storage = this.#storage;
- if (this.#kind == "CanonTag") {
- // This is a canon tag.
- for (const identifier of this.#data.hasInCanon) {
- // Iterate over the tags in canon and yield them if possible.
- const tag = storage.get(identifier);
- if (
- !this.#isTagInStorage(tag) || !HAS_IN_CANON.has(tag.#kind)
- ) {
- // The tag in canon no longer appears in storage; perhaps it
- // was deleted.
- /* do nothing */
- } else {
- // The tag in canon exists and is constructable from storage.
- yield tag;
- }
- }
- } else {
- /* do nothing */
- }
+ static {
+ // Overwrite the default `::constructor` method to instead give the
+ // actual (bound) constructor which was used to generate a given
+ // `Tag`.
+ Object.defineProperties(this.prototype, {
+ constructor: {
+ configurable: true,
+ enumerable: false,
+ get() {
+ // All `Tag`s are constructed via the `.Tag` constructor
+ // available in their `TagSystem`; return it.
+ return this.#system.Tag;
+ },
+ set: undefined,
+ },
+ });
}
- /** Yields the hidden labels of this `Tag`. */
- *hiddenLabels() {
- yield* this.#data.hiddenLabel;
+ /** Returns the authority (domain) name for this `Tag`. */
+ get authorityName() {
+ return this.#system.authorityName;
}
/** Returns the identifier of this `Tag`. */
return this.#identifier;
}
- /** Yields `Tag`s that this `Tag` is in canon of. */
- *inCanonTags() {
- const storage = this.#storage;
- if (HAS_IN_CANON.has(this.#kind)) {
- // This tag can be placed in canon.
- for (const identifier of this.#data.inCanon) {
- // Iterate over the canon tags and yield them if possible.
- const tag = storage.get(identifier);
- if (!this.#isTagInStorage(tag) || tag.#kind != "CanonTag") {
- // The canon tag no longer appears in storage; perhaps it was
- // deleted.
- /* do nothing */
- } else {
- // The canon tag exists and is constructable from storage.
- yield tag;
- }
- }
- } else {
- // This tag cannot be placed in canon.
- /* do nothing */
- }
- }
-
- /** Yields `Tag`s which involve this `Tag`. */
- *involvedInTags() {
- const storage = this.#storage;
- for (const identifier of this.#data.involvedIn) {
- // Iterate over the involving tags and yield them if possible.
- const tag = storage.get(identifier);
- const tagKind = tag.#kind;
- if (
- !this.#isTagInStorage(tag) ||
- !CONCEPTUAL_TAG_KINDS.has(tagKind) ||
- RELATIONSHIP_TAG_KINDS.has(tagKind) &&
- !INVOLVABLE_IN_RELATIONSHIP.has(this.#kind)
- ) {
- // The including tag no longer appears in storage; perhaps it
- // was deleted.
- /* do nothing */
- } else {
- // The including tag exists and is constructable from storage.
- yield tag;
- }
- }
- }
-
- /** Yields `Tag`s that this `Tag` involves. */
- *involvesTags() {
- const storage = this.#storage;
- const kind = this.#kind;
- if (CONCEPTUAL_TAG_KINDS.has(kind)) {
- // This tag can involve other tags.
- for (const identifier of this.#data.involves) {
- // Iterate over the involved and yield them if possible.
- const tag = storage.get(identifier);
- if (
- !this.#isTagInStorage(tag) ||
- RELATIONSHIP_TAG_KINDS.has(kind) &&
- !INVOLVABLE_IN_RELATIONSHIP.has(tag.#kind)
- ) {
- // The involved tag no longer appears in storage; perhaps it
- // was deleted.
- /* do nothing */
- } else {
- // The involved tag exists and is constructable from storage.
- yield tag;
- }
- }
- } else {
- // This tag cannot involve other tags.
- /* do nothing */
- }
- }
-
/** Returns the I·R·I for this `Tag`. */
get iri() {
const { identifier, iriSpace } = this;
return this.#kind;
}
- /** Yields `Tag`s which are narrower than this `Tag`. */
- *narrowerTags() {
- const storage = this.#storage;
- for (const identifier of this.#data.narrower) {
- const tag = storage.get(identifier);
- if (!this.#isTagInStorage(tag)) {
- // The narrower tag no longer appears in storage; perhaps it
- // was deleted.
- /* do nothing */
- } else {
- // The narrower tag exists and is constructable from storage.
- yield tag;
- }
- }
- }
-
- /**
- * Yields `Tag`s which are narrower than this `Tag`, transitively.
- */
- *narrowerTransitiveTags() {
- const storage = this.#storage;
- const encountered = new Set();
- let pending = new Set(this.#data.narrower);
- while (pending.size > 0) {
- // Loop until all narrower tags have been encountered.
- const processing = pending;
- pending = new Set();
- for (const identifier of processing) {
- // Iterate over the narrower tags and yield them if possible.
- if (!encountered.has(identifier)) {
- // The narrower tag has not been encountered before.
- encountered.add(identifier);
- const tag = storage.get(identifier);
- if (!this.#isTagInStorage(tag)) {
- // The narrower tag no longer appears in storage; perhaps
- // it was deleted.
- /* do nothing */
- } else {
- // The narrower tag exists and is constructable from
- // storage.
- yield tag;
- for (const transitive of tag.#data.narrower) {
- // Iterate over the narrower tags of the current narrower
- // tag and add them to pending as needed.
- if (!encountered.has(transitive)) {
- // The narrower narrower tag has not been encountered
- // yet.
- pending.add(transitive);
- } else {
- // The narrower narrower tag has already been
- // encountered.
- /* do nothing */
- }
- }
- }
- } else {
- // The narrower tag has already been encountered.
- /* do nothing */
- }
- }
- }
- }
-
/**
* Persist this `Tag` to storage and return an ActivityStreams
* serialization of a Tag Activity representing any changes, or
* as broader than another causes the other tag to reciprocally be
* marked as narrower.
*
- * ※ The inverse terms `hasInCanon`, `isIncludedIn`, and `narrower`
- * will never appear in the predicates of generated activities.
+ * ※ Inverse object properties will never appear in the predicates
+ * of generated activities.
*/
persist(silent = false) {
const system = this.#system;
const storage = this.#storage;
+ const {
+ objectProperties,
+ transitiveProperties,
+ dataProperties,
+ } = this.#schema;
const persistedData = this.#persistedData;
const data = this.#data;
const diffs = {};
for (const [key, value] of Object.entries(data)) {
// Iterate over each entry of the tag data and create a diff
// with the last persisted information.
- if (SKIP_IN_DIFF.has(key) || silent && LITERAL_TERMS.has(key)) {
+ if (
+ objectProperties[key]?.inverseOf != null ||
+ silent && key in dataProperties
+ ) {
// The current property is one which is skipped in diffs.
//
// In a silent persist, this includes any literal terms.
};
} else if (value instanceof Set) {
// The current property is set‐valued.
- let values = null; // initialized on first use
const oldValues = new Set(persisted);
const newValues = new Set(value);
for (const existing of persisted) {
// Iterate over each persisted property and either remove
// it from the list of new values or add it to the list of
// removed ones.
- //
- // ※ Some special handling is required here for
- // language‐tagged strings.
- if (
- value.has(existing) ||
- Object(existing) === existing &&
- (values ??= [...value]).some(($) =>
- `${$}` == `${existing}` &&
- $.language == existing.language
- )
- ) {
+ if (value.has(existing)) {
// The value is in both the old and new version of the
// data.
oldValues.delete(existing);
/* do nothing */
}
}
- diffs[key] = {
- old: oldValues,
- new: newValues,
- };
+ diffs[key] = { old: oldValues, new: newValues };
} else if (
`${value}` != `${persisted}` ||
value.language != persisted.language
};
} else {
// The current property did not change.
- diffs[key] = {
- old: new Set(),
- new: new Set(),
- };
+ diffs[key] = { old: new Set(), new: new Set() };
}
}
}
}
const persistedIdentifier = this.#identifier;
this.#persistedData = tagData(data); // cloning here is necessary
- for (
- const [term, inverse] of [
- ["broader", "narrower"],
- ["inCanon", "hasInCanon"],
- ["involves", "involvedIn"],
- ]
- ) {
- // Iterate over each term referencing other tags and update the
- // inverse property on those tags if possible.
- for (const referencedIdentifier of diffs[term].old) {
- // Iterate over the removed tags and remove this `Tag` from
- // their inverse property.
- const referenced = storage.get(referencedIdentifier);
- try {
- // Try removing this `Tag`.
- referenced.#data[inverse].delete(persistedIdentifier);
- storage.set(referencedIdentifier, referenced);
- } catch {
- // Removal failed, possibly because the other tag was
- // deleted.
- /* do nothing */
+ for (const inverse in objectProperties) {
+ // Iterate over each non‐transitive inverse property and update
+ // it based on its inverse on the corresponding tags if possible.
+ const term = objectProperties[inverse].inverseOf;
+ if (term == null || term in transitiveProperties) {
+ // The current property is not the inverse of an non‐transitive
+ // property.
+ /* do nothing */
+ } else {
+ // The current property is the inverse of a non‐transitive
+ // property.
+ for (const referencedIdentifier of diffs[term].old) {
+ // Iterate over the removed tags and remove this `Tag` from
+ // their inverse property.
+ const referenced = storage.get(referencedIdentifier);
+ try {
+ // Try removing this `Tag`.
+ referenced.#data[inverse].delete(persistedIdentifier);
+ storage.set(referencedIdentifier, referenced);
+ } catch {
+ // Removal failed, possibly because the other tag was
+ // deleted.
+ /* do nothing */
+ }
}
- }
- for (const referencedIdentifier of diffs[term].new) {
- const referenced = storage.get(referencedIdentifier);
- try {
- // Try adding this `Tag`.
- referenced.#data[inverse].add(persistedIdentifier);
- storage.set(referencedIdentifier, referenced);
- } catch {
- // Adding failed, possibly because the other tag was deleted.
- /* do nothing */
+ for (const referencedIdentifier of diffs[term].new) {
+ const referenced = storage.get(referencedIdentifier);
+ try {
+ // Try adding this `Tag`.
+ referenced.#data[inverse].add(persistedIdentifier);
+ storage.set(referencedIdentifier, referenced);
+ } catch {
+ // Adding failed, possibly because the other tag was deleted.
+ /* do nothing */
+ }
}
}
}
// things as needed.
for (const oldValue of oldValues) {
// Iterate over removals and unstate them.
- if (LITERAL_TERMS.has(term)) {
- // This is a literal term; push the change wrapped in an
- // object.
+ if (term in dataProperties) {
+ // This is a literal term; push it.
unstates.push({
predicate: term,
- object: Object(oldValue) === oldValue
- ? { ...langString(oldValue) }
- : { "@value": `${oldValue}` },
+ object: { ...oldValue },
});
} else {
// This is a named term; attempt to get its I·R·I and
}
for (const newValue of newValues) {
// Iterate over additions and state them.
- if (LITERAL_TERMS.has(term)) {
- // This is a literal term; push the change wrapped in an
- // object.
+ if (term in dataProperties) {
+ // This is a literal term; push it.
states.push({
predicate: term,
- object: Object(newValue) === newValue
- ? { ...langString(newValue) }
- : { "@value": `${newValue}` },
+ object: { ...newValue },
});
} else {
// This is a named term; attempt to get its I·R·I and
const {
/**
- * Returns the provided value converted into either a plain string
- * primitive or an object with `.["@value"]` and `.["@language"]`
- * properties.
+ * 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: methods } = this;
+ delete methods.constructor;
+ Object.defineProperty(superclass, "prototype", {
+ configurable: false,
+ enumerable: false,
+ value: Tag.prototype,
+ writable: false,
+ });
+ Object.defineProperties(
+ superclass,
+ Object.getOwnPropertyDescriptors(methods),
+ );
+ }
+
+ /**
+ * 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 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.
*
* TODO: Ideally this would be extracted more fully into an R·D·F
* library.
*/
langString,
} = (() => {
+ /**
+ * Returns the language string object corresponding to the provided
+ * value and language.
+ */
+ const getLangString = (value, language = "") => {
+ const valueMap = languageMap[language] ??= Object.create(null);
+ const literal = valueMap[value]?.deref();
+ if (literal != null) {
+ // There is already an object corresponding to the provided value
+ // and language.
+ return literal;
+ } else {
+ // No object already exists corresponding to the provided value
+ // and language; create one.
+ const result = Object.preventExtensions(
+ Object.create(String.prototype, {
+ "@value": {
+ enumerable: true,
+ value,
+ },
+ "@language": {
+ enumerable: !!language,
+ value: language || null,
+ },
+ language: { enumerable: false, get: getLanguage },
+ toString: { enumerable: false, value: toString },
+ valueOf: { enumerable: false, value: valueOf },
+ }),
+ );
+ const ref = new WeakRef(result);
+ langStringRegistry.register(result, { ref, language, value });
+ valueMap[value] = ref;
+ return result;
+ }
+ };
+
/** Returns the `.["@language"]` of this object. */
const getLanguage = Object.defineProperty(
function () {
- return this["@language"];
+ return this["@language"] || null;
},
"name",
{ value: "get language" },
);
+ /**
+ * A `FinalizationRegistry` for language string objects.
+ *
+ * This simply cleans up the corresponding `WeakRef` in the language
+ * map.
+ */
+ const langStringRegistry = new FinalizationRegistry(
+ ({ ref, language, value }) => {
+ const valueMap = languageMap[language];
+ if (valueMap?.[value] === ref) {
+ delete valueMap[value];
+ } else {
+ /* do nothing */
+ }
+ },
+ );
+
+ /**
+ * An object whose own values are an object mapping values to
+ * language string objects for the language specified by the key.
+ */
+ const languageMap = Object.create(null);
+
/** Returns the `.["@value"]` of this object. */
const toString = function () {
return this["@value"];
};
- /** Returns the `.["@value"]` of this object. */
+ /**
+ * Returns this object if it has a `.["@language"]`; otherwise, its
+ * `.["@value"]`.
+ */
const valueOf = function () {
- return this["@value"];
+ return this["@language"] ? this : this["@value"];
};
return {
Object($) === $
? "@value" in $
? "@language" in $
- ? Object.preventExtensions(
- Object.create(String.prototype, {
- "@value": {
- enumerable: true,
- value: `${$["@value"]}`,
- },
- "@language": {
- enumerable: true,
- value: `${$["@language"]}`,
- },
- language: { enumerable: false, get: getLanguage },
- toString: { enumerable: false, value: toString },
- valueOf: { enumerable: false, value: valueOf },
- }),
+ ? getLangString(
+ `${$["@value"]}`,
+ `${$["@language"] ?? ""}`,
)
- : `${$["@value"]}`
+ : getLangString(`${$["@value"]}`)
: "language" in $
- ? Object.preventExtensions(
- Object.create(String.prototype, {
- "@value": { enumerable: true, value: `${$}` },
- "@language": {
- enumerable: true,
- value: `${$.language}`,
- },
- language: { enumerable: false, get: getLanguage },
- toString: { enumerable: false, value: toString },
- valueOf: { enumerable: false, value: valueOf },
- }),
- )
- : `${$}`
- : `${$ ?? ""}`,
+ ? getLangString(`${$}`, `${$.language ?? ""}`)
+ : getLangString(`${$}`)
+ : getLangString(`${$ ?? ""}`),
};
})();
} else {
// No bound constructor has been created yet.
const storage = this.#storage;
- const BoundTag = Tag.bind(undefined, this, storage);
- return this.#Tag = Object.defineProperties(BoundTag, {
- all: {
- configurable: true,
- enumerable: false,
- value: Tag.all.bind(BoundTag, this, storage),
- writable: true,
- },
- fromIRI: {
- configurable: true,
- enumerable: false,
- value: Tag.fromIRI.bind(BoundTag, this, storage),
- writable: true,
- },
- fromIdentifier: {
- configurable: true,
- enumerable: false,
- value: Tag.fromIdentifier.bind(BoundTag, this, storage),
- writable: true,
- },
- fromTagURI: {
- configurable: true,
- enumerable: false,
- value: Tag.fromTagURI.bind(BoundTag, this, storage),
- writable: true,
- },
- identifiers: {
- configurable: true,
- enumerable: false,
- value: Tag.identifiers.bind(BoundTag, this, storage),
- writable: true,
- },
- name: { value: `${this.tagURI}#${Tag.name}` },
- prototype: { value: Tag.prototype },
- [Storage.toInstance]: {
- configurable: true,
- enumerable: false,
- value: Tag[Storage.toInstance].bind(BoundTag, this, storage),
- writable: true,
- },
- });
+ return this.#Tag = Tag.For(this, storage, schema);
}
}