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