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