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`.
/** The current (modified) data associated with this `Tag`. */
#data = tagData();
+ /**
+ * Adds the provided label(s) to this `Tag` as the provided
+ * predicate, then returns this `Tag`.
+ */
+ #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($);
+ values.add(literal);
+ }
+ return this;
+ }
+
+ /**
+ * Adds the provided tags to 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.
+ */
+ #addTag(predicate, ...tags) {
+ const storage = this.#storage;
+ const values = this.#data[predicate];
+ for (const $ of tags) {
+ // 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 state ${predicate} of Tag: Identifier must not be nullish.`,
+ );
+ } 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, 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 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 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.
+ values.add(identifier);
+ }
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Removes the provided string label(s) from this `Tag` as the
+ * provided predicate, then returns this `Tag`.
+ */
+ #deleteLabel(predicate, ...labels) {
+ const values = this.#data[predicate];
+ for (const $ of labels) {
+ // Iterate over each provided label and attempt to remove it.
+ const literal = langString($);
+ values.delete(literal);
+ }
+ return this;
+ }
+
+ /**
+ * 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.
+ */
+ #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;
+ }
+
/**
* Returns whether or not the provided value is a tag which shares a
* storage with this tag.
}
}
+ /**
+ * Yields the labels of this `Tag` according to the provided
+ * predicate.
+ */
+ *#yieldLabels(predicate) {
+ yield* this.#data[predicate];
+ }
+
+ /**
+ * 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[predicate]) {
+ // Iterate over the tags in this predicate and yield them if
+ // possible.
+ const tag = storage.get(identifier);
+ 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 tag exists and is constructable from storage.
+ yield tag;
+ }
+ }
+ }
+
+ /**
+ * 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[basePredicate]);
+ while (pending.size > 0) {
+ // Loop until all tags of the predicate have been encountered.
+ const processing = pending;
+ pending = new Set();
+ for (const identifier of processing) {
+ // Iterate over the tags and yield them if possible.
+ if (!encountered.has(identifier)) {
+ // The tag has not been encountered before.
+ encountered.add(identifier);
+ const tag = storage.get(identifier);
+ 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 tag exists and is constructable from storage.
+ yield tag;
+ 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 nested tag has not been encountered yet.
+ pending.add(transitive);
+ } else {
+ // The nested tag has already been encountered.
+ /* do nothing */
+ }
+ }
+ }
+ } else {
+ // The tag has already been encountered.
+ /* do nothing */
+ }
+ }
+ }
+ }
+
/**
* Constructs a new `Tag` of the provided kind and with the provided
* preferred label.
*
* ☡ This constructor throws if the provided kind is not supported.
*/
- constructor(system, storage, kind = "Tag", prefLabel = "") {
+ constructor(system, storage, schema, kind = "Tag", prefLabel = "") {
+ this.#system = system;
+ this.#storage = storage;
+ this.#schema = schema;
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 {
+ 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;
}
}
+ /**
+ * 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.
+ */
+ 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, {
+ prototype: {
+ configurable: false,
+ enumerable: false,
+ value: Object.create(
+ Tag.prototype,
+ Object.fromEntries(Array.from(
+ function* () {
+ for (const key in objectProperties) {
+ const {
+ inverseOf,
+ subPropertyOf,
+ } = objectProperties[key];
+ if (key in transitiveProperties) {
+ // Transitive property methods are added by their
+ // nontransitive subproperties.
+ /* do nothing */
+ } else {
+ yield [`${key}Tags`, function* () {
+ yield* this.#yieldTags(key);
+ }];
+ if (inverseOf == null) {
+ 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 {
+ /* do nothing */
+ }
+ if (
+ subPropertyOf != null &&
+ subPropertyOf in transitiveProperties
+ ) {
+ yield [`${subPropertyOf}Tags`, function* () {
+ yield* this.#yieldTransitiveTags(
+ subPropertyOf,
+ key,
+ );
+ }];
+ } else {
+ /* do nothing */
+ }
+ }
+ }
+ for (const key in dataProperties) {
+ if (key != "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 {
+ /* do nothing */
+ }
+ }
+ }(),
+ ([key, value]) => [key, {
+ configurable: true,
+ enumerable: false,
+ value: Object.defineProperty(value, "name", {
+ value: key,
+ }),
+ writable: true,
+ }],
+ )),
+ ),
+ writable: false,
+ },
+ });
+ return Object.defineProperties(
+ constructor,
+ Object.fromEntries([
+ ["name", { value: "TagSystem::Tag" }],
+ ...[
+ "all",
+ "fromIRI",
+ "fromIdentifier",
+ "fromTagURI",
+ "identifiers",
+ Storage.toInstance,
+ ].map((key) => [key, {
+ configurable: true,
+ enumerable: false,
+ value: Object.defineProperty(
+ Tag[key].bind(constructor, system, storage),
+ "name",
+ { value: String(key) },
+ ),
+ writable: true,
+ }]),
+ ]),
+ );
+ }
+
/**
* Yields the tags in the `TagSystem` associated with this
* constructor.
}
/**
- * 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
- 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);
- }
- }
- return this;
- }
-
- /**
- * Adds the provided tags to the list of tags that this `Tag` is
- * narrower than, then returns this `Tag`.
- *
- * Arguments may be string identifiers or objects with an
- * `.identifier` property.
- */
- addBroaderTag(...tags) {
- const storage = this.#storage;
- const broader = this.#data.broader;
- for (const $ of tags) {
- // Iterate over each tag and attempt to set it as broader than
- // this `Tag`.
- 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.",
- );
- } else if (broader.has(identifier)) {
- // Short‐circuit: The identifier is already something this
- // `Tag` is narrower than.
- /* 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 assign broader to 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}.`,
- );
- } else {
- // The current tag is a tag in the correct tag system; add
- // its identifier.
- broader.add(identifier);
- }
- }
- }
- return this;
- }
-
- /**
- * Adds the provided label(s) to this `Tag` as hidden labels, then
- * returns this `Tag`.
- */
- addHiddenLabel(...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 add 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);
- }
- }
- return this;
- }
-
- /**
- * Adds the provided tags to the list of tags that this `Tag` is in
- * canon with, 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);
- }
- }
- }
- }
- 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.
- *
- * ☡ 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.
- */
- 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);
- }
- }
- }
- }
- 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 `Tag`s which are broader than this `Tag`. */
- *broaderTags() {
- const storage = this.#storage;
- for (const identifier of this.#data.broader) {
- // Iterate over the broader tags 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.
- /* do nothing */
- } else {
- // The broader tag exists and is constructable from storage.
- yield tag;
- }
- }
- }
-
- /** Yields `Tag`s which are broader than this `Tag`, transitively. */
- *broaderTransitiveTags() {
- const storage = this.#storage;
- const encountered = new Set();
- let pending = new Set(this.#data.broader);
- while (pending.size > 0) {
- // Loop until all broader tags have been encountered.
- const processing = pending;
- pending = new Set();
- for (const identifier of processing) {
- // Iterate over the broader tags and yield them if possible.
- if (!encountered.has(identifier)) {
- // The broader 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.
- /* do nothing */
- } else {
- // The broader 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.
- if (!encountered.has(transitive)) {
- // The broader broader tag has not been encountered
- // yet.
- pending.add(transitive);
- } else {
- // The broader broader tag has already been
- // encountered.
- /* do nothing */
- }
- }
- }
- } else {
- // The broader 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`.
- *
- * Arguments may be string identifiers or objects with an
- * `.identifier` property.
- */
- deleteBroaderTag(...tags) {
- const broader = this.#data.broader;
- for (const $ of tags) {
- // Iterate over the provided tags and delete them.
- broader.delete(toIdentifier($));
- }
- return this;
- }
-
- /**
- * Removes the provided string label(s) from this `Tag` as hidden
- * labels, then returns this `Tag`.
+ * 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.
*/
- 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);
+ 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 label is a simple string.
- hiddenLabels.delete(literal);
+ // The current instance is not a `Tag` in this `TagSystem`.
+ /* do nothing */
}
}
- return this;
}
/**
- * Removes the provided tags from the list of tags that this `Tag` is
- * in canon with, then returns this `Tag`.
+ * Returns a new `Tag` constructed from the provided data and with
+ * the provided identifier.
*
- * Arguments may be string identifiers or objects with an
- * `.identifier` property.
- */
- deleteInCanonTag(...tags) {
- const inCanon = this.#data.inCanon;
- for (const $ of tags) {
- // Iterate over the provided tags and delete them.
- inCanon.delete(toIdentifier($));
- }
- return this;
- }
-
- /**
- * Removes the provided tags from the list of tags that this `Tag`
- * involves, then returns this `Tag`.
+ * ※ 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.
*
- * Arguments may be string identifiers or objects with an
- * `.identifier` property.
+ * ※ This function is not really intended for public usage.
*/
- 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 [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;
}
- /** 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 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
} 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);
}
}