]> Lady’s Gitweb - Etiquette/blob - model.js
Apply some reformatting and make Reuse‐compliant
[Etiquette] / model.js
1 // SPDX-FileCopyrightText: 2023, 2025 Lady <https://www.ladys.computer/about/#lady>
2 // SPDX-License-Identifier: MPL-2.0
3 /**
4 * ⁌ 📧🏷️ Étiquette ∷ model.js
5 *
6 * Copyright © 2023, 2025 Lady [@ Ladys Computer].
7 *
8 * This Source Code Form is subject to the terms of the Mozilla Public
9 * License, v. 2.0. If a copy of the MPL was not distributed with this
10 * file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
11 */
12
13 import { identity } from "./deps.js";
14 import { Storage } from "./memory.js";
15 import { taggingDiscoveryContext } from "./names.js";
16 import schema from "./schema.js";
17
18 const ÉTIQUETTE = "📧🏷️ Étiquette";
19
20 /**
21 * A tag.
22 *
23 * `Tag´s are not assigned identifiers and do not have side·effects on
24 * other tags in the `TagSystem´ until they are persisted with
25 * `::persist´, at which point changes to their relationships are
26 * applied.
27 *
28 * `Tag´s are also not kept up‐to‐date, but persisting an outdated
29 * `Tag´ will ⹐not⹑ undo subsequent changes.
30 *
31 * ※ This class is not itself directly exposed, although bound
32 * versions of it are via `TagSystem::Tag´.
33 */
34 class Tag {
35 /** The `TagSystem´ this `Tag´ belongs to. */
36 #system;
37
38 /** The `Storage´ managed by this `Tag´s `TagSystem´. */
39 #storage;
40
41 /** The schema in use for this `Tag´. */
42 #schema;
43
44 /**
45 * The 30‐bit W·R·M·G base32 identifier with leading checksum which
46 * has been assigned to this `Tag´.
47 *
48 * Will be `null´ if this `Tag´ has not been persisted. Otherwise,
49 * the format is `cxx-xxxx´ (`c´ = checksum; `x´ = digit).
50 */
51 #identifier = null;
52
53 /** The kind of this `Tag´. */
54 #kind = "Tag";
55
56 /**
57 * The data which was attached to this `Tag´ the last time it was
58 * persisted or retrieved from storage.
59 *
60 * Diffing with this will reveal changes.
61 */
62 #persistedData = null;
63
64 /** The current (modified) data associated with this `Tag´. */
65 #data = tagData();
66
67 /**
68 * Adds the provided label(s) to this `Tag´ as the provided
69 * predicate, then returns this `Tag´.
70 */
71 #addLabel(predicate, ...labels) {
72 const values = this.#data[predicate];
73 for (const $ of labels) {
74 // Iterate over each provided label and attempt to add it.
75 const literal = langString($);
76 values.add(literal);
77 }
78 return this;
79 }
80
81 /**
82 * Adds the provided tags to the list of tags that this `Tag´ is
83 * related to by the provided predicate, then returns this `Tag´.
84 *
85 * Arguments may be string identifiers or objects with an
86 * `.identifier´ property.
87 */
88 #addTag(predicate, ...tags) {
89 const storage = this.#storage;
90 const values = this.#data[predicate];
91 for (const $ of tags) {
92 // Iterate over each tag and attempt to state the predicate.
93 const identifier = toIdentifier($);
94 if (identifier == null) {
95 // ☡ The current tag has no identifier.
96 throw new TypeError(
97 `${ÉTIQUETTE}: Cannot state ${predicate} of Tag: Identifier must not be nullish.`,
98 );
99 } else if (values.has(identifier)) {
100 // Short‐circuit: The identifier has already been stated with
101 // this predicate.
102 /* do nothing */
103 } else {
104 // The current tag has an identifier, but it hasn¦t been stated
105 // with this predicate yet.
106 const tag = storage.get(identifier);
107 if (tag == null) {
108 // ☡ The current tag has not been persisted to this `Tag´s
109 // storage.
110 throw new RangeError(
111 `${ÉTIQUETTE}: Cannot state ${predicate} of Tag: Identifier is not persisted: ${identifier}.`,
112 );
113 } else if (!this.#isTagInStorage(tag)) {
114 // ☡ The current tag is not a tag in the correct tag system.
115 throw new TypeError(
116 `${ÉTIQUETTE}: Cannot state ${predicate} of Tag: Tags must be from the same Tag System, but got: ${identifier}.`,
117 );
118 } else if (
119 !isObjectPredicateOK(
120 this.#schema,
121 this.#kind,
122 predicate,
123 tag.#kind,
124 )
125 ) {
126 // ☡ This tag and the current tag form an invalid pair for
127 // this predicate.
128 throw new TypeError(
129 `${ÉTIQUETTE}: Cannot state ${predicate} of Tag: Not valid for domain and range: ${this.#kind}, ${tag.#kind}.`,
130 );
131 } else {
132 // The current tag is a tag in the correct tag system; add
133 // its identifier.
134 values.add(identifier);
135 }
136 }
137 }
138 return this;
139 }
140
141 /**
142 * Removes the provided string label(s) from this `Tag´ as the
143 * provided predicate, then returns this `Tag´.
144 */
145 #deleteLabel(predicate, ...labels) {
146 const values = this.#data[predicate];
147 for (const $ of labels) {
148 // Iterate over each provided label and attempt to remove it.
149 const literal = langString($);
150 values.delete(literal);
151 }
152 return this;
153 }
154
155 /**
156 * Removes the provided tags from the list of tags that this `Tag´ is
157 * related to by the provided predicate, then returns this `Tag´.
158 *
159 * Arguments may be string identifiers or objects with an
160 * `.identifier´ property.
161 */
162 #deleteTag(predicate, ...tags) {
163 const values = this.#data[predicate];
164 for (const $ of tags) {
165 // Iterate over the provided tags and delete them.
166 values.delete(toIdentifier($));
167 }
168 return this;
169 }
170
171 /**
172 * Returns whether or not the provided value is a tag which shares a
173 * storage with this tag.
174 *
175 * Sharing a storage also implies sharing a `TagSystem´.
176 */
177 #isTagInStorage($) {
178 try {
179 // Try to compare the provided values internal store with
180 // the provided storage.
181 return $.#storage == this.#storage;
182 } catch {
183 // The provided value was not a `Tag`.
184 return false;
185 }
186 }
187
188 /**
189 * Yields the labels of this `Tag´ according to the provided
190 * predicate.
191 */
192 *#yieldLabels(predicate) {
193 yield* this.#data[predicate];
194 }
195
196 /**
197 * Yields the tags that this `Tag´ is related to by the provided
198 * predicate.
199 */
200 *#yieldTags(predicate) {
201 const storage = this.#storage;
202 for (const identifier of this.#data[predicate]) {
203 // Iterate over the tags in this predicate and yield them if
204 // possible.
205 const tag = storage.get(identifier);
206 if (
207 !this.#isTagInStorage(tag) || !isObjectPredicateOK(
208 this.#schema,
209 this.#kind,
210 predicate,
211 tag.#kind,
212 )
213 ) {
214 // The tag no longer appears in storage or is not compatible;
215 // perhaps it was deleted.
216 /* do nothing */
217 } else {
218 // The tag exists and is constructable from storage.
219 yield tag;
220 }
221 }
222 }
223
224 /**
225 * Yields the tags that this `Tag´ is related to by the provided
226 * predicate, figured transitively.
227 */
228 *#yieldTransitiveTags(transitivePredicate, basePredicate) {
229 const storage = this.#storage;
230 const encountered = new Set();
231 let pending = new Set(this.#data[basePredicate]);
232 while (pending.size > 0) {
233 // Loop until all tags of the predicate have been encountered.
234 const processing = pending;
235 pending = new Set();
236 for (const identifier of processing) {
237 // Iterate over the tags and yield them if possible.
238 if (!encountered.has(identifier)) {
239 // The tag has not been encountered before.
240 encountered.add(identifier);
241 const tag = storage.get(identifier);
242 if (
243 !this.#isTagInStorage(tag) || !isObjectPredicateOK(
244 this.#schema,
245 this.#kind,
246 transitivePredicate,
247 tag.#kind,
248 )
249 ) {
250 // The tag no longer appears in storage or is not
251 // compatible; perhaps it was deleted.
252 /* do nothing */
253 } else {
254 // The tag exists and is constructable from storage.
255 yield tag;
256 for (const transitive of tag.#data[basePredicate]) {
257 // Iterate over the nested tags of the current tag and
258 // add them to pending as needed.
259 if (!encountered.has(transitive)) {
260 // The nested tag has not been encountered yet.
261 pending.add(transitive);
262 } else {
263 // The nested tag has already been encountered.
264 /* do nothing */
265 }
266 }
267 }
268 } else {
269 // The tag has already been encountered.
270 /* do nothing */
271 }
272 }
273 }
274 }
275
276 /**
277 * Constructs a new `Tag´ of the provided kind and with the provided
278 * preferred label.
279 *
280 * ※ The first two arguments of this constructor are bound when
281 * generating the value of `TagSystem::Tag´. It isn¦t possible to
282 * access this constructor in its unbound form from outside this
283 * module.
284 *
285 * ☡ This constructor throws if the provided kind is not supported.
286 */
287 constructor(system, storage, schema, kind = "Tag", prefLabel = "") {
288 this.#system = system;
289 this.#storage = storage;
290 this.#schema = schema;
291 const kindString = `${kind}`;
292 if (!(kindString in schema.classes)) {
293 // The provided kind is not supported.
294 throw new RangeError(
295 `${ÉTIQUETTE}: Cannot construct Tag: Unrecognized kind: ${kind}.`,
296 );
297 } else {
298 // The provided kind is one of the recognized tag kinds.
299 this.#kind = kindString;
300 this.#data.prefLabel = prefLabel;
301 }
302 }
303
304 /**
305 * Returns a new `Tag´ constructor for the provided system, storage,
306 * schema, created with an appropriate prototype for the properties
307 * so defined.
308 *
309 * ※ This function is not exposed.
310 */
311 static For(system, storage, schema) {
312 const {
313 objectProperties,
314 transitiveProperties,
315 dataProperties,
316 } = schema;
317 const constructor = function (...$s) {
318 return Reflect.construct(
319 Tag,
320 [system, storage, schema, ...$s],
321 new.target,
322 );
323 };
324 Object.defineProperties(constructor, {
325 name: { value: "TagSystem::Tag" },
326 prototype: {
327 configurable: false,
328 enumerable: false,
329 value: Object.create(
330 Tag.prototype,
331 Object.fromEntries(Array.from(
332 function* () {
333 for (const key in objectProperties) {
334 // Iterate over each object property and yield any
335 // necessary method definitions.
336 const {
337 inverseOf,
338 subPropertyOf,
339 } = objectProperties[key];
340 if (key in transitiveProperties) {
341 // The current key indicates a transitive property.
342 //
343 // Transitive property methods are added by their
344 // nontransitive subproperties.
345 /* do nothing */
346 } else {
347 // The current key does not indicate a transitive
348 // property.
349 yield [`${key}Tags`, function* () {
350 yield* this.#yieldTags(key);
351 }];
352 if (inverseOf == null) {
353 // The current key does not indicate an inverse
354 // property, so add and delete methods are also
355 // added.
356 const cased = key[0].toUpperCase()
357 + key.substring(1);
358 yield [`add${cased}Tag`, function (...tags) {
359 return this.#addTag(key, ...tags);
360 }];
361 yield [`delete${cased}Tag`, function (...tags) {
362 return this.#deleteTag(key, ...tags);
363 }];
364 } else {
365 // The current key indicates an inverse property,
366 // so no add and delete methods are necessary.
367 /* do nothing */
368 }
369 if (
370 subPropertyOf != null
371 && subPropertyOf in transitiveProperties
372 ) {
373 // The current key indicates a subproperty of a
374 // transitive property; its method is also added.
375 yield [`${subPropertyOf}Tags`, function* () {
376 yield* this.#yieldTransitiveTags(
377 subPropertyOf,
378 key,
379 );
380 }];
381 } else {
382 // The current key does not indicate a subproperty
383 // of a transitive property.
384 /* do nothing */
385 }
386 }
387 }
388 for (const key in dataProperties) {
389 // Iterate over each data property and yield any
390 // necessary method definitions.
391 if (key != "prefLabel") {
392 // The current key is not `"prefLabel"´.
393 const cased = key[0].toUpperCase()
394 + key.substring(1);
395 yield [`${key}s`, function* () {
396 yield* this.#yieldLabels(key);
397 }];
398 yield [`add${cased}`, function (...labels) {
399 return this.#addLabel(key, ...labels);
400 }];
401 yield [`delete${cased}`, function (...labels) {
402 return this.#deleteLabel(key, ...labels);
403 }];
404 } else {
405 // The current key is `"prefLabel"´. This is a
406 // special case which is not handled by the schema.
407 /* do nothing */
408 }
409 }
410 }(),
411 ([key, value]) => [key, {
412 configurable: true,
413 enumerable: false,
414 value: Object.defineProperty(value, "name", {
415 value: key,
416 }),
417 writable: true,
418 }],
419 )),
420 ),
421 writable: false,
422 },
423 });
424 return new TagConstructor(constructor, system, storage, schema);
425 }
426
427 /**
428 * Assigns the provided data and identifier to the provided tag.
429 *
430 * ☡ This function throws if the provided tag is not a `Tag´.
431 *
432 * ※ This function is not exposed.
433 */
434 static assignData(tag, data, identifier) {
435 tag.#identifier = `${identifier}`;
436 tag.#persistedData = tagData(data);
437 tag.#data = tagData(data);
438 return tag;
439 }
440
441 /**
442 * Returns a new `Tag´ with the provided identifier, kind, and
443 * prefLabel.
444 *
445 * ※ This function exists to enable `TagSystem´s to replay Create
446 * activities, maintaining the identifier of the original.
447 *
448 * ☡ This function throws if the provided identifier is already in
449 * use.
450 *
451 * ※ This function is not exposed.
452 */
453 static new(system, identifier, kind = "Tag", prefLabel = "") {
454 const storage = (new system.Tag()).#storage;
455 if (storage.has(identifier)) {
456 throw new RangeError(
457 `${ÉTIQUETTE}: Cannot create Tag: Identifier already in use: ${identifier}.`,
458 );
459 } else {
460 const createdTag = new system.Tag(kind, prefLabel);
461 createdTag.#identifier = identifier;
462 createdTag.persist(true);
463 return createdTag;
464 }
465 }
466
467 /**
468 * Returns the `TagSystem´ that the provided value belongs to.
469 *
470 * ※ This function can be used to check if the provided value has
471 * private tag features.
472 *
473 * ※ `Tag::system´ is an overridable, publicly‐accessible means of
474 * accessing the system.
475 *
476 * ※ This function is not exposed.
477 */
478 static getSystem($) {
479 return !(#system in Object($)) ? null : $.#system;
480 }
481
482 static {
483 // Overwrite the default `::constructor´ method to instead give the
484 // actual (bound) constructor which was used to generate a given
485 // `Tag´.
486 Object.defineProperties(this.prototype, {
487 constructor: {
488 configurable: true,
489 enumerable: false,
490 get() {
491 // All `Tag´s are constructed via the `.Tag´ constructor
492 // available in their `TagSystem´; return it.
493 return this.#system.Tag;
494 },
495 set: undefined,
496 },
497 });
498 }
499
500 /** Returns the authority (domain) name for this `Tag´. */
501 get authorityName() {
502 return this.#system.authorityName;
503 }
504
505 /** Returns the identifier of this `Tag´. */
506 get identifier() {
507 return this.#identifier;
508 }
509
510 /** Returns the I·R·I for this `Tag´. */
511 get iri() {
512 const { identifier, iriSpace } = this;
513 return identifier == null ? null : `${iriSpace}${identifier}`;
514 }
515
516 /** Returns the I·R·I space for this `Tag´. */
517 get iriSpace() {
518 return this.#system.iriSpace;
519 }
520
521 /** Returns the kind of this `Tag´. */
522 get kind() {
523 return this.#kind;
524 }
525
526 /**
527 * Returns the `TagSystem´ for this `Tag´.
528 *
529 * ※ Internally, `Tag.getSystem´ is preferred.
530 */
531 get system() {
532 return this.#system;
533 }
534
535 /**
536 * Persist this `Tag´ to storage and return an ActivityStreams
537 * serialization of a Tag Activity representing any changes, or
538 * `null´ if no changes were made.
539 *
540 * If the second argument is `true´, the `Tag´ will be persisted but
541 * no serialization will be made. This is somewhat more efficient.
542 *
543 * ※ Persistence can imply side‐effects on other objects, which are
544 * not noted explicitly in the activity. For example, marking a tag
545 * as broader than another causes the other tag to reciprocally be
546 * marked as narrower.
547 *
548 * ※ Inverse object properties will never appear in the predicates
549 * of generated activities.
550 */
551 persist(silent = false) {
552 const system = this.#system;
553 const storage = this.#storage;
554 const {
555 objectProperties,
556 transitiveProperties,
557 dataProperties,
558 } = this.#schema;
559 const persistedData = this.#persistedData;
560 const data = this.#data;
561 const diffs = {};
562 for (const [key, value] of Object.entries(data)) {
563 // Iterate over each entry of the tag data and create a diff
564 // with the last persisted information.
565 if (
566 objectProperties[key]?.inverseOf != null
567 || silent && key in dataProperties
568 ) {
569 // The current property is one which is skipped in diffs.
570 //
571 // In a silent persist, this includes any literal terms.
572 /* do nothing */
573 } else {
574 // The current property should be diffed.
575 const persisted = persistedData?.[key] ?? null;
576 if (persisted == null) {
577 // There is no persisted data for the current property yet.
578 diffs[key] = {
579 old: new Set(),
580 new: value instanceof Set
581 ? new Set(value)
582 : new Set([value]),
583 };
584 } else if (value instanceof Set) {
585 // The current property is set‐valued.
586 const oldValues = new Set(persisted);
587 const newValues = new Set(value);
588 for (const existing of persisted) {
589 // Iterate over each persisted property and either remove
590 // it from the list of new values or add it to the list of
591 // removed ones.
592 if (value.has(existing)) {
593 // The value is in both the old and new version of the
594 // data.
595 oldValues.delete(existing);
596 newValues.delete(existing);
597 } else {
598 // The value is not shared.
599 /* do nothing */
600 }
601 }
602 diffs[key] = { old: oldValues, new: newValues };
603 } else if (
604 `${value}` != `${persisted}`
605 || value.language != persisted.language
606 ) {
607 // The current property is (optionally language‐tagged)
608 // string‐valued and the value changed.
609 diffs[key] = {
610 old: new Set([persisted]),
611 new: new Set([value]),
612 };
613 } else {
614 // The current property did not change.
615 diffs[key] = { old: new Set(), new: new Set() };
616 }
617 }
618 }
619 const identifier = this.#identifier;
620 if (identifier != null) {
621 // This `Tag´ has already been persisted; use its existing
622 // identifier and persist.
623 storage.set(identifier, this);
624 } else {
625 // This `Tag´ has not been persisted yet; save the new
626 // identifier after persisting.
627 this.#identifier = storage.add(this);
628 }
629 const persistedIdentifier = this.#identifier;
630 this.#persistedData = tagData(data); // cloning here is necessary
631 for (const inverse in objectProperties) {
632 // Iterate over each non‐transitive inverse property and update
633 // it based on its inverse on the corresponding tags if possible.
634 const term = objectProperties[inverse].inverseOf;
635 if (term == null || term in transitiveProperties) {
636 // The current property is not the inverse of an non‐transitive
637 // property.
638 /* do nothing */
639 } else {
640 // The current property is the inverse of a non‐transitive
641 // property.
642 for (const referencedIdentifier of diffs[term].old) {
643 // Iterate over the removed tags and remove this `Tag´ from
644 // their inverse property.
645 const referenced = storage.get(referencedIdentifier);
646 try {
647 // Try removing this `Tag´.
648 referenced.#data[inverse].delete(persistedIdentifier);
649 storage.set(referencedIdentifier, referenced);
650 } catch {
651 // Removal failed, possibly because the other tag was
652 // deleted.
653 /* do nothing */
654 }
655 }
656 for (const referencedIdentifier of diffs[term].new) {
657 const referenced = storage.get(referencedIdentifier);
658 try {
659 // Try adding this `Tag`.
660 referenced.#data[inverse].add(persistedIdentifier);
661 storage.set(referencedIdentifier, referenced);
662 } catch {
663 // Adding failed, possibly because the other tag was
664 // deleted.
665 /* do nothing */
666 }
667 }
668 }
669 }
670 if (silent) {
671 // This is a silent persist.
672 return undefined;
673 } else {
674 // This is not a silent persist; an activity needs to be
675 // generated if a change was made.
676 const activity = {
677 "@context": taggingDiscoveryContext,
678 "@type": [
679 "TagActivity",
680 identifier == null ? "Create" : "Update",
681 ],
682 context: `${system.iri}`,
683 object: `${this.iri}`,
684 endTime: new Date().toISOString(),
685 ...(() => {
686 const statements = {
687 unstates: [],
688 states: [],
689 };
690 const { unstates, states } = statements;
691 if (identifier == null) {
692 // This is a Create activity.
693 states.push({ predicate: "a", object: `${this.kind}` });
694 } else {
695 // This is an Update activity.
696 /* do nothing */
697 }
698 for (
699 const [term, {
700 old: oldValues,
701 new: newValues,
702 }] of Object.entries(diffs)
703 ) {
704 // Iterate over the diffs of each term and state/unstate
705 // things as needed.
706 for (const oldValue of oldValues) {
707 // Iterate over removals and unstate them.
708 if (term in dataProperties) {
709 // This is a literal term; push it.
710 unstates.push({
711 predicate: term,
712 object: { ...oldValue },
713 });
714 } else {
715 // This is a named term; attempt to get its I·R·I and
716 // push it.
717 try {
718 // Attempt to resolve the value and push the change.
719 const tag = storage.get(oldValue);
720 if (!this.#isTagInStorage(tag)) {
721 // The value did not resolve to a tag in storage.
722 /* do nothing */
723 } else {
724 // The value resolved; push its I·R·I.
725 unstates.push({
726 predicate: term,
727 object: tag.iri,
728 });
729 }
730 } catch {
731 // Value resolution failed for some reason; perhaps
732 // the tag was deleted.
733 /* do nothing */
734 }
735 }
736 }
737 for (const newValue of newValues) {
738 // Iterate over additions and state them.
739 if (term in dataProperties) {
740 // This is a literal term; push it.
741 states.push({
742 predicate: term,
743 object: { ...newValue },
744 });
745 } else {
746 // This is a named term; attempt to get its I·R·I and
747 // push it.
748 try {
749 // Attempt to resolve the value and push the change.
750 const tag = storage.get(newValue);
751 if (!this.#isTagInStorage(tag)) {
752 // The value did not resolve to a tag in storage.
753 /* do nothing */
754 } else {
755 // The value resolved; push its I·R·I.
756 states.push({
757 predicate: term,
758 object: tag.iri,
759 });
760 }
761 } catch {
762 // Value resolution failed for some reason; perhaps
763 // the tag was deleted.
764 /* do nothing */
765 }
766 }
767 }
768 }
769 if (unstates.length == 0) {
770 // Nothing was unstated.
771 delete statements.unstates;
772 } else {
773 // Things were stated.
774 /* do nothing */
775 }
776 if (states.length == 0) {
777 // Nothing was stated.
778 delete statements.states;
779 } else {
780 // Things were stated.
781 /* do nothing */
782 }
783 return statements;
784 })(),
785 };
786 if (
787 !Object.hasOwn(activity, "states")
788 && !Object.hasOwn(activity, "unstates")
789 ) {
790 // No meaningful changes were actually persisted.
791 return null;
792 } else {
793 // There were meaningful changes persisted regarding this
794 // `Tag´.
795 return activity;
796 }
797 }
798 }
799
800 /** Returns the preferred label for this `Tag´. */
801 get prefLabel() {
802 return this.#data.prefLabel;
803 }
804
805 /** Sets the preferred label of this `Tag´ to the provided label. */
806 set prefLabel($) {
807 this.#data.prefLabel = langString($);
808 }
809
810 /** Returns the Tag U·R·I for this `Tag´. */
811 get tagURI() {
812 const { identifier } = this;
813 return identifier == null
814 ? null
815 : `tag:${this.taggingEntity}:${identifier}`;
816 }
817
818 /** Returns the tagging entity (domain and date) for this `Tag´. */
819 get taggingEntity() {
820 return this.#system.taggingEntity;
821 }
822
823 /** Returns the string form of the preferred label of this `Tag´. */
824 toString() {
825 return `${this.#data.prefLabel}`;
826 }
827
828 /**
829 * Returns a new object whose enumerable own properties contain the
830 * data from this object needed for storage.
831 *
832 * ※ This method is not really intended for public usage.
833 */
834 [Storage.toObject]() {
835 const data = this.#data;
836 return Object.assign(Object.create(null), {
837 ...data,
838 kind: this.#kind,
839 });
840 }
841 }
842
843 const {
844 /**
845 * A `Tag´ constructor function.
846 *
847 * This class extends the identity function, meaning that the object
848 * provided as the constructor is used verbatim (with new private
849 * fields added).
850 *
851 * ※ The instance methods of this class are provided as static
852 * methods on the superclass which all `Tag´ constructors inherit
853 * from.
854 *
855 * ※ This class is not exposed.
856 */
857 TagConstructor,
858
859 /**
860 * The exposed constructor function from which all `Tag´ constructors
861 * inherit.
862 *
863 * ☡ This constructor always throws.
864 */
865 TagSuper,
866 } = (() => {
867 const tagConstructorBehaviours = Object.create(null);
868 return {
869 TagConstructor: class extends identity {
870 /**
871 * The `TagSystem´ used for `Tag´s constructed by this
872 * constructor.
873 */
874 #system;
875
876 /** The `Storage´ managed by this constructors `TagSystem´. */
877 #storage;
878
879 /** The schema in use for this constructor. */
880 #schema;
881
882 /**
883 * Constructs a new `Tag´ constructor by adding the appropriate
884 * private fields to the provided constructor, setting its
885 * prototype, and then returning it.
886 *
887 * ※ This constructor does not modify the `name´ or `prototype´
888 * properties of the provided constructor.
889 *
890 * ※ See `Tag.For´, where this constructor is used.
891 */
892 constructor(constructor, system, storage, schema) {
893 super(constructor);
894 Object.setPrototypeOf(this, TagSuper);
895 this.#system = system;
896 this.#storage = storage;
897 this.#schema = schema;
898 }
899
900 static {
901 // Define the superclass constructor which all `Tag´
902 // constructors will inherit from.
903 const superclass = tagConstructorBehaviours.TagSuper =
904 function Tag() {
905 throw new TypeError("Tags must belong to a System.");
906 };
907 const { prototype: staticFeatures } = this;
908 delete staticFeatures.constructor;
909 Object.defineProperty(superclass, "prototype", {
910 configurable: false,
911 enumerable: false,
912 value: Tag.prototype,
913 writable: false,
914 });
915 Object.defineProperties(
916 superclass,
917 Object.getOwnPropertyDescriptors(staticFeatures),
918 );
919 }
920
921 /**
922 * Yields the tags in the `TagSystem´ associated with this
923 * constructor.
924 */
925 *all() {
926 const system = this.#system;
927 const storage = this.#storage;
928 for (const instance of storage.values()) {
929 // Iterate over the entries and yield the ones which are
930 // `Tag´s in this `TagSystem´.
931 if (Tag.getSystem(instance) == system) {
932 // The current instance is a `Tag´ in this `TagSystem´.
933 yield instance;
934 } else {
935 // The current instance is not a `Tag´ in this
936 // `TagSystem´.
937 /* do nothing */
938 }
939 }
940 }
941
942 /**
943 * Returns a new `Tag´ resolved from the provided I·R·I.
944 *
945 * ☡ This function throws if the I·R·I is not in the `.iriSpace´
946 * of the `TagSystem´ associated with this constructor.
947 *
948 * ※ If the I·R·I is not recognized, this function returns
949 * `undefined`.
950 */
951 fromIRI(iri) {
952 const system = this.#system;
953 const storage = this.#storage;
954 const name = `${iri}`;
955 const prefix = `${system.iriSpace}`;
956 if (!name.startsWith(prefix)) {
957 // The I·R·I does not begin with the expected prefix.
958 throw new RangeError(
959 `${ÉTIQUETTE}: I·R·I did not begin with the expected prefix: ${iri}`,
960 );
961 } else {
962 // The I·R·I begins with the expected prefix.
963 const identifier = name.substring(prefix.length);
964 try {
965 // Attempt to resolve the identifier.
966 const instance = storage.get(identifier);
967 return Tag.getSystem(instance) == system
968 ? instance
969 : undefined;
970 } catch {
971 // Do not throw for bad identifiers.
972 return undefined;
973 }
974 }
975 }
976
977 /**
978 * Returns a new `Tag´ resolved from the provided identifier.
979 *
980 * ☡ This function throws if the identifier is invalid.
981 *
982 * ※ If the identifier is valid but not recognized, this
983 * function returns `undefined´.
984 */
985 fromIdentifier(identifier) {
986 const system = this.#system;
987 const storage = this.#storage;
988 const instance = storage.get(identifier);
989 return Tag.getSystem(instance) == system
990 ? instance
991 : undefined;
992 }
993
994 /**
995 * Returns a new `Tag´ resolved from the provided Tag U·R·I.
996 *
997 * ☡ This function throws if the provided Tag U·R·I does not
998 * match the tagging entity of this constructors `TagSystem´.
999 *
1000 * ※ If the specific component of the Tag U·R·I is not
1001 * recognized, this function returns `undefined´.
1002 */
1003 fromTagURI(tagURI) {
1004 const system = this.#system;
1005 const storage = this.#storage;
1006 const tagName = `${tagURI}`;
1007 const tagPrefix = `tag:${system.taggingEntity}:`;
1008 if (!tagName.startsWith(tagPrefix)) {
1009 // The Tag U·R·I does not begin with the expected prefix.
1010 throw new RangeError(
1011 `${ÉTIQUETTE}: Tag U·R·I did not begin with the expected prefix: ${tagURI}`,
1012 );
1013 } else {
1014 // The I·R·I begins with the expected prefix.
1015 const identifier = tagName.substring(tagPrefix.length);
1016 try {
1017 // Attempt to resolve the identifier.
1018 const instance = storage.get(identifier);
1019 return Tag.getSystem(instance) == system
1020 ? instance
1021 : undefined;
1022 } catch {
1023 // Do not throw for bad identifiers.
1024 return undefined;
1025 }
1026 }
1027 }
1028
1029 /**
1030 * Yields the tag identifiers in the `TagSystem´ associated with
1031 * this constructor.
1032 */
1033 *identifiers() {
1034 const system = this.#system;
1035 const storage = this.#storage;
1036 for (const [identifier, instance] of storage.entries()) {
1037 // Iterate over the entries and yield the ones which are
1038 // `Tag´s in this `TagSystem´.
1039 if (Tag.getSystem(instance) == system) {
1040 // The current instance is a `Tag´ in this `TagSystem´.
1041 yield identifier;
1042 } else {
1043 // The current instance is not a `Tag´ in this `TagSystem´.
1044 /* do nothing */
1045 }
1046 }
1047 }
1048
1049 /** Returns the `TagSystem´ for this `Tag´ constructor. */
1050 get system() {
1051 return this.#system;
1052 }
1053
1054 /**
1055 * Returns a new `Tag´ constructed from the provided data and
1056 * with the provided identifier.
1057 *
1058 * ※ This function is not really intended for public usage.
1059 */
1060 [Storage.toInstance](data, identifier) {
1061 const tag = new this(data.kind);
1062 return Tag.assignData(tag, data, identifier);
1063 }
1064 },
1065 TagSuper: tagConstructorBehaviours.TagSuper,
1066 };
1067 })();
1068
1069 const {
1070 /**
1071 * Returns whether the provided schema, subject class, object
1072 * property, and object class are consistent.
1073 *
1074 * This is hardly a full reasoner; it is tuned to the abilites and
1075 * needs of this module.
1076 */
1077 isObjectPredicateOK,
1078 } = (() => {
1079 const cachedClassAndSuperclasses = new WeakMap();
1080 const cachedClassRestrictions = new WeakMap();
1081 const cachedPredicateRestrictions = new WeakMap();
1082
1083 const classAndSuperclasses = function* (
1084 classes,
1085 baseClass,
1086 touched = new Set(),
1087 ) {
1088 if (baseClass == "Thing" || touched.has(baseClass)) {
1089 /* do nothing */
1090 } else {
1091 yield baseClass;
1092 touched.add(baseClass);
1093 const subClassOf = classes[baseClass]?.subClassOf ?? "Thing";
1094 for (
1095 const superclass of (
1096 typeof subClassOf == "string"
1097 ? [subClassOf]
1098 : Array.from(subClassOf)
1099 ).filter(($) => typeof $ == "string")
1100 ) {
1101 yield* classAndSuperclasses(classes, superclass, touched);
1102 }
1103 }
1104 };
1105
1106 const getClassAndSuperclasses = (schema, baseClass) => {
1107 const schemaCache = cachedClassAndSuperclasses.get(schema);
1108 const cached = schemaCache?.[baseClass];
1109 if (cached != null) {
1110 return cached;
1111 } else {
1112 const { classes } = schema;
1113 const result = [...classAndSuperclasses(classes, baseClass)];
1114 if (schemaCache) {
1115 schemaCache[baseClass] = result;
1116 } else {
1117 cachedClassRestrictions.set(
1118 schema,
1119 Object.assign(Object.create(null), { [baseClass]: result }),
1120 );
1121 }
1122 return result;
1123 }
1124 };
1125
1126 const getClassRestrictions = (schema, domain) => {
1127 const schemaCache = cachedClassRestrictions.get(schema);
1128 const cached = schemaCache?.[domain];
1129 if (cached != null) {
1130 return cached;
1131 } else {
1132 const { classes } = schema;
1133 const restrictions = Object.create(null);
1134 const subClassOf = classes[domain]?.subClassOf ?? "Thing";
1135 for (
1136 const superclass of (
1137 typeof subClassOf == "string"
1138 ? [subClassOf]
1139 : Array.from(subClassOf)
1140 ).filter(($) => Object($) === $)
1141 ) {
1142 const { onProperty, allValuesFrom } = superclass;
1143 restrictions[onProperty] = processSpace(allValuesFrom);
1144 }
1145 if (schemaCache) {
1146 schemaCache[domain] = restrictions;
1147 } else {
1148 cachedClassRestrictions.set(
1149 schema,
1150 Object.assign(Object.create(null), {
1151 [domain]: restrictions,
1152 }),
1153 );
1154 }
1155 return restrictions;
1156 }
1157 };
1158
1159 const getPredicateRestrictions = (schema, predicate) => {
1160 const schemaCache = cachedPredicateRestrictions.get(schema);
1161 const cached = schemaCache?.[predicate];
1162 if (cached != null) {
1163 return cached;
1164 } else {
1165 const { objectProperties } = schema;
1166 const restrictions = [
1167 ...predicateRestrictions(objectProperties, predicate),
1168 ].reduce(
1169 (result, { domainIntersection, rangeIntersection }) => {
1170 result.domainIntersection.push(...domainIntersection);
1171 result.rangeIntersection.push(...rangeIntersection);
1172 return result;
1173 },
1174 Object.assign(Object.create(null), {
1175 domainIntersection: [],
1176 rangeIntersection: [],
1177 }),
1178 );
1179 if (schemaCache) {
1180 schemaCache[predicate] = restrictions;
1181 } else {
1182 cachedPredicateRestrictions.set(
1183 schema,
1184 Object.assign(Object.create(null), {
1185 [predicate]: restrictions,
1186 }),
1187 );
1188 }
1189 return restrictions;
1190 }
1191 };
1192
1193 const processSpace = (space) =>
1194 Object(space) === space
1195 ? "length" in space
1196 ? Array.from(
1197 space,
1198 (subspace) =>
1199 Object(subspace) === subspace
1200 ? Array.from(subspace.unionOf)
1201 : [subspace],
1202 )
1203 : [Array.from(space.unionOf)]
1204 : [[space]];
1205
1206 const predicateRestrictions = function* (
1207 objectProperties,
1208 predicate,
1209 touched = new Set(),
1210 ) {
1211 if (predicate == "Property" || touched.has(predicate)) {
1212 /* do nothing */
1213 } else {
1214 const { domain, range, subPropertyOf } =
1215 objectProperties[predicate];
1216 yield Object.assign(Object.create(null), {
1217 domainIntersection: processSpace(domain ?? "Thing"),
1218 rangeIntersection: processSpace(range ?? "Thing"),
1219 });
1220 touched.add(predicate);
1221 for (
1222 const superproperty of (
1223 subPropertyOf == null
1224 ? ["Property"]
1225 : typeof subPropertyOf == "string"
1226 ? [subPropertyOf]
1227 : Array.from(subPropertyOf)
1228 )
1229 ) {
1230 yield* predicateRestrictions(
1231 objectProperties,
1232 superproperty,
1233 touched,
1234 );
1235 }
1236 }
1237 };
1238
1239 return {
1240 isObjectPredicateOK: (
1241 schema,
1242 subjectClass,
1243 predicate,
1244 objectClass,
1245 ) => {
1246 const { objectProperties } = schema;
1247 const predicateDefinition = objectProperties[predicate];
1248 const isInverse = "inverseOf" in predicateDefinition;
1249 const usedPredicate = isInverse
1250 ? predicateDefinition.inverseOf
1251 : predicate;
1252 const domain = isInverse ? objectClass : subjectClass;
1253 const domains = new Set(getClassAndSuperclasses(schema, domain));
1254 const ranges = new Set(getClassAndSuperclasses(
1255 schema,
1256 isInverse ? subjectClass : objectClass,
1257 ));
1258 const predicateRestrictions = getPredicateRestrictions(
1259 schema,
1260 usedPredicate,
1261 );
1262 const { domainIntersection } = predicateRestrictions;
1263 const rangeIntersection = [
1264 ...predicateRestrictions.rangeIntersection,
1265 ...function* () {
1266 for (const domain of domains) {
1267 const classRestrictionOnPredicate =
1268 getClassRestrictions(schema, domain)[usedPredicate];
1269 if (classRestrictionOnPredicate != null) {
1270 yield* classRestrictionOnPredicate;
1271 } else {
1272 /* do nothing */
1273 }
1274 }
1275 }(),
1276 ];
1277 return domainIntersection.every((domainUnion) =>
1278 domainUnion.some((domain) =>
1279 domain == "Thing" || domains.has(domain)
1280 )
1281 )
1282 && rangeIntersection.every((rangeUnion) =>
1283 rangeUnion.some((range) =>
1284 range == "Thing" || ranges.has(range)
1285 )
1286 );
1287 },
1288 };
1289 })();
1290
1291 const {
1292 /**
1293 * Returns the provided value converted into a `String´ object with
1294 * `.["@value"]´ and `.["@language"]´ properties.
1295 *
1296 * The same object will be returned for every call with an equivalent
1297 * value.
1298 *
1299 * TODO: Ideally this would be extracted more fully into an R·D·F
1300 * library.
1301 *
1302 * ※ This function is not exposed.
1303 */
1304 langString,
1305 } = (() => {
1306 /**
1307 * Returns the language string object corresponding to the provided
1308 * value and language.
1309 */
1310 const getLangString = (value, language = "") => {
1311 const valueMap = languageMap[language] ??= Object.create(null);
1312 const literal = valueMap[value]?.deref();
1313 if (literal != null) {
1314 // There is already an object corresponding to the provided value
1315 // and language.
1316 return literal;
1317 } else {
1318 // No object already exists corresponding to the provided value
1319 // and language; create one.
1320 const result = Object.preventExtensions(
1321 Object.create(String.prototype, {
1322 "@value": {
1323 enumerable: true,
1324 value,
1325 },
1326 "@language": {
1327 enumerable: !!language,
1328 value: language || null,
1329 },
1330 language: { enumerable: false, get: getLanguage },
1331 toString: { enumerable: false, value: toString },
1332 valueOf: { enumerable: false, value: valueOf },
1333 }),
1334 );
1335 const ref = new WeakRef(result);
1336 langStringRegistry.register(result, { ref, language, value });
1337 valueMap[value] = ref;
1338 return result;
1339 }
1340 };
1341
1342 /** Returns the `.["@language"]´ of this object. */
1343 const getLanguage = Object.defineProperty(
1344 function () {
1345 return this["@language"] || null;
1346 },
1347 "name",
1348 { value: "get language" },
1349 );
1350
1351 /**
1352 * A `FinalizationRegistry´ for language string objects.
1353 *
1354 * This simply cleans up the corresponding `WeakRef´ in the language
1355 * map.
1356 */
1357 const langStringRegistry = new FinalizationRegistry(
1358 ({ ref, language, value }) => {
1359 const valueMap = languageMap[language];
1360 if (valueMap?.[value] === ref) {
1361 delete valueMap[value];
1362 } else {
1363 /* do nothing */
1364 }
1365 },
1366 );
1367
1368 /**
1369 * An object whose own values are an object mapping values to
1370 * language string objects for the language specified by the key.
1371 */
1372 const languageMap = Object.create(null);
1373
1374 /** Returns the `.["@value"]´ of this object. */
1375 const toString = function () {
1376 return this["@value"];
1377 };
1378
1379 /**
1380 * Returns this object if it has a `.["@language"]´; otherwise, its
1381 * `.["@value"]´.
1382 */
1383 const valueOf = function () {
1384 return this["@language"] ? this : this["@value"];
1385 };
1386
1387 return {
1388 langString: ($) =>
1389 Object($) === $
1390 ? "@value" in $
1391 ? "@language" in $
1392 ? getLangString(
1393 `${$["@value"]}`,
1394 `${$["@language"] ?? ""}`,
1395 )
1396 : getLangString(`${$["@value"]}`)
1397 : "language" in $
1398 ? getLangString(`${$}`, `${$.language ?? ""}`)
1399 : getLangString(`${$}`)
1400 : getLangString(`${$ ?? ""}`),
1401 };
1402 })();
1403
1404 /**
1405 * Returns a normalized tag data object derived from the provided
1406 * object.
1407 *
1408 * ※ The properties of this function need to match the term names used
1409 * in the ActivityStreams serialization.
1410 *
1411 * ※ This function is not exposed.
1412 */
1413 const tagData = ($) => {
1414 const data = Object($);
1415 const {
1416 // prefLabel intentionally not set here
1417 altLabel,
1418 hiddenLabel,
1419 broader,
1420 narrower,
1421 inCanon,
1422 hasInCanon,
1423 involves,
1424 involvedIn,
1425 } = data;
1426 let prefLabel = langString(data.prefLabel);
1427 return Object.preventExtensions(Object.create(null, {
1428 prefLabel: {
1429 enumerable: true,
1430 get: () => prefLabel,
1431 set: ($) => {
1432 prefLabel = langString($);
1433 },
1434 },
1435 altLabel: {
1436 enumerable: true,
1437 value: new Set(
1438 altLabel != null
1439 ? Array.from(altLabel, langString)
1440 : undefined,
1441 ),
1442 },
1443 hiddenLabel: {
1444 enumerable: true,
1445 value: new Set(
1446 hiddenLabel != null
1447 ? Array.from(hiddenLabel, langString)
1448 : undefined,
1449 ),
1450 },
1451 broader: {
1452 enumerable: true,
1453 value: new Set(
1454 broader != null
1455 ? Array.from(broader, toIdentifier)
1456 : undefined,
1457 ),
1458 },
1459 narrower: {
1460 enumerable: true,
1461 value: new Set(
1462 narrower != null
1463 ? Array.from(narrower, toIdentifier)
1464 : undefined,
1465 ),
1466 },
1467 inCanon: {
1468 enumerable: true,
1469 value: new Set(
1470 inCanon != null
1471 ? Array.from(inCanon, toIdentifier)
1472 : undefined,
1473 ),
1474 },
1475 hasInCanon: {
1476 enumerable: true,
1477 value: new Set(
1478 hasInCanon != null
1479 ? Array.from(hasInCanon, toIdentifier)
1480 : undefined,
1481 ),
1482 },
1483 involves: {
1484 enumerable: true,
1485 value: new Set(
1486 involves != null
1487 ? Array.from(involves, toIdentifier)
1488 : undefined,
1489 ),
1490 },
1491 involvedIn: {
1492 enumerable: true,
1493 value: new Set(
1494 involvedIn != null
1495 ? Array.from(involvedIn, toIdentifier)
1496 : undefined,
1497 ),
1498 },
1499 }));
1500 };
1501
1502 /**
1503 * Returns an identifier corresponding to the provided object.
1504 *
1505 * This is either the value of its `.identifier´ or its string value.
1506 *
1507 * ※ This function is not exposed.
1508 */
1509 const toIdentifier = ($) =>
1510 $ == null
1511 ? null
1512 : Object($) === $ && "identifier" in $
1513 ? $.identifier
1514 : `${$}`;
1515
1516 /**
1517 * A tag system, with storage.
1518 *
1519 * The `::Tag´ constructor available on any `TagSystem´ instance can be
1520 * used to create new `Tag´s within the system.
1521 */
1522 export class TagSystem {
1523 /** The cached bound `Tag´ constructor for this `TagSystem´. */
1524 #Tag = null;
1525
1526 /** The domain of this `TagSystem´. */
1527 #domain;
1528
1529 /** The date of this `TagSystem´. */
1530 #date;
1531
1532 /** The identifier of this `TagSystem´. */
1533 #identifier;
1534
1535 /** The schema used by this `TagSystem´. */
1536 #schema = schema;
1537
1538 /** The internal `Storage` of this `TagSystem´. */
1539 #storage = new Storage();
1540
1541 /**
1542 * Constructs a new `TagSystem´ with the provided domain and date.
1543 *
1544 * Only actual, lowercased domain names are allowed for the domain,
1545 * and the date must be “full” (include month and day components).
1546 * This is for alignment with general best practices for Tag U·R·I¦s.
1547 *
1548 * ☡ This constructor throws if provided with an invalid date.
1549 */
1550 constructor(domain, date, identifier = "") {
1551 const domainString = `${domain}`;
1552 const dateString = `${date}`;
1553 this.#identifier = `${identifier}`;
1554 try {
1555 // If the identifier is a valid storage I·D, reserve it.
1556 this.#storage.delete(this.#identifier);
1557 } catch {
1558 // The identifier is not a valid storage I·D, so no worries.
1559 /* do nothing */
1560 }
1561 if (
1562 !/^[a-z](?:[\da-z-]*[\da-z])?(?:\.[a-z](?:[\da-z-]*[\da-z])?)*$/u
1563 .test(domainString)
1564 ) {
1565 // ☡ The domain is invalid.
1566 throw new RangeError(`Invalid domain: ${domain}.`);
1567 } else if (
1568 !/^\d{4}-\d{2}-\d{2}$/u.test(dateString)
1569 || dateString != new Date(dateString).toISOString().split("T")[0]
1570 ) {
1571 // ☡ The date is invalid.
1572 throw new RangeError(`Invalid date: ${date}.`);
1573 } else {
1574 // The domain and date are 🆗.
1575 this.#domain = domainString;
1576 this.#date = dateString;
1577 }
1578 }
1579
1580 /**
1581 * Returns a bound constructor for constructing `Tags´ in this
1582 * `TagSystem´.
1583 */
1584 get Tag() {
1585 if (this.#Tag != null) {
1586 // A bound constructor has already been generated; return it.
1587 return this.#Tag;
1588 } else {
1589 // No bound constructor has been created yet.
1590 const storage = this.#storage;
1591 return this.#Tag = Tag.For(this, storage, this.#schema);
1592 }
1593 }
1594
1595 /**
1596 * Applies the provided activity to this `TagSystem´ by replaying its
1597 * statement changes.
1598 *
1599 * ※ This method assumes that the provided activity conforms to the
1600 * assumptions made by this module; i·e that its tags use the same
1601 * identifier format and its activities do not use statements with
1602 * inverse property predicates. It is not intended for the generic
1603 * playback of activities produced by other scripts or mechanisms,
1604 * for which a more sophisticated solution is required.
1605 *
1606 * ☡ This method throws an error if the provided activity cannot be
1607 * processed.
1608 */
1609 apply(activity) {
1610 const { Tag: TagConstructor } = this;
1611 const {
1612 classes,
1613 objectProperties,
1614 transitiveProperties,
1615 dataProperties,
1616 } = this.#schema;
1617 const { object, states, unstates } = activity;
1618 const activityTypes = [].concat(activity["@type"]);
1619 if (!object) {
1620 // ☡ The provided activity has no object.
1621 throw new TypeError(
1622 `${ÉTIQUETTE}: Cannot apply activity: Activity lacks an object.`,
1623 );
1624 } else {
1625 // The provided activity has an object.
1626 const iri = `${object}`;
1627 const iriSpace = `${this.iriSpace}`;
1628 const identifier = (() => {
1629 // Extract the identifier from the object I·R·I.
1630 if (!iri.startsWith(iriSpace)) {
1631 // ☡ The object of the provided activity is not in the I·R·I
1632 // space of this `TagSystem´.
1633 throw new RangeError(
1634 `Cannot apply activity: Object is not in I·R·I space: ${object}`,
1635 );
1636 } else {
1637 // ☡ The object of the provided activity is in the I·R·I
1638 // space of this `TagSystem´.
1639 return iri.substring(iriSpace.length);
1640 }
1641 })();
1642 const tag = (() => {
1643 // Either resolve the identifier to an existing tag or create
1644 // a new one.
1645 if (activityTypes.includes("Create")) {
1646 // The provided activity is a Create activity.
1647 const kind = states.findLast(
1648 ({ predicate, object }) =>
1649 predicate == "a" && `${object}` in classes,
1650 )?.object;
1651 if (kind == null) {
1652 // ☡ There is no recognized tag class provided for the tag;
1653 // it cannot be created.
1654 throw new RangeError(
1655 `Cannot apply activity: Tag type not recognized.`,
1656 );
1657 } else {
1658 // There is a recognized tag class provided for the tag.
1659 return Tag.new(this, identifier, kind);
1660 }
1661 } else {
1662 // The provided activity is not a Create activity.
1663 return TagConstructor.fromIdentifier(identifier);
1664 }
1665 })();
1666 if (!tag) {
1667 // ☡ Resolving the tag identifier failed.
1668 throw new RangeError(
1669 `${ÉTIQUETTE}: Cannot apply activity: No tag for identifier: ${identifier}.`,
1670 );
1671 } else {
1672 // Resolving the identifier succeeded; apply the changes to the
1673 // tag and then silently persist it.
1674 for (
1675 const [statements, mode] of [
1676 [unstates ?? [], "delete"],
1677 [states ?? [], "add"],
1678 ]
1679 ) {
1680 // Delete unstatements, then add statements.
1681 for (const { predicate: $p, object: $o } of statements) {
1682 // Iterate over the statements and apply them.
1683 const predicate = `${$p}`;
1684 const term = predicate in dataProperties
1685 ? langString($o)
1686 : predicate in objectProperties
1687 && !(predicate in transitiveProperties
1688 || objectProperties[predicate].inverseOf != null)
1689 ? `${$o}`
1690 : null;
1691 if (term == null) {
1692 // The provided predicate is not recognized; ignore it.
1693 /* do nothing */
1694 } else if (predicate == "prefLabel") {
1695 // Preflabels are handled specially.
1696 if (mode == "delete") {
1697 // Unstating a preflabel has no effect unless a new one
1698 // is also stated.
1699 /* do nothing */
1700 } else {
1701 // Update the preflabel.
1702 tag.prefLabel = term;
1703 }
1704 } else {
1705 // The predicate is not `"prefLabel"´.
1706 const related = (() => {
1707 // If the predicate is an object property, attempt to
1708 // resolve the object.
1709 if (!(predicate in objectProperties)) {
1710 // The predicate is not an object property; return
1711 // null.
1712 return null;
1713 } else {
1714 // The predicate is an object property.
1715 try {
1716 // Attempt to resolve the object.
1717 return TagConstructor.fromIRI(term);
1718 } catch {
1719 // Resolving failed; return undefined.
1720 return undefined;
1721 }
1722 }
1723 })();
1724 if (related === undefined) {
1725 // The predicate is an object property, but its object
1726 // was not resolvable.
1727 //
1728 // ☡ This is a silent error to allow for selective
1729 // replay of activities while ignoring terms which are
1730 // not covered.
1731 /* do nothing */
1732 } else {
1733 // The predicate is not an object property or has a
1734 // resolvable object.
1735 //
1736 // Apply the statement.
1737 tag[
1738 mode.concat(
1739 predicate[0].toUpperCase(),
1740 predicate.substring(1),
1741 predicate in objectProperties ? "Tag" : "",
1742 )
1743 ](related ?? term);
1744 }
1745 }
1746 }
1747 }
1748 tag.persist(true);
1749 return tag;
1750 }
1751 }
1752 }
1753
1754 /** Returns the authority name (domain) for this `TagSystem´. */
1755 get authorityName() {
1756 return this.#domain;
1757 }
1758
1759 /** Returns the date of this `TagSystem´, as a string. */
1760 get date() {
1761 return this.#date;
1762 }
1763
1764 /**
1765 * Yields the entities in this `TagSystem´.
1766 *
1767 * ※ Entities can hypothetically be anything. If you specifically
1768 * want the `Tag´s, use `::Tag.all´ instead.
1769 */
1770 *entities() {
1771 yield* this.#storage.values();
1772 }
1773
1774 /**
1775 * Returns the identifier of this `TagSystem´.
1776 *
1777 * ※ Often this is just the empty string.
1778 */
1779 get identifier() {
1780 return this.#identifier;
1781 }
1782
1783 /** Yields the identifiers in use in this `TagSystem´. */
1784 *identifiers() {
1785 yield* this.#storage.keys();
1786 }
1787
1788 /** Returns the I·R·I for this `TagSystem´. */
1789 get iri() {
1790 return `${this.iriSpace}${this.identifier}`;
1791 }
1792
1793 /**
1794 * Returns the prefix used for I·R·I¦s of `Tag´s in this `TagSystem´.
1795 */
1796 get iriSpace() {
1797 return `https://${this.authorityName}/tag:${this.taggingEntity}:`;
1798 }
1799
1800 /** Returns the Tag U·R·I for this `TagSystem´. */
1801 get tagURI() {
1802 return `tag:${this.taggingEntity}:${this.identifier}`;
1803 }
1804
1805 /**
1806 * Returns the tagging entity (domain and date) for this `TagSystem´.
1807 */
1808 get taggingEntity() {
1809 return `${this.authorityName},${this.date}`;
1810 }
1811 }
This page took 0.245199 seconds and 5 git commands to generate.