X-Git-Url: https://git.ladys.computer/Etiquette/blobdiff_plain/e20983aca1b36dd368d52e02ee018c2eaf0ca55f..f8903c5b3bd12d02af174e5043d906d63da0b0d1:/model.js
diff --git a/model.js b/model.js
index 3f0b6ad..d2990eb 100644
--- a/model.js
+++ b/model.js
@@ -1,66 +1,72 @@
-// đ§đˇď¸ Ătiquette ⡠model.js
-// ====================================================================
-//
-// Copyright Š 2023 Lady [@ Ladyâs Computer].
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at .
+// SPDX-FileCopyrightText: 2023, 2025 Lady
+// SPDX-License-Identifier: MPL-2.0
+/**
+ * â đ§đˇď¸ Ătiquette ⡠model.js
+ *
+ * Copyright Š 2023, 2025 Lady [@ Ladys Computer].
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at .
+ */
+import { identity } from "./deps.js";
import { Storage } from "./memory.js";
import { taggingDiscoveryContext } from "./names.js";
import schema from "./schema.js";
+const ĂTIQUETTE = "đ§đˇď¸ Ătiquette";
+
/**
* A tag.
*
- * `Tag`s are not assigned identifiers and do not have side¡effects on
- * other tags in the `TagSystem` until they are persisted with
- * `::persist`, at which point changes to their relationships are
+ * `Tag´s are not assigned identifiers and do not have side¡effects on
+ * other tags in the `TagSystem´ until they are persisted with
+ * `::persist´, at which point changes to their relationships are
* applied.
*
- * `Tag`s are also not kept upâtoâdate, but persisting an outdated
- * `Tag` will *not* undo subsequent changes.
+ * `Tag´s are also not kept upâtoâdate, but persisting an outdated
+ * `Tag´ will âšnotâš undo subsequent changes.
*
* âť This class is not itself directly exposed, although bound
- * versions of it are via `TagSystem::Tag`.
+ * versions of it are via `TagSystem::Tag´.
*/
class Tag {
- /** The `TagSystem` this `Tag` belongs to. */
+ /** The `TagSystem´ this `Tag´ belongs to. */
#system;
- /** The `Storage` managed by this `Tag`âs `TagSystem`. */
+ /** The `Storage´ managed by this `Tag´s `TagSystem´. */
#storage;
- /** The schema in use for this `Tag`. */
+ /** 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`.
+ * has been assigned to this `Tag´.
*
- * Will be `null` if this `Tag` has not been persisted. Otherwise,
- * the format is `cxx-xxxx` (`c` = checksum; `x` = digit).
+ * Will be `null´ if this `Tag´ has not been persisted. Otherwise,
+ * the format is `cxx-xxxx´ (`c´ = checksum; `x´ = digit).
*/
#identifier = null;
- /** The kind of this `Tag`. */
+ /** The kind of this `Tag´. */
#kind = "Tag";
/**
- * The data which was attached to this `Tag` the last time it was
+ * The data which was attached to this `Tag´ the last time it was
* persisted or retrieved from storage.
*
* Diffing with this will reveal changes.
*/
#persistedData = null;
- /** The current (modified) data associated with 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`.
+ * Adds the provided label(s) to this `Tag´ as the provided
+ * predicate, then returns this `Tag´.
*/
#addLabel(predicate, ...labels) {
const values = this.#data[predicate];
@@ -73,11 +79,11 @@ class Tag {
}
/**
- * Adds the provided tags to the list of tags that this `Tag` is
- * related to by the provided predicate, then returns this `Tag`.
+ * 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.
+ * `.identifier´ property.
*/
#addTag(predicate, ...tags) {
const storage = this.#storage;
@@ -88,26 +94,26 @@ class Tag {
if (identifier == null) {
// ⥠The current tag has no identifier.
throw new TypeError(
- `Cannot state ${predicate} of Tag: Identifier must not be nullish.`,
+ `${ĂTIQUETTE}: 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
+ // 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
+ // ⥠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}.`,
+ `${ĂTIQUETTE}: 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}.`,
+ `${ĂTIQUETTE}: Cannot state ${predicate} of Tag: Tags must be from the same Tag System, but got: ${identifier}.`,
);
} else if (
!isObjectPredicateOK(
@@ -120,7 +126,7 @@ class Tag {
// ⥠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}.`,
+ `${ĂTIQUETTE}: 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
@@ -133,8 +139,8 @@ class Tag {
}
/**
- * Removes the provided string label(s) from this `Tag` as the
- * provided predicate, then returns this `Tag`.
+ * 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];
@@ -147,11 +153,11 @@ class Tag {
}
/**
- * Removes the provided tags from the list of tags that this `Tag` is
- * related to by the provided predicate, 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.
+ * `.identifier´ property.
*/
#deleteTag(predicate, ...tags) {
const values = this.#data[predicate];
@@ -166,11 +172,11 @@ class Tag {
* 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`.
+ * Sharing a storage also implies sharing a `TagSystem´.
*/
#isTagInStorage($) {
try {
- // Try to compare the provided valueâs internal store with
+ // Try to compare the provided values internal store with
// the provided storage.
return $.#storage == this.#storage;
} catch {
@@ -180,7 +186,7 @@ class Tag {
}
/**
- * Yields the labels of this `Tag` according to the provided
+ * Yields the labels of this `Tag´ according to the provided
* predicate.
*/
*#yieldLabels(predicate) {
@@ -188,7 +194,7 @@ class Tag {
}
/**
- * Yields the tags that this `Tag` is related to by the provided
+ * Yields the tags that this `Tag´ is related to by the provided
* predicate.
*/
*#yieldTags(predicate) {
@@ -216,7 +222,7 @@ class Tag {
}
/**
- * Yields the tags that this `Tag` is related to by the provided
+ * Yields the tags that this `Tag´ is related to by the provided
* predicate, figured transitively.
*/
*#yieldTransitiveTags(transitivePredicate, basePredicate) {
@@ -268,11 +274,11 @@ class Tag {
}
/**
- * Constructs a new `Tag` of the provided kind and with the provided
+ * 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
+ * generating the value of `TagSystem::Tag´. It isnŒt possible to
* access this constructor in its unbound form from outside this
* module.
*
@@ -286,7 +292,7 @@ class Tag {
if (!(kindString in schema.classes)) {
// The provided kind is not supported.
throw new RangeError(
- `Cannot construct Tag: Unrecognized kind: ${kind}.`,
+ `${ĂTIQUETTE}: Cannot construct Tag: Unrecognized kind: ${kind}.`,
);
} else {
// The provided kind is one of the recognized tag kinds.
@@ -296,7 +302,7 @@ class Tag {
}
/**
- * Returns a new `Tag` constructor for the provided system, storage,
+ * Returns a new `Tag´ constructor for the provided system, storage,
* schema, created with an appropriate prototype for the properties
* so defined.
*
@@ -316,6 +322,7 @@ class Tag {
);
};
Object.defineProperties(constructor, {
+ name: { value: "TagSystem::Tag" },
prototype: {
configurable: false,
enumerable: false,
@@ -324,21 +331,30 @@ class Tag {
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) {
- const cased = key[0].toUpperCase() +
- key.substring(1);
+ // 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);
}];
@@ -346,12 +362,16 @@ class Tag {
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
+ 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,
@@ -359,14 +379,19 @@ class Tag {
);
}];
} 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") {
- const cased = key[0].toUpperCase() +
- key.substring(1);
+ // The current key is not `"prefLabel"´.
+ const cased = key[0].toUpperCase()
+ + key.substring(1);
yield [`${key}s`, function* () {
yield* this.#yieldLabels(key);
}];
@@ -377,6 +402,8 @@ class Tag {
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 */
}
}
@@ -394,203 +421,75 @@ class Tag {
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.
- *
- * âť 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;
- }
- }
+ return new TagConstructor(constructor, system, storage, schema);
}
/**
- * 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.
+ * Assigns the provided data and identifier to the provided tag.
*
- * ⥠This function throws if the identifier is invalid.
+ * ⥠This function throws if the provided tag is not a `Tag´.
*
- * âť If the identifier is valid but not recognized, this function
- * returns `null`.
+ * âť This function is not exposed.
*/
- static fromIdentifier(system, storage, identifier) {
- const instance = storage.get(identifier);
- return Tag.getSystem(instance) == system ? instance : null;
+ static assignData(tag, data, identifier) {
+ tag.#identifier = `${identifier}`;
+ tag.#persistedData = tagData(data);
+ tag.#data = tagData(data);
+ return tag;
}
/**
- * Returns a new `Tag` resolved from the provided Tag U¡R¡I.
+ * Returns a new `Tag´ with the provided identifier, kind, and
+ * prefLabel.
*
- * âť 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 exists to enable `TagSystem´s to replay Create
+ * activities, maintaining the identifier of the original.
*
- * ⥠This function throws if the provided Tag U¡R¡I does not match
- * the tagging entity of this constructorâs `TagSystem`.
+ * ⥠This function throws if the provided identifier is already in
+ * use.
*
- * ❠If the specific component of the Tag U¡R¡I is not recognized,
- * this function returns `null`.
+ * âť This function is not exposed.
*/
- 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.
+ static new(system, identifier, kind = "Tag", prefLabel = "") {
+ const storage = (new system.Tag()).#storage;
+ if (storage.has(identifier)) {
throw new RangeError(
- `Tag U¡R¡I did not begin with the expected prefix: ${tagURI}`,
+ `${ĂTIQUETTE}: Cannot create Tag: Identifier already in use: ${identifier}.`,
);
} 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;
- }
+ const createdTag = new system.Tag(kind, prefLabel);
+ createdTag.#identifier = identifier;
+ createdTag.persist(true);
+ return createdTag;
}
}
/**
- * Returns the `TagSystem` that the provided value belongs to.
+ * Returns the `TagSystem´ that the provided value belongs to.
*
* âť This function can be used to check if the provided value has
* private tag features.
*
+ * âť `Tag::system´ is an overridable, publiclyâaccessible means of
+ * accessing the system.
+ *
* âť 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
+ // Overwrite the default `::constructor´ method to instead give the
// actual (bound) constructor which was used to generate a given
- // `Tag`.
+ // `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.
+ // All `Tag´s are constructed via the `.Tag´ constructor
+ // available in their `TagSystem´; return it.
return this.#system.Tag;
},
set: undefined,
@@ -598,38 +497,47 @@ class Tag {
});
}
- /** Returns the authority (domain) name for this `Tag`. */
+ /** Returns the authority (domain) name for this `Tag´. */
get authorityName() {
return this.#system.authorityName;
}
- /** Returns the identifier of this `Tag`. */
+ /** Returns the identifier of this `Tag´. */
get identifier() {
return this.#identifier;
}
- /** Returns the I¡R¡I for this `Tag`. */
+ /** Returns the I¡R¡I for this `Tag´. */
get iri() {
const { identifier, iriSpace } = this;
return identifier == null ? null : `${iriSpace}${identifier}`;
}
- /** Returns the I¡R¡I space for this `Tag`. */
+ /** Returns the I¡R¡I space for this `Tag´. */
get iriSpace() {
return this.#system.iriSpace;
}
- /** Returns the kind of this `Tag`. */
+ /** Returns the kind of this `Tag´. */
get kind() {
return this.#kind;
}
/**
- * Persist this `Tag` to storage and return an ActivityStreams
+ * Returns the `TagSystem´ for this `Tag´.
+ *
+ * ❠Internally, `Tag.getSystem´ is preferred.
+ */
+ get system() {
+ return this.#system;
+ }
+
+ /**
+ * Persist this `Tag´ to storage and return an ActivityStreams
* serialization of a Tag Activity representing any changes, or
- * `null` if no changes were made.
+ * `null´ if no changes were made.
*
- * If the second argument is `true`, the `Tag` will be persisted but
+ * If the second argument is `true´, the `Tag´ will be persisted but
* no serialization will be made. This is somewhat more efficient.
*
* âť Persistence can imply sideâeffects on other objects, which are
@@ -655,8 +563,8 @@ class Tag {
// Iterate over each entry of the tag data and create a diff
// with the last persisted information.
if (
- objectProperties[key]?.inverseOf != null ||
- silent && key in dataProperties
+ objectProperties[key]?.inverseOf != null
+ || silent && key in dataProperties
) {
// The current property is one which is skipped in diffs.
//
@@ -693,8 +601,8 @@ class Tag {
}
diffs[key] = { old: oldValues, new: newValues };
} else if (
- `${value}` != `${persisted}` ||
- value.language != persisted.language
+ `${value}` != `${persisted}`
+ || value.language != persisted.language
) {
// The current property is (optionally languageâtagged)
// stringâvalued and the value changed.
@@ -710,11 +618,11 @@ class Tag {
}
const identifier = this.#identifier;
if (identifier != null) {
- // This `Tag` has already been persisted; use its existing
+ // This `Tag´ has already been persisted; use its existing
// identifier and persist.
storage.set(identifier, this);
} else {
- // This `Tag` has not been persisted yet; save the new
+ // This `Tag´ has not been persisted yet; save the new
// identifier after persisting.
this.#identifier = storage.add(this);
}
@@ -732,11 +640,11 @@ class Tag {
// 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
+ // Iterate over the removed tags and remove this `Tag´ from
// their inverse property.
const referenced = storage.get(referencedIdentifier);
try {
- // Try removing this `Tag`.
+ // Try removing this `Tag´.
referenced.#data[inverse].delete(persistedIdentifier);
storage.set(referencedIdentifier, referenced);
} catch {
@@ -752,7 +660,8 @@ class Tag {
referenced.#data[inverse].add(persistedIdentifier);
storage.set(referencedIdentifier, referenced);
} catch {
- // Adding failed, possibly because the other tag was deleted.
+ // Adding failed, possibly because the other tag was
+ // deleted.
/* do nothing */
}
}
@@ -819,8 +728,8 @@ class Tag {
});
}
} catch {
- // Value resolution failed for some reason; perhaps the
- // tag was deleted.
+ // Value resolution failed for some reason; perhaps
+ // the tag was deleted.
/* do nothing */
}
}
@@ -850,8 +759,8 @@ class Tag {
});
}
} catch {
- // Value resolution failed for some reason; perhaps the
- // tag was deleted.
+ // Value resolution failed for some reason; perhaps
+ // the tag was deleted.
/* do nothing */
}
}
@@ -875,29 +784,30 @@ class Tag {
})(),
};
if (
- !Object.hasOwn(activity, "states") &&
- !Object.hasOwn(activity, "unstates")
+ !Object.hasOwn(activity, "states")
+ && !Object.hasOwn(activity, "unstates")
) {
// No meaningful changes were actually persisted.
return null;
} else {
- // There were meaningful changes persisted regarding this `Tag`.
+ // There were meaningful changes persisted regarding this
+ // `Tag´.
return activity;
}
}
}
- /** Returns the preferred label for this `Tag`. */
+ /** Returns the preferred label for this `Tag´. */
get prefLabel() {
return this.#data.prefLabel;
}
- /** Sets the preferred label of this `Tag` to the provided label. */
+ /** Sets the preferred label of this `Tag´ to the provided label. */
set prefLabel($) {
this.#data.prefLabel = langString($);
}
- /** Returns the Tag U¡R¡I for this `Tag`. */
+ /** Returns the Tag U¡R¡I for this `Tag´. */
get tagURI() {
const { identifier } = this;
return identifier == null
@@ -905,12 +815,12 @@ class Tag {
: `tag:${this.taggingEntity}:${identifier}`;
}
- /** Returns the tagging entity (domain and date) for this `Tag`. */
+ /** Returns the tagging entity (domain and date) for this `Tag´. */
get taggingEntity() {
return this.#system.taggingEntity;
}
- /** Returns the string form of the preferred label of this `Tag`. */
+ /** Returns the string form of the preferred label of this `Tag´. */
toString() {
return `${this.#data.prefLabel}`;
}
@@ -930,6 +840,232 @@ class Tag {
}
}
+const {
+ /**
+ * 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 constructors `TagSystem´. */
+ #storage;
+
+ /** The schema in use for this constructor. */
+ #schema;
+
+ /**
+ * Constructs a new `Tag´ constructor by adding the appropriate
+ * private fields to the provided constructor, setting its
+ * prototype, and then returning it.
+ *
+ * ❠This constructor does not modify the `name´ or `prototype´
+ * properties of the provided constructor.
+ *
+ * ❠See `Tag.For´, where this constructor is used.
+ */
+ constructor(constructor, system, storage, schema) {
+ super(constructor);
+ Object.setPrototypeOf(this, TagSuper);
+ this.#system = system;
+ this.#storage = storage;
+ this.#schema = schema;
+ }
+
+ static {
+ // Define the superclass constructor which all `Tag´
+ // constructors will inherit from.
+ const superclass = tagConstructorBehaviours.TagSuper =
+ function Tag() {
+ throw new TypeError("Tags must belong to a System.");
+ };
+ const { prototype: staticFeatures } = this;
+ delete staticFeatures.constructor;
+ Object.defineProperty(superclass, "prototype", {
+ configurable: false,
+ enumerable: false,
+ value: Tag.prototype,
+ writable: false,
+ });
+ Object.defineProperties(
+ superclass,
+ Object.getOwnPropertyDescriptors(staticFeatures),
+ );
+ }
+
+ /**
+ * Yields the tags in the `TagSystem´ associated with this
+ * constructor.
+ */
+ *all() {
+ const system = this.#system;
+ const storage = this.#storage;
+ for (const instance of storage.values()) {
+ // Iterate over the entries and yield the ones which are
+ // `Tag´s in this `TagSystem´.
+ if (Tag.getSystem(instance) == system) {
+ // The current instance is a `Tag´ in this `TagSystem´.
+ yield instance;
+ } else {
+ // The current instance is not a `Tag´ in this
+ // `TagSystem´.
+ /* do nothing */
+ }
+ }
+ }
+
+ /**
+ * Returns a new `Tag´ resolved from the provided I¡R¡I.
+ *
+ * ⥠This function throws if the I¡R¡I is not in the `.iriSpace´
+ * of the `TagSystem´ associated with this constructor.
+ *
+ * ❠If the I¡R¡I is not recognized, this function returns
+ * `undefined`.
+ */
+ fromIRI(iri) {
+ const system = this.#system;
+ const storage = this.#storage;
+ const name = `${iri}`;
+ const prefix = `${system.iriSpace}`;
+ if (!name.startsWith(prefix)) {
+ // The I¡R¡I does not begin with the expected prefix.
+ throw new RangeError(
+ `${ĂTIQUETTE}: 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 constructors `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(
+ `${ĂTIQUETTE}: Tag U¡R¡I did not begin with the expected prefix: ${tagURI}`,
+ );
+ } else {
+ // The I¡R¡I begins with the expected prefix.
+ const identifier = tagName.substring(tagPrefix.length);
+ try {
+ // Attempt to resolve the identifier.
+ const instance = storage.get(identifier);
+ return Tag.getSystem(instance) == system
+ ? instance
+ : undefined;
+ } catch {
+ // Do not throw for bad identifiers.
+ return undefined;
+ }
+ }
+ }
+
+ /**
+ * Yields the tag identifiers in the `TagSystem´ associated with
+ * this constructor.
+ */
+ *identifiers() {
+ const system = this.#system;
+ const storage = this.#storage;
+ for (const [identifier, instance] of storage.entries()) {
+ // Iterate over the entries and yield the ones which are
+ // `Tag´s in this `TagSystem´.
+ if (Tag.getSystem(instance) == system) {
+ // The current instance is a `Tag´ in this `TagSystem´.
+ yield identifier;
+ } else {
+ // The current instance is not a `Tag´ in this `TagSystem´.
+ /* do nothing */
+ }
+ }
+ }
+
+ /** Returns the `TagSystem´ for this `Tag´ constructor. */
+ get system() {
+ return this.#system;
+ }
+
+ /**
+ * Returns a new `Tag´ constructed from the provided data and
+ * with the provided identifier.
+ *
+ * âť This function is not really intended for public usage.
+ */
+ [Storage.toInstance](data, identifier) {
+ const tag = new this(data.kind);
+ return Tag.assignData(tag, data, identifier);
+ }
+ },
+ TagSuper: tagConstructorBehaviours.TagSuper,
+ };
+})();
+
const {
/**
* Returns whether the provided schema, subject class, object
@@ -1142,8 +1278,8 @@ const {
domainUnion.some((domain) =>
domain == "Thing" || domains.has(domain)
)
- ) &&
- rangeIntersection.every((rangeUnion) =>
+ )
+ && rangeIntersection.every((rangeUnion) =>
rangeUnion.some((range) =>
range == "Thing" || ranges.has(range)
)
@@ -1154,8 +1290,8 @@ const {
const {
/**
- * Returns the provided value converted into a `String` object with
- * `.["@value"]` and `.["@language"]` properties.
+ * 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.
@@ -1203,7 +1339,7 @@ const {
}
};
- /** Returns the `.["@language"]` of this object. */
+ /** Returns the `.["@language"]´ of this object. */
const getLanguage = Object.defineProperty(
function () {
return this["@language"] || null;
@@ -1213,9 +1349,9 @@ const {
);
/**
- * A `FinalizationRegistry` for language string objects.
+ * A `FinalizationRegistry´ for language string objects.
*
- * This simply cleans up the corresponding `WeakRef` in the language
+ * This simply cleans up the corresponding `WeakRef´ in the language
* map.
*/
const langStringRegistry = new FinalizationRegistry(
@@ -1235,14 +1371,14 @@ const {
*/
const languageMap = Object.create(null);
- /** Returns the `.["@value"]` of this object. */
+ /** Returns the `.["@value"]´ of this object. */
const toString = function () {
return this["@value"];
};
/**
- * Returns this object if it has a `.["@language"]`; otherwise, its
- * `.["@value"]`.
+ * Returns this object if it has a `.["@language"]´; otherwise, its
+ * `.["@value"]´.
*/
const valueOf = function () {
return this["@language"] ? this : this["@value"];
@@ -1366,7 +1502,7 @@ const tagData = ($) => {
/**
* Returns an identifier corresponding to the provided object.
*
- * This is either the value of its `.identifier` or its string value.
+ * This is either the value of its `.identifier´ or its string value.
*
* âť This function is not exposed.
*/
@@ -1380,31 +1516,34 @@ const toIdentifier = ($) =>
/**
* A tag system, with storage.
*
- * The `::Tag` constructor available on any `TagSystem` instance can be
- * used to create new `Tag`s within the system.
+ * The `::Tag´ constructor available on any `TagSystem´ instance can be
+ * used to create new `Tag´s within the system.
*/
export class TagSystem {
- /** The cached bound `Tag` constructor for this `TagSystem`. */
+ /** The cached bound `Tag´ constructor for this `TagSystem´. */
#Tag = null;
- /** The domain of this `TagSystem`. */
+ /** The domain of this `TagSystem´. */
#domain;
- /** The date of this `TagSystem`. */
+ /** The date of this `TagSystem´. */
#date;
- /** The identifier of this `TagSystem`. */
+ /** The identifier of this `TagSystem´. */
#identifier;
- /** The internal `Storage` of this `TagSystem`. */
+ /** The schema used by this `TagSystem´. */
+ #schema = schema;
+
+ /** The internal `Storage` of this `TagSystem´. */
#storage = new Storage();
/**
- * Constructs a new `TagSystem` with the provided domain and date.
+ * Constructs a new `TagSystem´ with the provided domain and date.
*
* Only actual, lowercased domain names are allowed for the domain,
* and the date must be âfullâ (include month and day components).
- * This is for alignment with general best practices for Tag URIâs.
+ * This is for alignment with general best practices for Tag U¡R¡IŒs.
*
* ⥠This constructor throws if provided with an invalid date.
*/
@@ -1426,8 +1565,8 @@ export class TagSystem {
// ⥠The domain is invalid.
throw new RangeError(`Invalid domain: ${domain}.`);
} else if (
- !/^\d{4}-\d{2}-\d{2}$/u.test(dateString) ||
- dateString != new Date(dateString).toISOString().split("T")[0]
+ !/^\d{4}-\d{2}-\d{2}$/u.test(dateString)
+ || dateString != new Date(dateString).toISOString().split("T")[0]
) {
// ⥠The date is invalid.
throw new RangeError(`Invalid date: ${date}.`);
@@ -1439,8 +1578,8 @@ export class TagSystem {
}
/**
- * Returns a bound constructor for constructing `Tags` in this
- * `TagSystem`.
+ * Returns a bound constructor for constructing `Tags´ in this
+ * `TagSystem´.
*/
get Tag() {
if (this.#Tag != null) {
@@ -1449,32 +1588,191 @@ export class TagSystem {
} else {
// No bound constructor has been created yet.
const storage = this.#storage;
- return this.#Tag = Tag.For(this, storage, schema);
+ return this.#Tag = Tag.For(this, storage, this.#schema);
+ }
+ }
+
+ /**
+ * Applies the provided activity to this `TagSystem´ by replaying its
+ * statement changes.
+ *
+ * âť This method assumes that the provided activity conforms to the
+ * assumptions made by this module; i¡e that its tags use the same
+ * identifier format and its activities do not use statements with
+ * inverse property predicates. It is not intended for the generic
+ * playback of activities produced by other scripts or mechanisms,
+ * for which a more sophisticated solution is required.
+ *
+ * ⥠This method throws an error if the provided activity cannot be
+ * processed.
+ */
+ apply(activity) {
+ const { Tag: TagConstructor } = this;
+ const {
+ classes,
+ objectProperties,
+ transitiveProperties,
+ dataProperties,
+ } = this.#schema;
+ const { object, states, unstates } = activity;
+ const activityTypes = [].concat(activity["@type"]);
+ if (!object) {
+ // ⥠The provided activity has no object.
+ throw new TypeError(
+ `${ĂTIQUETTE}: Cannot apply activity: Activity lacks an object.`,
+ );
+ } else {
+ // The provided activity has an object.
+ const iri = `${object}`;
+ const iriSpace = `${this.iriSpace}`;
+ const identifier = (() => {
+ // Extract the identifier from the object I¡R¡I.
+ if (!iri.startsWith(iriSpace)) {
+ // ⥠The object of the provided activity is not in the I¡R¡I
+ // space of this `TagSystem´.
+ throw new RangeError(
+ `Cannot apply activity: Object is not in I¡R¡I space: ${object}`,
+ );
+ } else {
+ // ⥠The object of the provided activity is in the I¡R¡I
+ // space of this `TagSystem´.
+ return iri.substring(iriSpace.length);
+ }
+ })();
+ const tag = (() => {
+ // Either resolve the identifier to an existing tag or create
+ // a new one.
+ if (activityTypes.includes("Create")) {
+ // The provided activity is a Create activity.
+ const kind = states.findLast(
+ ({ predicate, object }) =>
+ predicate == "a" && `${object}` in classes,
+ )?.object;
+ if (kind == null) {
+ // ⥠There is no recognized tag class provided for the tag;
+ // it cannot be created.
+ throw new RangeError(
+ `Cannot apply activity: Tag type not recognized.`,
+ );
+ } else {
+ // There is a recognized tag class provided for the tag.
+ return Tag.new(this, identifier, kind);
+ }
+ } else {
+ // The provided activity is not a Create activity.
+ return TagConstructor.fromIdentifier(identifier);
+ }
+ })();
+ if (!tag) {
+ // ⥠Resolving the tag identifier failed.
+ throw new RangeError(
+ `${ĂTIQUETTE}: Cannot apply activity: No tag for identifier: ${identifier}.`,
+ );
+ } else {
+ // Resolving the identifier succeeded; apply the changes to the
+ // tag and then silently persist it.
+ for (
+ const [statements, mode] of [
+ [unstates ?? [], "delete"],
+ [states ?? [], "add"],
+ ]
+ ) {
+ // Delete unstatements, then add statements.
+ for (const { predicate: $p, object: $o } of statements) {
+ // Iterate over the statements and apply them.
+ const predicate = `${$p}`;
+ const term = predicate in dataProperties
+ ? langString($o)
+ : predicate in objectProperties
+ && !(predicate in transitiveProperties
+ || objectProperties[predicate].inverseOf != null)
+ ? `${$o}`
+ : null;
+ if (term == null) {
+ // The provided predicate is not recognized; ignore it.
+ /* do nothing */
+ } else if (predicate == "prefLabel") {
+ // Preflabels are handled specially.
+ if (mode == "delete") {
+ // Unstating a preflabel has no effect unless a new one
+ // is also stated.
+ /* do nothing */
+ } else {
+ // Update the preflabel.
+ tag.prefLabel = term;
+ }
+ } else {
+ // The predicate is not `"prefLabel"´.
+ const related = (() => {
+ // If the predicate is an object property, attempt to
+ // resolve the object.
+ if (!(predicate in objectProperties)) {
+ // The predicate is not an object property; return
+ // null.
+ return null;
+ } else {
+ // The predicate is an object property.
+ try {
+ // Attempt to resolve the object.
+ return TagConstructor.fromIRI(term);
+ } catch {
+ // Resolving failed; return undefined.
+ return undefined;
+ }
+ }
+ })();
+ if (related === undefined) {
+ // The predicate is an object property, but its object
+ // was not resolvable.
+ //
+ // ⥠This is a silent error to allow for selective
+ // replay of activities while ignoring terms which are
+ // not covered.
+ /* do nothing */
+ } else {
+ // The predicate is not an object property or has a
+ // resolvable object.
+ //
+ // Apply the statement.
+ tag[
+ mode.concat(
+ predicate[0].toUpperCase(),
+ predicate.substring(1),
+ predicate in objectProperties ? "Tag" : "",
+ )
+ ](related ?? term);
+ }
+ }
+ }
+ }
+ tag.persist(true);
+ return tag;
+ }
}
}
- /** Returns the authority name (domain) for this `TagSystem`. */
+ /** Returns the authority name (domain) for this `TagSystem´. */
get authorityName() {
return this.#domain;
}
- /** Returns the date of this `TagSystem`, as a string. */
+ /** Returns the date of this `TagSystem´, as a string. */
get date() {
return this.#date;
}
/**
- * Yields the entities in this `TagSystem`.
+ * Yields the entities in this `TagSystem´.
*
* âť Entities can hypothetically be anything. If you specifically
- * want the `Tag`s, use `::Tag.all` instead.
+ * want the `Tag´s, use `::Tag.all´ instead.
*/
*entities() {
yield* this.#storage.values();
}
/**
- * Returns the identifier of this `TagSystem`.
+ * Returns the identifier of this `TagSystem´.
*
* âť Often this is just the empty string.
*/
@@ -1482,30 +1780,30 @@ export class TagSystem {
return this.#identifier;
}
- /** Yields the identifiers in use in this `TagSystem`. */
+ /** Yields the identifiers in use in this `TagSystem´. */
*identifiers() {
yield* this.#storage.keys();
}
- /** Returns the I¡R¡I for this `TagSystem`. */
+ /** Returns the I¡R¡I for this `TagSystem´. */
get iri() {
return `${this.iriSpace}${this.identifier}`;
}
/**
- * Returns the prefix used for I¡R¡Iâs of `Tag`s in this `TagSystem`.
+ * Returns the prefix used for I¡R¡IŒs of `Tag´s in this `TagSystem´.
*/
get iriSpace() {
return `https://${this.authorityName}/tag:${this.taggingEntity}:`;
}
- /** Returns the Tag U¡R¡I for this `TagSystem`. */
+ /** Returns the Tag U¡R¡I for this `TagSystem´. */
get tagURI() {
return `tag:${this.taggingEntity}:${this.identifier}`;
}
/**
- * Returns the tagging entity (domain and date) for this `TagSystem`.
+ * Returns the tagging entity (domain and date) for this `TagSystem´.
*/
get taggingEntity() {
return `${this.authorityName},${this.date}`;