X-Git-Url: https://git.ladys.computer/Etiquette/blobdiff_plain/6aa0118531463c0f1716b19013607658966a84cc..f8903c5b3bd12d02af174e5043d906d63da0b0d1:/model.js?ds=sidebyside diff --git a/model.js b/model.js index cfd228d..d2990eb 100644 --- a/model.js +++ b/model.js @@ -1,67 +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]; @@ -74,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; @@ -89,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( @@ -121,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 @@ -134,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]; @@ -148,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]; @@ -167,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 { @@ -181,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) { @@ -189,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) { @@ -217,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) { @@ -269,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. * @@ -287,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. @@ -297,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. * @@ -348,8 +353,8 @@ class Tag { // 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); + const cased = key[0].toUpperCase() + + key.substring(1); yield [`add${cased}Tag`, function (...tags) { return this.#addTag(key, ...tags); }]; @@ -362,8 +367,8 @@ class Tag { /* 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. @@ -384,9 +389,9 @@ class Tag { // 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); + // The current key is not `"prefLabel"´. + const cased = key[0].toUpperCase() + + key.substring(1); yield [`${key}s`, function* () { yield* this.#yieldLabels(key); }]; @@ -397,7 +402,7 @@ class Tag { return this.#deleteLabel(key, ...labels); }]; } else { - // The current key is `"prefLabel"`. This is a + // The current key is `"prefLabel"´. This is a // special case which is not handled by the schema. /* do nothing */ } @@ -422,7 +427,7 @@ class Tag { /** * Assigns the provided data and identifier to the provided tag. * - * ☡ This function throws if the provided tag is not a `Tag`. + * ☡ This function throws if the provided tag is not a `Tag´. * * ※ This function is not exposed. */ @@ -434,11 +439,40 @@ class Tag { } /** - * Returns the `TagSystem` that the provided value belongs to. + * Returns a new `Tag´ with the provided identifier, kind, and + * prefLabel. + * + * ※ This function exists to enable `TagSystem´s to replay Create + * activities, maintaining the identifier of the original. + * + * ☡ This function throws if the provided identifier is already in + * use. + * + * ※ This function is not exposed. + */ + static new(system, identifier, kind = "Tag", prefLabel = "") { + const storage = (new system.Tag()).#storage; + if (storage.has(identifier)) { + throw new RangeError( + `${ÉTIQUETTE}: Cannot create Tag: Identifier already in use: ${identifier}.`, + ); + } else { + const createdTag = new system.Tag(kind, prefLabel); + createdTag.#identifier = identifier; + createdTag.persist(true); + return createdTag; + } + } + + /** + * 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($) { @@ -446,16 +480,16 @@ class 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, @@ -463,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 @@ -520,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. // @@ -558,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. @@ -575,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); } @@ -597,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 { @@ -617,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 */ } } @@ -684,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 */ } } @@ -715,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 */ } } @@ -740,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 @@ -770,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}`; } @@ -797,14 +842,14 @@ class Tag { const { /** - * A `Tag` constructor function. + * 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 + * methods on the superclass which all `Tag´ constructors inherit * from. * * ※ This class is not exposed. @@ -812,7 +857,7 @@ const { TagConstructor, /** - * The exposed constructor function from which all `Tag` constructors + * The exposed constructor function from which all `Tag´ constructors * inherit. * * ☡ This constructor always throws. @@ -823,26 +868,26 @@ const { return { TagConstructor: class extends identity { /** - * The `TagSystem` used for `Tag`s constructed by this + * The `TagSystem´ used for `Tag´s constructed by this * constructor. */ #system; - /** The `Storage` managed by this constructor’s `TagSystem`. */ + /** 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 + * 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` + * ※ This constructor does not modify the `name´ or `prototype´ * properties of the provided constructor. * - * ※ See `Tag.For`, where this constructor is used. + * ※ See `Tag.For´, where this constructor is used. */ constructor(constructor, system, storage, schema) { super(constructor); @@ -853,14 +898,14 @@ const { } static { - // Define the superclass constructor which all `Tag` + // 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; + const { prototype: staticFeatures } = this; + delete staticFeatures.constructor; Object.defineProperty(superclass, "prototype", { configurable: false, enumerable: false, @@ -869,12 +914,12 @@ const { }); Object.defineProperties( superclass, - Object.getOwnPropertyDescriptors(methods), + Object.getOwnPropertyDescriptors(staticFeatures), ); } /** - * Yields the tags in the `TagSystem` associated with this + * Yields the tags in the `TagSystem´ associated with this * constructor. */ *all() { @@ -882,23 +927,23 @@ const { 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`. + // `Tag´s in this `TagSystem´. if (Tag.getSystem(instance) == system) { - // The current instance is a `Tag` in this `TagSystem`. + // The current instance is a `Tag´ in this `TagSystem´. yield instance; } else { - // The current instance is not a `Tag` in this - // `TagSystem`. + // The current instance is not a `Tag´ in this + // `TagSystem´. /* do nothing */ } } } /** - * Returns a new `Tag` resolved from the provided I·R·I. + * 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. + * ☡ 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`. @@ -911,7 +956,7 @@ const { 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}`, + `${ÉTIQUETTE}: I·R·I did not begin with the expected prefix: ${iri}`, ); } else { // The I·R·I begins with the expected prefix. @@ -930,12 +975,12 @@ const { } /** - * Returns a new `Tag` resolved from the provided identifier. + * 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`. + * function returns `undefined´. */ fromIdentifier(identifier) { const system = this.#system; @@ -947,13 +992,13 @@ const { } /** - * Returns a new `Tag` resolved from the provided Tag U·R·I. + * 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`. + * 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`. + * recognized, this function returns `undefined´. */ fromTagURI(tagURI) { const system = this.#system; @@ -963,7 +1008,7 @@ const { 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}`, + `${ÉTIQUETTE}: Tag U·R·I did not begin with the expected prefix: ${tagURI}`, ); } else { // The I·R·I begins with the expected prefix. @@ -982,7 +1027,7 @@ const { } /** - * Yields the tag identifiers in the `TagSystem` associated with + * Yields the tag identifiers in the `TagSystem´ associated with * this constructor. */ *identifiers() { @@ -990,19 +1035,24 @@ const { 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`. + // `Tag´s in this `TagSystem´. if (Tag.getSystem(instance) == system) { - // The current instance is a `Tag` in this `TagSystem`. + // The current instance is a `Tag´ in this `TagSystem´. yield identifier; } else { - // The current instance is not a `Tag` in this `TagSystem`. + // 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 + * Returns a new `Tag´ constructed from the provided data and * with the provided identifier. * * ※ This function is not really intended for public usage. @@ -1228,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) ) @@ -1240,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. @@ -1289,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; @@ -1299,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( @@ -1321,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"]; @@ -1452,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. */ @@ -1466,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. */ @@ -1512,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}.`); @@ -1525,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) { @@ -1535,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. */ @@ -1568,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}`;