X-Git-Url: https://git.ladys.computer/Etiquette/blobdiff_plain/e20983aca1b36dd368d52e02ee018c2eaf0ca55f..f8903c5b3bd12d02af174e5043d906d63da0b0d1:/model.js?ds=sidebyside 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}`;