]> Lady’s Gitweb - Etiquette/blob - model.js
6963a16778601567b5d56bef31763af503eeeba1
[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 /**
389 * Adds the provided label(s) to this `Tag` as alternate labels, then
390 * returns this `Tag`.
391 */
392 addAltLabel(...labels) {
393 const altLabels = this.#data.altLabel;
394 let objectLabels = null; // initialized on first use
395 for (const $ of labels) {
396 // Iterate over each provided label and attempt to add it.
397 const literal = langString($);
398 if (Object(literal) === literal) {
399 // The current label is a language‐tagged string.
400 objectLabels ??= [...function* () {
401 for (const altLabel of altLabels) {
402 // Iterate over the existing labels and yield the
403 // language‐tagged strings.
404 if (Object(altLabel) === altLabel) {
405 // The current existing label is a language‐tagged
406 // string.
407 yield altLabel;
408 } else {
409 // The current existing label is not a language‐tagged
410 // string.
411 /* do nothing */
412 }
413 }
414 }()];
415 if (
416 objectLabels.some((objectLabel) =>
417 objectLabel["@value"] == literal["@value"] &&
418 objectLabel["@language"] == literal["@language"]
419 )
420 ) {
421 // There is a match with the current label in the existing
422 // labels.
423 /* do nothing */
424 } else {
425 // There is no match and this label must be added.
426 altLabels.add(literal);
427 objectLabels.push(literal);
428 }
429 } else {
430 // The current label is a simple string.
431 altLabels.add(literal);
432 }
433 }
434 return this;
435 }
436
437 /**
438 * Adds the provided tags to the list of tags that this `Tag` is
439 * narrower than, then returns this `Tag`.
440 *
441 * Arguments may be string identifiers or objects with an
442 * `.identifier` property.
443 */
444 addBroaderTag(...tags) {
445 const storage = this.#storage;
446 const broader = this.#data.broader;
447 for (const $ of tags) {
448 // Iterate over each tag and attempt to set it as broader than
449 // this `Tag`.
450 const identifier = toIdentifier($);
451 if (identifier == null) {
452 // ☡ The current tag has no identifier.
453 throw new TypeError(
454 "Cannot assign broader to Tag: Identifier must not be nullish.",
455 );
456 } else if (broader.has(identifier)) {
457 // Short‐circuit: The identifier is already something this
458 // `Tag` is narrower than.
459 /* do nothing */
460 } else {
461 // The current tag has an identifier.
462 const tag = storage.get(identifier);
463 if (tag == null) {
464 // ☡ The current tag has not been persisted to this `Tag`’s
465 // storage.
466 throw new RangeError(
467 `Cannot assign broader to Tag: Identifier is not persisted: ${identifier}.`,
468 );
469 } else if (!this.#isTagInStorage(tag)) {
470 // ☡ The current tag is not a tag in the correct tag system.
471 throw new TypeError(
472 `Cannot assign broader to Tag: Tags must be from the same Tag System, but got: ${identifier}.`,
473 );
474 } else {
475 // The current tag is a tag in the correct tag system; add
476 // its identifier.
477 broader.add(identifier);
478 }
479 }
480 }
481 return this;
482 }
483
484 /**
485 * Adds the provided label(s) to this `Tag` as hidden labels, then
486 * returns this `Tag`.
487 */
488 addHiddenLabel(...labels) {
489 const hiddenLabels = this.#data.hiddenLabel;
490 let objectLabels = null; // initialized on first use
491 for (const $ of labels) {
492 // Iterate over each provided label and attempt to add it.
493 const literal = langString($);
494 if (Object(literal) === literal) {
495 // The current label is a language‐tagged string.
496 objectLabels ??= [...function* () {
497 for (const hiddenLabel of hiddenLabels) {
498 // Iterate over the existing labels and yield the
499 // language‐tagged strings.
500 if (Object(hiddenLabel) === hiddenLabel) {
501 // The current existing label is a language‐tagged
502 // string.
503 yield hiddenLabel;
504 } else {
505 // The current existing label is not a language‐tagged
506 // string.
507 /* do nothing */
508 }
509 }
510 }()];
511 if (
512 objectLabels.some((objectLabel) =>
513 objectLabel["@value"] == literal["@value"] &&
514 objectLabel["@language"] == literal["@language"]
515 )
516 ) {
517 // There is a match with the current label in the existing
518 // labels.
519 /* do nothing */
520 } else {
521 // There is no match and this label must be added.
522 hiddenLabels.add(literal);
523 objectLabels.push(literal);
524 }
525 } else {
526 // The current label is a simple string.
527 hiddenLabels.add(literal);
528 }
529 }
530 return this;
531 }
532
533 /**
534 * Adds the provided tags to the list of tags that this `Tag` is in
535 * canon with, then returns this `Tag`.
536 *
537 * Arguments may be string identifiers or objects with an
538 * `.identifier` property.
539 *
540 * ☡ This method will throw if a provided argument does not indicate
541 * a canon tag, or if this `Tag` is not of a kind which can be placed
542 * in canon.
543 */
544 addInCanonTag(...tags) {
545 const storage = this.#storage;
546 const kind = this.#kind;
547 const inCanon = this.#data.inCanon;
548 if (!HAS_IN_CANON.has(kind)) {
549 // ☡ This is not an entity tag, setting tag, or recognized
550 // subclass.
551 throw new TypeError(
552 `Cannot put Tag in canon: Incorrect Tag type: ${kind}.`,
553 );
554 } else {
555 // This has a kind which can be placed in canon.
556 for (const $ of tags) {
557 // Iterate over each tag and attempt to set this `Tag` in canon
558 // of it.
559 const identifier = toIdentifier($);
560 if (identifier == null) {
561 // ☡ The current tag has no identifier.
562 throw new TypeError(
563 "Cannot put Tag in canon: Identifier must not be nullish.",
564 );
565 } else if (inCanon.has(identifier)) {
566 // Short‐circuit: The identifier is already something this
567 // `Tag` is in canon of.
568 /* do nothing */
569 } else {
570 // The current tag has an identifier.
571 const tag = storage.get(identifier);
572 if (tag == null) {
573 // ☡ The current tag has not been persisted to this `Tag`’s
574 // storage.
575 throw new RangeError(
576 `Cannot put Tag in canon: Identifier is not persisted: ${identifier}.`,
577 );
578 } else if (
579 // ※ If the first check succeeds, then the current tag
580 // must have `Tag` private class features.
581 !this.#isTagInStorage(tag) || tag.#kind != "CanonTag"
582 ) {
583 // ☡ The current tag is not a canon tag in the correct
584 // tag system.
585 throw new TypeError(
586 `Cannot put Tag in canon: Tags can only be in Canon Tags from the same Tag System, but got: ${identifier}.`,
587 );
588 } else {
589 // The current tag is a canon tag in the correct tag
590 // system; add its identifier.
591 inCanon.add(identifier);
592 }
593 }
594 }
595 }
596 return this;
597 }
598
599 /**
600 * Adds the provided tags to the list of tags that this `Tag`
601 * involves, then returns this `Tag`.
602 *
603 * Arguments may be string identifiers or objects with an
604 * `.identifier` property.
605 *
606 * ☡ This method will throw if this `Tag` is not a conceptual tag, or
607 * if this `Tag` is a relationship tag and a provided argument does
608 * not indicate a character or relationship tag.
609 */
610 addInvolvesTag(...tags) {
611 const storage = this.#storage;
612 const kind = this.#kind;
613 const involves = this.#data.involves;
614 if (!CONCEPTUAL_TAG_KINDS.has(kind)) {
615 // ☡ This is not a conceptual tag or recognized subclass.
616 throw new TypeError(
617 `Cannot involve Tag: Incorrect Tag type: ${kind}.`,
618 );
619 } else {
620 // This is a conceptual tag.
621 for (const $ of tags) {
622 // Iterate over each tag and attempt to set this `Tag` as
623 // involving it.
624 const identifier = toIdentifier($);
625 if (identifier == null) {
626 // ☡ The current tag has no identifier.
627 throw new TypeError(
628 "Cannot involve Tag: Identifier must not be nullish.",
629 );
630 } else if (involves.has(identifier)) {
631 // Short‐circuit: The identifier is already something this
632 // `Tag` involves.
633 /* do nothing */
634 } else {
635 // The current tag has an identifier.
636 const tag = storage.get(identifier);
637 if (tag == null) {
638 // ☡ The current tag has not been persisted to this `Tag`’s
639 // storage.
640 throw new RangeError(
641 `Cannot involve Tag: Identifier is not persisted: ${identifier}.`,
642 );
643 } else if (
644 // ※ If the first check succeeds, then the current tag
645 // must have `Tag` private class features.
646 !this.#isTagInStorage(tag) ||
647 RELATIONSHIP_TAG_KINDS.has(kind) &&
648 !INVOLVABLE_IN_RELATIONSHIP.has(tag.#kind)
649 ) {
650 // ☡ The current tag is in the correct tag system and
651 // includable.
652 throw new TypeError(
653 `Cannot involve Tag: Tags must be the same Tag System and involvable, but got: ${identifier}.`,
654 );
655 } else {
656 // The current tag is an involvable tag in the correct tag
657 // system; add its identifier.
658 involves.add(identifier);
659 }
660 }
661 }
662 }
663 return this;
664 }
665
666 /** Yields the alternative labels of this `Tag`. */
667 *altLabels() {
668 yield* this.#data.altLabel;
669 }
670
671 /** Returns the authority (domain) name for this `Tag`. */
672 get authorityName() {
673 return this.#system.authorityName;
674 }
675
676 /** Yields `Tag`s which are broader than this `Tag`. */
677 *broaderTags() {
678 const storage = this.#storage;
679 for (const identifier of this.#data.broader) {
680 // Iterate over the broader tags and yield them if possible.
681 const tag = storage.get(identifier);
682 if (!this.#isTagInStorage(tag)) {
683 // The broader tag no longer appears in storage; perhaps it was
684 // deleted.
685 /* do nothing */
686 } else {
687 // The broader tag exists and is constructable from storage.
688 yield tag;
689 }
690 }
691 }
692
693 /** Yields `Tag`s which are broader than this `Tag`, transitively. */
694 *broaderTransitiveTags() {
695 const storage = this.#storage;
696 const encountered = new Set();
697 let pending = new Set(this.#data.broader);
698 while (pending.size > 0) {
699 // Loop until all broader tags have been encountered.
700 const processing = pending;
701 pending = new Set();
702 for (const identifier of processing) {
703 // Iterate over the broader tags and yield them if possible.
704 if (!encountered.has(identifier)) {
705 // The broader tag has not been encountered before.
706 encountered.add(identifier);
707 const tag = storage.get(identifier);
708 if (!this.#isTagInStorage(tag)) {
709 // The broader tag no longer appears in storage; perhaps it
710 // was deleted.
711 /* do nothing */
712 } else {
713 // The broader tag exists and is constructable from
714 // storage.
715 yield tag;
716 for (const transitive of tag.#data.broader) {
717 // Iterate over the broader tags of the current broader
718 // tag and add them to pending as needed.
719 if (!encountered.has(transitive)) {
720 // The broader broader tag has not been encountered
721 // yet.
722 pending.add(transitive);
723 } else {
724 // The broader broader tag has already been
725 // encountered.
726 /* do nothing */
727 }
728 }
729 }
730 } else {
731 // The broader tag has already been encountered.
732 /* do nothing */
733 }
734 }
735 }
736 }
737
738 /**
739 * Removes the provided string label(s) from this `Tag` as alternate
740 * labels, then returns this `Tag`.
741 */
742 deleteAltLabel(...labels) {
743 const altLabels = this.#data.altLabel;
744 let objectLabels = null; // initialized on first use
745 for (const $ of labels) {
746 // Iterate over each provided label and attempt to remove it.
747 const literal = langString($);
748 if (Object(literal) === literal) {
749 // The current label is a language‐tagged string.
750 objectLabels ??= [...function* () {
751 for (const altLabel of altLabels) {
752 // Iterate over the existing labels and yield the
753 // language‐tagged strings.
754 if (Object(altLabel) === altLabel) {
755 // The current existing label is a language‐tagged
756 // string.
757 yield altLabel;
758 } else {
759 // The current existing label is not a language‐tagged
760 // string.
761 /* do nothing */
762 }
763 }
764 }()];
765 const existing = objectLabels.find((objectLabel) =>
766 objectLabel["@value"] == literal["@value"] &&
767 objectLabel["@language"] == literal["@language"]
768 );
769 altLabels.delete(existing);
770 } else {
771 // The current label is a simple string.
772 altLabels.delete(literal);
773 }
774 }
775 return this;
776 }
777
778 /**
779 * Removes the provided tags from the list of tags that this `Tag` is
780 * narrower than, then returns this `Tag`.
781 *
782 * Arguments may be string identifiers or objects with an
783 * `.identifier` property.
784 */
785 deleteBroaderTag(...tags) {
786 const broader = this.#data.broader;
787 for (const $ of tags) {
788 // Iterate over the provided tags and delete them.
789 broader.delete(toIdentifier($));
790 }
791 return this;
792 }
793
794 /**
795 * Removes the provided string label(s) from this `Tag` as hidden
796 * labels, then returns this `Tag`.
797 */
798 deleteHiddenLabel(...labels) {
799 const hiddenLabels = this.#data.hiddenLabel;
800 let objectLabels = null; // initialized on first use
801 for (const $ of labels) {
802 // Iterate over each provided label and attempt to remove it.
803 const literal = langString($);
804 if (Object(literal) === literal) {
805 // The current label is a language‐tagged string.
806 objectLabels ??= [...function* () {
807 for (const hiddenLabel of hiddenLabels) {
808 // Iterate over the existing labels and yield the
809 // language‐tagged strings.
810 if (Object(hiddenLabel) === hiddenLabel) {
811 // The current existing label is a language‐tagged
812 // string.
813 yield hiddenLabel;
814 } else {
815 // The current existing label is not a language‐tagged
816 // string.
817 /* do nothing */
818 }
819 }
820 }()];
821 const existing = objectLabels.find((objectLabel) =>
822 objectLabel["@value"] == literal["@value"] &&
823 objectLabel["@language"] == literal["@language"]
824 );
825 hiddenLabels.delete(existing);
826 } else {
827 // The current label is a simple string.
828 hiddenLabels.delete(literal);
829 }
830 }
831 return this;
832 }
833
834 /**
835 * Removes the provided tags from the list of tags that this `Tag` is
836 * in canon with, then returns this `Tag`.
837 *
838 * Arguments may be string identifiers or objects with an
839 * `.identifier` property.
840 */
841 deleteInCanonTag(...tags) {
842 const inCanon = this.#data.inCanon;
843 for (const $ of tags) {
844 // Iterate over the provided tags and delete them.
845 inCanon.delete(toIdentifier($));
846 }
847 return this;
848 }
849
850 /**
851 * Removes the provided tags from the list of tags that this `Tag`
852 * involves, then returns this `Tag`.
853 *
854 * Arguments may be string identifiers or objects with an
855 * `.identifier` property.
856 */
857 deleteInvolvesTag(...tags) {
858 const involves = this.#data.involves;
859 for (const $ of tags) {
860 // Iterate over the provided tags and delete them.
861 involves.delete(toIdentifier($));
862 }
863 return this;
864 }
865
866 /** Yields `Tag`s that are in canon of this `Tag`. */
867 *hasInCanonTags() {
868 const storage = this.#storage;
869 if (this.#kind == "CanonTag") {
870 // This is a canon tag.
871 for (const identifier of this.#data.hasInCanon) {
872 // Iterate over the tags in canon and yield them if possible.
873 const tag = storage.get(identifier);
874 if (
875 !this.#isTagInStorage(tag) || !HAS_IN_CANON.has(tag.#kind)
876 ) {
877 // The tag in canon no longer appears in storage; perhaps it
878 // was deleted.
879 /* do nothing */
880 } else {
881 // The tag in canon exists and is constructable from storage.
882 yield tag;
883 }
884 }
885 } else {
886 /* do nothing */
887 }
888 }
889
890 /** Yields the hidden labels of this `Tag`. */
891 *hiddenLabels() {
892 yield* this.#data.hiddenLabel;
893 }
894
895 /** Returns the identifier of this `Tag`. */
896 get identifier() {
897 return this.#identifier;
898 }
899
900 /** Yields `Tag`s that this `Tag` is in canon of. */
901 *inCanonTags() {
902 const storage = this.#storage;
903 if (HAS_IN_CANON.has(this.#kind)) {
904 // This tag can be placed in canon.
905 for (const identifier of this.#data.inCanon) {
906 // Iterate over the canon tags and yield them if possible.
907 const tag = storage.get(identifier);
908 if (!this.#isTagInStorage(tag) || tag.#kind != "CanonTag") {
909 // The canon tag no longer appears in storage; perhaps it was
910 // deleted.
911 /* do nothing */
912 } else {
913 // The canon tag exists and is constructable from storage.
914 yield tag;
915 }
916 }
917 } else {
918 // This tag cannot be placed in canon.
919 /* do nothing */
920 }
921 }
922
923 /** Yields `Tag`s which involve this `Tag`. */
924 *involvedInTags() {
925 const storage = this.#storage;
926 for (const identifier of this.#data.involvedIn) {
927 // Iterate over the involving tags and yield them if possible.
928 const tag = storage.get(identifier);
929 const tagKind = tag.#kind;
930 if (
931 !this.#isTagInStorage(tag) ||
932 !CONCEPTUAL_TAG_KINDS.has(tagKind) ||
933 RELATIONSHIP_TAG_KINDS.has(tagKind) &&
934 !INVOLVABLE_IN_RELATIONSHIP.has(this.#kind)
935 ) {
936 // The including tag no longer appears in storage; perhaps it
937 // was deleted.
938 /* do nothing */
939 } else {
940 // The including tag exists and is constructable from storage.
941 yield tag;
942 }
943 }
944 }
945
946 /** Yields `Tag`s that this `Tag` involves. */
947 *involvesTags() {
948 const storage = this.#storage;
949 const kind = this.#kind;
950 if (CONCEPTUAL_TAG_KINDS.has(kind)) {
951 // This tag can involve other tags.
952 for (const identifier of this.#data.involves) {
953 // Iterate over the involved and yield them if possible.
954 const tag = storage.get(identifier);
955 if (
956 !this.#isTagInStorage(tag) ||
957 RELATIONSHIP_TAG_KINDS.has(kind) &&
958 !INVOLVABLE_IN_RELATIONSHIP.has(tag.#kind)
959 ) {
960 // The involved tag no longer appears in storage; perhaps it
961 // was deleted.
962 /* do nothing */
963 } else {
964 // The involved tag exists and is constructable from storage.
965 yield tag;
966 }
967 }
968 } else {
969 // This tag cannot involve other tags.
970 /* do nothing */
971 }
972 }
973
974 /** Returns the I·R·I for this `Tag`. */
975 get iri() {
976 const tagURI = this.tagURI;
977 return tagURI == null
978 ? null
979 : `https://${this.authorityName}/${tagURI}`;
980 }
981
982 /** Returns the kind of this `Tag`. */
983 get kind() {
984 return this.#kind;
985 }
986
987 /** Yields `Tag`s which are narrower than this `Tag`. */
988 *narrowerTags() {
989 const storage = this.#storage;
990 for (const identifier of this.#data.narrower) {
991 const tag = storage.get(identifier);
992 if (!this.#isTagInStorage(tag)) {
993 // The narrower tag no longer appears in storage; perhaps it
994 // was deleted.
995 /* do nothing */
996 } else {
997 // The narrower tag exists and is constructable from storage.
998 yield tag;
999 }
1000 }
1001 }
1002
1003 /**
1004 * Yields `Tag`s which are narrower than this `Tag`, transitively.
1005 */
1006 *narrowerTransitiveTags() {
1007 const storage = this.#storage;
1008 const encountered = new Set();
1009 let pending = new Set(this.#data.narrower);
1010 while (pending.size > 0) {
1011 // Loop until all narrower tags have been encountered.
1012 const processing = pending;
1013 pending = new Set();
1014 for (const identifier of processing) {
1015 // Iterate over the narrower tags and yield them if possible.
1016 if (!encountered.has(identifier)) {
1017 // The narrower tag has not been encountered before.
1018 encountered.add(identifier);
1019 const tag = storage.get(identifier);
1020 if (!this.#isTagInStorage(tag)) {
1021 // The narrower tag no longer appears in storage; perhaps
1022 // it was deleted.
1023 /* do nothing */
1024 } else {
1025 // The narrower tag exists and is constructable from
1026 // storage.
1027 yield tag;
1028 for (const transitive of tag.#data.narrower) {
1029 // Iterate over the narrower tags of the current narrower
1030 // tag and add them to pending as needed.
1031 if (!encountered.has(transitive)) {
1032 // The narrower narrower tag has not been encountered
1033 // yet.
1034 pending.add(transitive);
1035 } else {
1036 // The narrower narrower tag has already been
1037 // encountered.
1038 /* do nothing */
1039 }
1040 }
1041 }
1042 } else {
1043 // The narrower tag has already been encountered.
1044 /* do nothing */
1045 }
1046 }
1047 }
1048 }
1049
1050 /**
1051 * Persist this `Tag` to storage and return an ActivityStreams
1052 * serialization of a Tag Activity representing any changes, or
1053 * `null` if no changes were made.
1054 *
1055 * If the second argument is `true`, the `Tag` will be persisted but
1056 * no serialization will be made. This is somewhat more efficient.
1057 *
1058 * ※ Persistence can imply side‐effects on other objects, which are
1059 * not noted explicitly in the activity. For example, marking a tag
1060 * as broader than another causes the other tag to reciprocally be
1061 * marked as narrower.
1062 *
1063 * ※ The inverse terms `hasInCanon`, `isIncludedIn`, and `narrower`
1064 * will never appear in the predicates of generated activities.
1065 */
1066 persist(silent = false) {
1067 const system = this.#system;
1068 const storage = this.#storage;
1069 const persistedData = this.#persistedData;
1070 const data = this.#data;
1071 const diffs = {};
1072 for (const [key, value] of Object.entries(data)) {
1073 // Iterate over each entry of the tag data and create a diff
1074 // with the last persisted information.
1075 if (SKIP_IN_DIFF.has(key) || silent && LITERAL_TERMS.has(key)) {
1076 // The current property is one which is skipped in diffs.
1077 //
1078 // In a silent persist, this includes any literal terms.
1079 /* do nothing */
1080 } else {
1081 // The current property should be diffed.
1082 const persisted = persistedData?.[key] ?? null;
1083 if (persisted == null) {
1084 // There is no persisted data for the current property yet.
1085 diffs[key] = {
1086 old: new Set(),
1087 new: value instanceof Set
1088 ? new Set(value)
1089 : new Set([value]),
1090 };
1091 } else if (value instanceof Set) {
1092 // The current property is set‐valued.
1093 let values = null; // initialized on first use
1094 const oldValues = new Set(persisted);
1095 const newValues = new Set(value);
1096 for (const existing of persisted) {
1097 // Iterate over each persisted property and either remove
1098 // it from the list of new values or add it to the list of
1099 // removed ones.
1100 //
1101 // ※ Some special handling is required here for
1102 // language‐tagged strings.
1103 if (
1104 value.has(existing) ||
1105 Object(existing) === existing &&
1106 (values ??= [...value]).some(($) =>
1107 `${$}` == `${existing}` &&
1108 $.language == existing.language
1109 )
1110 ) {
1111 // The value is in both the old and new version of the
1112 // data.
1113 oldValues.delete(existing);
1114 newValues.delete(existing);
1115 } else {
1116 // The value is not shared.
1117 /* do nothing */
1118 }
1119 }
1120 diffs[key] = {
1121 old: oldValues,
1122 new: newValues,
1123 };
1124 } else if (
1125 `${value}` != `${persisted}` ||
1126 value.language != persisted.language
1127 ) {
1128 // The current property is (optionally language‐tagged)
1129 // string‐valued and the value changed.
1130 diffs[key] = {
1131 old: new Set([persisted]),
1132 new: new Set([value]),
1133 };
1134 } else {
1135 // The current property did not change.
1136 diffs[key] = {
1137 old: new Set(),
1138 new: new Set(),
1139 };
1140 }
1141 }
1142 }
1143 const identifier = this.#identifier;
1144 if (identifier != null) {
1145 // This `Tag` has already been persisted; use its existing
1146 // identifier and persist.
1147 storage.set(identifier, this);
1148 } else {
1149 // This `Tag` has not been persisted yet; save the new
1150 // identifier after persisting.
1151 this.#identifier = storage.add(this);
1152 }
1153 const persistedIdentifier = this.#identifier;
1154 this.#persistedData = tagData(data); // cloning here is necessary
1155 for (
1156 const [term, inverse] of [
1157 ["broader", "narrower"],
1158 ["inCanon", "hasInCanon"],
1159 ["involves", "involvedIn"],
1160 ]
1161 ) {
1162 // Iterate over each term referencing other tags and update the
1163 // inverse property on those tags if possible.
1164 for (const referencedIdentifier of diffs[term].old) {
1165 // Iterate over the removed tags and remove this `Tag` from
1166 // their inverse property.
1167 const referenced = storage.get(referencedIdentifier);
1168 try {
1169 // Try removing this `Tag`.
1170 referenced.#data[inverse].delete(persistedIdentifier);
1171 storage.set(referencedIdentifier, referenced);
1172 } catch {
1173 // Removal failed, possibly because the other tag was
1174 // deleted.
1175 /* do nothing */
1176 }
1177 }
1178 for (const referencedIdentifier of diffs[term].new) {
1179 const referenced = storage.get(referencedIdentifier);
1180 try {
1181 // Try adding this `Tag`.
1182 referenced.#data[inverse].add(persistedIdentifier);
1183 storage.set(referencedIdentifier, referenced);
1184 } catch {
1185 // Adding failed, possibly because the other tag was deleted.
1186 /* do nothing */
1187 }
1188 }
1189 }
1190 if (silent) {
1191 // This is a silent persist.
1192 return undefined;
1193 } else {
1194 // This is not a silent persist; an activity needs to be
1195 // generated if a change was made.
1196 const activity = {
1197 "@context": taggingDiscoveryContext,
1198 "@type": [
1199 "TagActivity",
1200 identifier == null ? "Create" : "Update",
1201 ],
1202 context: `${system.iri}`,
1203 object: `${this.iri}`,
1204 endTime: new Date().toISOString(),
1205 ...(() => {
1206 const statements = {
1207 unstates: [],
1208 states: [],
1209 };
1210 const { unstates, states } = statements;
1211 if (identifier == null) {
1212 // This is a Create activity.
1213 states.push({ predicate: "a", object: `${this.kind}` });
1214 } else {
1215 // This is an Update activity.
1216 /* do nothing */
1217 }
1218 for (
1219 const [term, {
1220 old: oldValues,
1221 new: newValues,
1222 }] of Object.entries(diffs)
1223 ) {
1224 // Iterate over the diffs of each term and state/unstate
1225 // things as needed.
1226 for (const oldValue of oldValues) {
1227 // Iterate over removals and unstate them.
1228 if (LITERAL_TERMS.has(term)) {
1229 // This is a literal term; push the change wrapped in an
1230 // object.
1231 unstates.push({
1232 predicate: term,
1233 object: Object(oldValue) === oldValue
1234 ? { ...langString(oldValue) }
1235 : { "@value": `${oldValue}` },
1236 });
1237 } else {
1238 // This is a named term; attempt to get its I·R·I and
1239 // push it.
1240 try {
1241 // Attempt to resolve the value and push the change.
1242 const tag = storage.get(oldValue);
1243 if (!this.#isTagInStorage(tag)) {
1244 // The value did not resolve to a tag in storage.
1245 /* do nothing */
1246 } else {
1247 // The value resolved; push its I·R·I.
1248 unstates.push({
1249 predicate: term,
1250 object: tag.iri,
1251 });
1252 }
1253 } catch {
1254 // Value resolution failed for some reason; perhaps the
1255 // tag was deleted.
1256 /* do nothing */
1257 }
1258 }
1259 }
1260 for (const newValue of newValues) {
1261 // Iterate over additions and state them.
1262 if (LITERAL_TERMS.has(term)) {
1263 // This is a literal term; push the change wrapped in an
1264 // object.
1265 states.push({
1266 predicate: term,
1267 object: Object(newValue) === newValue
1268 ? { ...langString(newValue) }
1269 : { "@value": `${newValue}` },
1270 });
1271 } else {
1272 // This is a named term; attempt to get its I·R·I and
1273 // push it.
1274 try {
1275 // Attempt to resolve the value and push the change.
1276 const tag = storage.get(newValue);
1277 if (!this.#isTagInStorage(tag)) {
1278 // The value did not resolve to a tag in storage.
1279 /* do nothing */
1280 } else {
1281 // The value resolved; push its I·R·I.
1282 states.push({
1283 predicate: term,
1284 object: tag.iri,
1285 });
1286 }
1287 } catch {
1288 // Value resolution failed for some reason; perhaps the
1289 // tag was deleted.
1290 /* do nothing */
1291 }
1292 }
1293 }
1294 }
1295 if (unstates.length == 0) {
1296 // Nothing was unstated.
1297 delete statements.unstates;
1298 } else {
1299 // Things were stated.
1300 /* do nothing */
1301 }
1302 if (states.length == 0) {
1303 // Nothing was stated.
1304 delete statements.states;
1305 } else {
1306 // Things were stated.
1307 /* do nothing */
1308 }
1309 return statements;
1310 })(),
1311 };
1312 if (
1313 !Object.hasOwn(activity, "states") &&
1314 !Object.hasOwn(activity, "unstates")
1315 ) {
1316 // No meaningful changes were actually persisted.
1317 return null;
1318 } else {
1319 // There were meaningful changes persisted regarding this `Tag`.
1320 return activity;
1321 }
1322 }
1323 }
1324
1325 /** Returns the preferred label for this `Tag`. */
1326 get prefLabel() {
1327 return this.#data.prefLabel;
1328 }
1329
1330 /** Sets the preferred label of this `Tag` to the provided label. */
1331 set prefLabel($) {
1332 this.#data.prefLabel = langString($);
1333 }
1334
1335 /** Returns the Tag U·R·I for this `Tag`. */
1336 get tagURI() {
1337 const { identifier } = this;
1338 return identifier == null
1339 ? null
1340 : `tag:${this.taggingEntity}:${identifier}`;
1341 }
1342
1343 /** Returns the tagging entity (domain and date) for this `Tag`. */
1344 get taggingEntity() {
1345 return this.#system.taggingEntity;
1346 }
1347
1348 /** Returns the string form of the preferred label of this `Tag`. */
1349 toString() {
1350 return `${this.#data.prefLabel}`;
1351 }
1352
1353 /**
1354 * Returns a new object whose enumerable own properties contain the
1355 * data from this object needed for storage.
1356 *
1357 * ※ This method is not really intended for public usage.
1358 */
1359 [Storage.toObject]() {
1360 const data = this.#data;
1361 return Object.assign(Object.create(null), {
1362 ...data,
1363 kind: this.#kind,
1364 });
1365 }
1366 }
1367
1368 const {
1369 /**
1370 * Returns the provided value converted into either a plain string
1371 * primitive or an object with `.["@value"]` and `.["@language"]`
1372 * properties.
1373 *
1374 * TODO: Ideally this would be extracted more fully into an R·D·F
1375 * library.
1376 *
1377 * ※ This function is not exposed.
1378 */
1379 langString,
1380 } = (() => {
1381 /** Returns the `.["@language"]` of this object. */
1382 const getLanguage = Object.defineProperty(
1383 function () {
1384 return this["@language"];
1385 },
1386 "name",
1387 { value: "get language" },
1388 );
1389
1390 /** Returns the `.["@value"]` of this object. */
1391 const toString = function () {
1392 return this["@value"];
1393 };
1394
1395 /** Returns the `.["@value"]` of this object. */
1396 const valueOf = function () {
1397 return this["@value"];
1398 };
1399
1400 return {
1401 langString: ($) =>
1402 Object($) === $
1403 ? "@value" in $
1404 ? "@language" in $
1405 ? Object.preventExtensions(
1406 Object.create(String.prototype, {
1407 "@value": {
1408 enumerable: true,
1409 value: `${$["@value"]}`,
1410 },
1411 "@language": {
1412 enumerable: true,
1413 value: `${$["@language"]}`,
1414 },
1415 language: { enumerable: false, get: getLanguage },
1416 toString: { enumerable: false, value: toString },
1417 valueOf: { enumerable: false, value: valueOf },
1418 }),
1419 )
1420 : `${$["@value"]}`
1421 : "language" in $
1422 ? Object.preventExtensions(
1423 Object.create(String.prototype, {
1424 "@value": { enumerable: true, value: `${$}` },
1425 "@language": {
1426 enumerable: true,
1427 value: `${$.language}`,
1428 },
1429 language: { enumerable: false, get: getLanguage },
1430 toString: { enumerable: false, value: toString },
1431 valueOf: { enumerable: false, value: valueOf },
1432 }),
1433 )
1434 : `${$}`
1435 : `${$ ?? ""}`,
1436 };
1437 })();
1438
1439 /**
1440 * Returns a normalized tag data object derived from the provided
1441 * object.
1442 *
1443 * ※ The properties of this function need to match the term names used
1444 * in the ActivityStreams serialization.
1445 *
1446 * ※ This function is not exposed.
1447 */
1448 const tagData = ($) => {
1449 const data = Object($);
1450 const {
1451 // prefLabel intentionally not set here
1452 altLabel,
1453 hiddenLabel,
1454 broader,
1455 narrower,
1456 inCanon,
1457 hasInCanon,
1458 involves,
1459 involvedIn,
1460 } = data;
1461 let prefLabel = langString(data.prefLabel);
1462 return Object.preventExtensions(Object.create(null, {
1463 prefLabel: {
1464 enumerable: true,
1465 get: () => prefLabel,
1466 set: ($) => {
1467 prefLabel = langString($);
1468 },
1469 },
1470 altLabel: {
1471 enumerable: true,
1472 value: new Set(
1473 altLabel != null
1474 ? Array.from(altLabel, langString)
1475 : undefined,
1476 ),
1477 },
1478 hiddenLabel: {
1479 enumerable: true,
1480 value: new Set(
1481 hiddenLabel != null
1482 ? Array.from(hiddenLabel, langString)
1483 : undefined,
1484 ),
1485 },
1486 broader: {
1487 enumerable: true,
1488 value: new Set(
1489 broader != null
1490 ? Array.from(broader, toIdentifier)
1491 : undefined,
1492 ),
1493 },
1494 narrower: {
1495 enumerable: true,
1496 value: new Set(
1497 narrower != null
1498 ? Array.from(narrower, toIdentifier)
1499 : undefined,
1500 ),
1501 },
1502 inCanon: {
1503 enumerable: true,
1504 value: new Set(
1505 inCanon != null
1506 ? Array.from(inCanon, toIdentifier)
1507 : undefined,
1508 ),
1509 },
1510 hasInCanon: {
1511 enumerable: true,
1512 value: new Set(
1513 hasInCanon != null
1514 ? Array.from(hasInCanon, toIdentifier)
1515 : undefined,
1516 ),
1517 },
1518 involves: {
1519 enumerable: true,
1520 value: new Set(
1521 involves != null
1522 ? Array.from(involves, toIdentifier)
1523 : undefined,
1524 ),
1525 },
1526 involvedIn: {
1527 enumerable: true,
1528 value: new Set(
1529 involvedIn != null
1530 ? Array.from(involvedIn, toIdentifier)
1531 : undefined,
1532 ),
1533 },
1534 }));
1535 };
1536
1537 /**
1538 * Returns an identifier corresponding to the provided object.
1539 *
1540 * This is either the value of its `.identifier` or its string value.
1541 *
1542 * ※ This function is not exposed.
1543 */
1544 const toIdentifier = ($) =>
1545 $ == null
1546 ? null
1547 : Object($) === $ && "identifier" in $
1548 ? $.identifier
1549 : `${$}`;
1550
1551 /**
1552 * A tag system, with storage.
1553 *
1554 * The `::Tag` constructor available on any `TagSystem` instance can be
1555 * used to create new `Tag`s within the system.
1556 */
1557 export class TagSystem {
1558 /** The cached bound `Tag` constructor for this `TagSystem`. */
1559 #Tag = null;
1560
1561 /** The domain of this `TagSystem`. */
1562 #domain;
1563
1564 /** The date of this `TagSystem`. */
1565 #date;
1566
1567 /** The identifier of this `TagSystem`. */
1568 #identifier;
1569
1570 /** The internal `Storage` of this `TagSystem`. */
1571 #storage = new Storage();
1572
1573 /**
1574 * Constructs a new `TagSystem` with the provided domain and date.
1575 *
1576 * Only actual, lowercased domain names are allowed for the domain,
1577 * and the date must be “full” (include month and day components).
1578 * This is for alignment with general best practices for Tag URI’s.
1579 *
1580 * ☡ This constructor throws if provided with an invalid date.
1581 */
1582 constructor(domain, date, identifier = "") {
1583 const domainString = `${domain}`;
1584 const dateString = `${date}`;
1585 this.#identifier = `${identifier}`;
1586 try {
1587 // If the identifier is a valid storage I·D, reserve it.
1588 this.#storage.delete(this.#identifier);
1589 } catch {
1590 // The identifier is not a valid storage I·D, so no worries.
1591 /* do nothing */
1592 }
1593 if (
1594 !/^[a-z](?:[\da-z-]*[\da-z])?(?:\.[a-z](?:[\da-z-]*[\da-z])?)*$/u
1595 .test(domainString)
1596 ) {
1597 // ☡ The domain is invalid.
1598 throw new RangeError(`Invalid domain: ${domain}.`);
1599 } else if (
1600 !/^\d{4}-\d{2}-\d{2}$/u.test(dateString) ||
1601 dateString != new Date(dateString).toISOString().split("T")[0]
1602 ) {
1603 // ☡ The date is invalid.
1604 throw new RangeError(`Invalid date: ${date}.`);
1605 } else {
1606 // The domain and date are 🆗.
1607 this.#domain = domainString;
1608 this.#date = dateString;
1609 }
1610 }
1611
1612 /**
1613 * Returns a bound constructor for constructing `Tags` in this
1614 * `TagSystem`.
1615 */
1616 get Tag() {
1617 if (this.#Tag != null) {
1618 // A bound constructor has already been generated; return it.
1619 return this.#Tag;
1620 } else {
1621 // No bound constructor has been created yet.
1622 const storage = this.#storage;
1623 const BoundTag = Tag.bind(undefined, this, storage);
1624 return this.#Tag = Object.defineProperties(BoundTag, {
1625 all: {
1626 configurable: true,
1627 enumerable: false,
1628 value: Tag.all.bind(BoundTag, this, storage),
1629 writable: true,
1630 },
1631 fromIRI: {
1632 configurable: true,
1633 enumerable: false,
1634 value: Tag.fromIRI.bind(BoundTag, this, storage),
1635 writable: true,
1636 },
1637 fromIdentifier: {
1638 configurable: true,
1639 enumerable: false,
1640 value: Tag.fromIdentifier.bind(BoundTag, this, storage),
1641 writable: true,
1642 },
1643 fromTagURI: {
1644 configurable: true,
1645 enumerable: false,
1646 value: Tag.fromTagURI.bind(BoundTag, this, storage),
1647 writable: true,
1648 },
1649 identifiers: {
1650 configurable: true,
1651 enumerable: false,
1652 value: Tag.identifiers.bind(BoundTag, this, storage),
1653 writable: true,
1654 },
1655 name: { value: `${this.tagURI}#${Tag.name}` },
1656 prototype: { value: Tag.prototype },
1657 [Storage.toInstance]: {
1658 configurable: true,
1659 enumerable: false,
1660 value: Tag[Storage.toInstance].bind(BoundTag, this, storage),
1661 writable: true,
1662 },
1663 });
1664 }
1665 }
1666
1667 /** Returns the authority name (domain) for this `TagSystem`. */
1668 get authorityName() {
1669 return this.#domain;
1670 }
1671
1672 /** Returns the date of this `TagSystem`, as a string. */
1673 get date() {
1674 return this.#date;
1675 }
1676
1677 /**
1678 * Yields the entities in this `TagSystem`.
1679 *
1680 * ※ Entities can hypothetically be anything. If you specifically
1681 * want the `Tag`s, use `::Tag.all` instead.
1682 */
1683 *entities() {
1684 yield* this.#storage.values();
1685 }
1686
1687 /**
1688 * Returns the identifier of this `TagSystem`.
1689 *
1690 * ※ Often this is just the empty string.
1691 */
1692 get identifier() {
1693 return this.#identifier;
1694 }
1695
1696 /** Yields the identifiers in use in this `TagSystem`. */
1697 *identifiers() {
1698 yield* this.#storage.keys();
1699 }
1700
1701 /** Returns the I·R·I for this `TagSystem`. */
1702 get iri() {
1703 return `https://${this.authorityName}/${this.tagURI}`;
1704 }
1705
1706 /** Returns the Tag U·R·I for this `TagSystem`. */
1707 get tagURI() {
1708 return `tag:${this.taggingEntity}:${this.identifier}`;
1709 }
1710
1711 /**
1712 * Returns the tagging entity (domain and date) for this `TagSystem`.
1713 */
1714 get taggingEntity() {
1715 return `${this.authorityName},${this.date}`;
1716 }
1717 }
This page took 0.391289 seconds and 3 git commands to generate.