1 // 📧🏷️ Étiquette ∷ model.js
2 // ====================================================================
4 // Copyright © 2023 Lady [@ Lady’s Computer].
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/>.
10 import { Storage
} from "./memory.js";
11 import { taggingDiscoveryContext
} from "./names.js";
12 import schema
from "./schema.js";
17 * `Tag`s are not assigned identifiers and do not have side·effects on
18 * other tags in the `TagSystem` until they are persisted with
19 * `::persist`, at which point changes to their relationships are
22 * `Tag`s are also not kept up‐to‐date, but persisting an outdated
23 * `Tag` will *not* undo subsequent changes.
25 * ※ This class is not itself directly exposed, although bound
26 * versions of it are via `TagSystem::Tag`.
29 /** The `TagSystem` this `Tag` belongs to. */
32 /** The `Storage` managed by this `Tag`’s `TagSystem`. */
35 /** The schema in use for this `Tag`. */
39 * The 30‐bit W·R·M·G base32 identifier with leading checksum which
40 * has been assigned to this `Tag`.
42 * Will be `null` if this `Tag` has not been persisted. Otherwise,
43 * the format is `cxx-xxxx` (`c` = checksum; `x` = digit).
47 /** The kind of this `Tag`. */
51 * The data which was attached to this `Tag` the last time it was
52 * persisted or retrieved from storage.
54 * Diffing with this will reveal changes.
56 #persistedData
= null;
58 /** The current (modified) data associated with this `Tag`. */
62 * Adds the provided label(s) to this `Tag` as the provided
63 * predicate, then returns this `Tag`.
65 #addLabel(predicate
, ...labels
) {
66 const values
= this.#data
[predicate
];
67 for (const $ of labels
) {
68 // Iterate over each provided label and attempt to add it.
69 const literal
= langString($);
76 * Adds the provided tags to the list of tags that this `Tag` is
77 * related to by the provided predicate, then returns this `Tag`.
79 * Arguments may be string identifiers or objects with an
80 * `.identifier` property.
82 #addTag(predicate
, ...tags
) {
83 const storage
= this.#storage
;
84 const values
= this.#data
[predicate
];
85 for (const $ of tags
) {
86 // Iterate over each tag and attempt to state the predicate.
87 const identifier
= toIdentifier($);
88 if (identifier
== null) {
89 // ☡ The current tag has no identifier.
91 `Cannot state ${predicate} of Tag: Identifier must not be nullish.`,
93 } else if (values
.has(identifier
)) {
94 // Short‐circuit: The identifier has already been stated with
98 // The current tag has an identifier, but it hasn’t been stated
99 // with this predicate yet.
100 const tag
= storage
.get(identifier
);
102 // ☡ The current tag has not been persisted to this `Tag`’s
104 throw new RangeError(
105 `Cannot state ${predicate} of Tag: Identifier is not persisted: ${identifier}.`,
107 } else if (!this.#isTagInStorage(tag
)) {
108 // ☡ The current tag is not a tag in the correct tag system.
110 `Cannot state ${predicate} of Tag: Tags must be from the same Tag System, but got: ${identifier}.`,
113 !isObjectPredicateOK(
120 // ☡ This tag and the current tag form an invalid pair for
123 `Cannot state ${predicate} of Tag: Not valid for domain and range: ${this.#kind}, ${tag.#kind}.`,
126 // The current tag is a tag in the correct tag system; add
128 values
.add(identifier
);
136 * Removes the provided string label(s) from this `Tag` as the
137 * provided predicate, then returns this `Tag`.
139 #deleteLabel(predicate
, ...labels
) {
140 const values
= this.#data
[predicate
];
141 for (const $ of labels
) {
142 // Iterate over each provided label and attempt to remove it.
143 const literal
= langString($);
144 values
.delete(literal
);
150 * Removes the provided tags from the list of tags that this `Tag` is
151 * related to by the provided predicate, then returns this `Tag`.
153 * Arguments may be string identifiers or objects with an
154 * `.identifier` property.
156 #deleteTag(predicate
, ...tags
) {
157 const values
= this.#data
[predicate
];
158 for (const $ of tags
) {
159 // Iterate over the provided tags and delete them.
160 values
.delete(toIdentifier($));
166 * Returns whether or not the provided value is a tag which shares a
167 * storage with this tag.
169 * Sharing a storage also implies sharing a `TagSystem`.
173 // Try to compare the provided value’s internal store with
174 // the provided storage.
175 return $.#storage
== this.#storage
;
177 // The provided value was not a `Tag`.
183 * Yields the labels of this `Tag` according to the provided
186 *#yieldLabels(predicate
) {
187 yield* this.#data
[predicate
];
191 * Yields the tags that this `Tag` is related to by the provided
194 *#yieldTags(predicate
) {
195 const storage
= this.#storage
;
196 for (const identifier
of this.#data
[predicate
]) {
197 // Iterate over the tags in this predicate and yield them if
199 const tag
= storage
.get(identifier
);
201 !this.#isTagInStorage(tag
) || !isObjectPredicateOK(
208 // The tag no longer appears in storage or is not compatible;
209 // perhaps it was deleted.
212 // The tag exists and is constructable from storage.
219 * Yields the tags that this `Tag` is related to by the provided
220 * predicate, figured transitively.
222 *#yieldTransitiveTags(transitivePredicate
, basePredicate
) {
223 const storage
= this.#storage
;
224 const encountered
= new Set();
225 let pending
= new Set(this.#data
[basePredicate
]);
226 while (pending
.size
> 0) {
227 // Loop until all tags of the predicate have been encountered.
228 const processing
= pending
;
230 for (const identifier
of processing
) {
231 // Iterate over the tags and yield them if possible.
232 if (!encountered
.has(identifier
)) {
233 // The tag has not been encountered before.
234 encountered
.add(identifier
);
235 const tag
= storage
.get(identifier
);
237 !this.#isTagInStorage(tag
) || !isObjectPredicateOK(
244 // The tag no longer appears in storage or is not
245 // compatible; perhaps it was deleted.
248 // The tag exists and is constructable from storage.
250 for (const transitive
of tag
.#data
[basePredicate
]) {
251 // Iterate over the nested tags of the current tag and
252 // add them to pending as needed.
253 if (!encountered
.has(transitive
)) {
254 // The nested tag has not been encountered yet.
255 pending
.add(transitive
);
257 // The nested tag has already been encountered.
263 // The tag has already been encountered.
271 * Constructs a new `Tag` of the provided kind and with the provided
274 * ※ The first two arguments of this constructor are bound when
275 * generating the value of `TagSystem::Tag`. It isn’t possible to
276 * access this constructor in its unbound form from outside this
279 * ☡ This constructor throws if the provided kind is not supported.
281 constructor(system
, storage
, schema
, kind
= "Tag", prefLabel
= "") {
282 this.#system
= system
;
283 this.#storage
= storage
;
284 this.#schema
= schema
;
285 const kindString
= `${kind}`;
286 if (!(kindString
in schema
.classes
)) {
287 // The provided kind is not supported.
288 throw new RangeError(
289 `Cannot construct Tag: Unrecognized kind: ${kind}.`,
292 // The provided kind is one of the recognized tag kinds.
293 this.#kind
= kindString
;
294 this.#data
.prefLabel
= prefLabel
;
299 * Returns a new `Tag` constructor for the provided system, storage,
300 * schema, created with an appropriate prototype for the properties
303 * ※ This function is not exposed.
305 static For(system
, storage
, schema
) {
308 transitiveProperties
,
311 const constructor = function (...$s
) {
312 return Reflect
.construct(
314 [system
, storage
, schema
, ...$s
],
318 Object
.defineProperties(constructor, {
322 value
: Object
.create(
324 Object
.fromEntries(Array
.from(
326 for (const key
in objectProperties
) {
330 } = objectProperties
[key
];
331 if (key
in transitiveProperties
) {
332 // Transitive property methods are added by their
333 // nontransitive subproperties.
336 yield [`${key}Tags`, function* () {
337 yield* this.#yieldTags(key
);
339 if (inverseOf
== null) {
340 const cased
= key
[0].toUpperCase() +
342 yield [`add${cased}Tag`, function (...tags
) {
343 return this.#addTag(key
, ...tags
);
345 yield [`delete${cased}Tag`, function (...tags
) {
346 return this.#deleteTag(key
, ...tags
);
352 subPropertyOf
!= null &&
353 subPropertyOf
in transitiveProperties
355 yield [`${subPropertyOf}Tags`, function* () {
356 yield* this.#yieldTransitiveTags(
366 for (const key
in dataProperties
) {
367 if (key
!= "prefLabel") {
368 const cased
= key
[0].toUpperCase() +
370 yield [`${key}s`, function* () {
371 yield* this.#yieldLabels(key
);
373 yield [`add${cased}`, function (...labels
) {
374 return this.#addLabel(key
, ...labels
);
376 yield [`delete${cased}`, function (...labels
) {
377 return this.#deleteLabel(key
, ...labels
);
384 ([key
, value
]) => [key
, {
387 value
: Object
.defineProperty(value
, "name", {
397 return Object
.defineProperties(
400 ["name", { value
: "TagSystem::Tag" }],
408 ].map((key
) => [key
, {
411 value
: Object
.defineProperty(
412 Tag
[key
].bind(constructor, system
, storage
),
414 { value
: String(key
) },
423 * Yields the tags in the `TagSystem` associated with this
426 * ※ The first two arguments of this function are bound when
427 * generating the value of `TagSystem::Tag`. It isn’t possible to
428 * access this function in its unbound form from outside this module.
430 static *all(system
, storage
) {
431 for (const instance
of storage
.values()) {
432 // Iterate over the entries and yield the ones which are `Tag`s
433 // in this `TagSystem`.
434 if (Tag
.getSystem(instance
) == system
) {
435 // The current instance is a `Tag` in this `TagSystem`.
438 // The current instance is not a `Tag` in this `TagSystem`.
445 * Returns a new `Tag` resolved from the provided I·R·I.
447 * ※ The first two arguments of this function are bound when
448 * generating the value of `TagSystem::Tag`. It isn’t possible to
449 * access this function in its unbound form from outside this module.
451 * ☡ This function throws if the I·R·I is not in the `.iriSpace` of
452 * the `TagSystem` associated with this constructor.
454 * ※ If the I·R·I is not recognized, this function returns `null`.
456 static fromIRI(system
, storage
, iri
) {
457 const name
= `${iri}`;
458 const prefix
= `${system.iriSpace}`;
459 if (!name
.startsWith(prefix
)) {
460 // The I·R·I does not begin with the expected prefix.
461 throw new RangeError(
462 `I·R·I did not begin with the expected prefix: ${iri}`,
465 // The I·R·I begins with the expected prefix.
466 const identifier
= name
.substring(prefix
.length
);
468 // Attempt to resolve the identifier.
469 const instance
= storage
.get(identifier
);
470 return Tag
.getSystem(instance
) == system
? instance
: null;
472 // Do not throw for bad identifiers.
479 * Returns a new `Tag` resolved from the provided identifier.
481 * ※ The first two arguments of this function are bound when
482 * generating the value of `TagSystem::Tag`. It isn’t possible to
483 * access this function in its unbound form from outside this module.
485 * ☡ This function throws if the identifier is invalid.
487 * ※ If the identifier is valid but not recognized, this function
490 static fromIdentifier(system
, storage
, identifier
) {
491 const instance
= storage
.get(identifier
);
492 return Tag
.getSystem(instance
) == system
? instance
: null;
496 * Returns a new `Tag` resolved from the provided Tag U·R·I.
498 * ※ The first two arguments of this function are bound when
499 * generating the value of `TagSystem::Tag`. It isn’t possible to
500 * access this function in its unbound form from outside this module.
502 * ☡ This function throws if the provided Tag U·R·I does not match
503 * the tagging entity of this constructor’s `TagSystem`.
505 * ※ If the specific component of the Tag U·R·I is not recognized,
506 * this function returns `null`.
508 static fromTagURI(system
, storage
, tagURI
) {
509 const tagName
= `${tagURI}`;
510 const tagPrefix
= `tag:${system.taggingEntity}:`;
511 if (!tagName
.startsWith(tagPrefix
)) {
512 // The Tag U·R·I does not begin with the expected prefix.
513 throw new RangeError(
514 `Tag U·R·I did not begin with the expected prefix: ${tagURI}`,
517 // The I·R·I begins with the expected prefix.
518 const identifier
= tagName
.substring(tagPrefix
.length
);
520 // Attempt to resolve the identifier.
521 const instance
= storage
.get(identifier
);
522 return Tag
.getSystem(instance
) == system
? instance
: null;
524 // Do not throw for bad identifiers.
531 * Returns the `TagSystem` that the provided value belongs to.
533 * ※ This function can be used to check if the provided value has
534 * private tag features.
536 * ※ This function is not exposed.
538 static getSystem($) {
539 return !(#system
in Object($)) ? null : $.#system
;
543 * Yields the tag identifiers in the `TagSystem` associated with this
546 * ※ The first two arguments of this function are bound when
547 * generating the value of `TagSystem::Tag`. It isn’t possible to
548 * access this function in its unbound form from outside this module.
550 static *identifiers(system
, storage
) {
551 for (const [identifier
, instance
] of storage
.entries()) {
552 // Iterate over the entries and yield the ones which are `Tag`s
553 // in this `TagSystem`.
554 if (Tag
.getSystem(instance
) == system
) {
555 // The current instance is a `Tag` in this `TagSystem`.
558 // The current instance is not a `Tag` in this `TagSystem`.
565 * Returns a new `Tag` constructed from the provided data and with
566 * the provided identifier.
568 * ※ This function will not work if called directly from `Tag` (and
569 * nor is it available *to* be called as such from outside this
570 * module). It must be called from a `TagSystem::Tag` bound
573 * ※ This function is not really intended for public usage.
575 static [Storage
.toInstance
](_system
, _storage
, data
, identifier
) {
576 const tag
= new this(data
.kind
);
577 tag
.#identifier
= `${identifier}`;
578 tag
.#persistedData
= tagData(data
);
579 tag
.#data
= tagData(data
);
584 // Overwrite the default `::constructor` method to instead give the
585 // actual (bound) constructor which was used to generate a given
587 Object
.defineProperties(this.prototype, {
592 // All `Tag`s are constructed via the `.Tag` constructor
593 // available in their `TagSystem`; return it.
594 return this.#system
.Tag
;
601 /** Returns the authority (domain) name for this `Tag`. */
602 get authorityName() {
603 return this.#system
.authorityName
;
606 /** Returns the identifier of this `Tag`. */
608 return this.#identifier
;
611 /** Returns the I·R·I for this `Tag`. */
613 const { identifier
, iriSpace
} = this;
614 return identifier
== null ? null : `${iriSpace}${identifier}`;
617 /** Returns the I·R·I space for this `Tag`. */
619 return this.#system
.iriSpace
;
622 /** Returns the kind of this `Tag`. */
628 * Persist this `Tag` to storage and return an ActivityStreams
629 * serialization of a Tag Activity representing any changes, or
630 * `null` if no changes were made.
632 * If the second argument is `true`, the `Tag` will be persisted but
633 * no serialization will be made. This is somewhat more efficient.
635 * ※ Persistence can imply side‐effects on other objects, which are
636 * not noted explicitly in the activity. For example, marking a tag
637 * as broader than another causes the other tag to reciprocally be
638 * marked as narrower.
640 * ※ Inverse object properties will never appear in the predicates
641 * of generated activities.
643 persist(silent
= false) {
644 const system
= this.#system
;
645 const storage
= this.#storage
;
648 transitiveProperties
,
651 const persistedData
= this.#persistedData
;
652 const data
= this.#data
;
654 for (const [key
, value
] of Object
.entries(data
)) {
655 // Iterate over each entry of the tag data and create a diff
656 // with the last persisted information.
658 objectProperties
[key
]?.inverseOf
!= null ||
659 silent
&& key
in dataProperties
661 // The current property is one which is skipped in diffs.
663 // In a silent persist, this includes any literal terms.
666 // The current property should be diffed.
667 const persisted
= persistedData
?.[key
] ?? null;
668 if (persisted
== null) {
669 // There is no persisted data for the current property yet.
672 new: value
instanceof Set
676 } else if (value
instanceof Set
) {
677 // The current property is set‐valued.
678 const oldValues
= new Set(persisted
);
679 const newValues
= new Set(value
);
680 for (const existing
of persisted
) {
681 // Iterate over each persisted property and either remove
682 // it from the list of new values or add it to the list of
684 if (value
.has(existing
)) {
685 // The value is in both the old and new version of the
687 oldValues
.delete(existing
);
688 newValues
.delete(existing
);
690 // The value is not shared.
694 diffs
[key
] = { old
: oldValues
, new: newValues
};
696 `${value}` != `${persisted}` ||
697 value
.language
!= persisted
.language
699 // The current property is (optionally language‐tagged)
700 // string‐valued and the value changed.
702 old
: new Set([persisted
]),
703 new: new Set([value
]),
706 // The current property did not change.
707 diffs
[key
] = { old
: new Set(), new: new Set() };
711 const identifier
= this.#identifier
;
712 if (identifier
!= null) {
713 // This `Tag` has already been persisted; use its existing
714 // identifier and persist.
715 storage
.set(identifier
, this);
717 // This `Tag` has not been persisted yet; save the new
718 // identifier after persisting.
719 this.#identifier
= storage
.add(this);
721 const persistedIdentifier
= this.#identifier
;
722 this.#persistedData
= tagData(data
); // cloning here is necessary
723 for (const inverse
in objectProperties
) {
724 // Iterate over each non‐transitive inverse property and update
725 // it based on its inverse on the corresponding tags if possible.
726 const term
= objectProperties
[inverse
].inverseOf
;
727 if (term
== null || term
in transitiveProperties
) {
728 // The current property is not the inverse of an non‐transitive
732 // The current property is the inverse of a non‐transitive
734 for (const referencedIdentifier
of diffs
[term
].old
) {
735 // Iterate over the removed tags and remove this `Tag` from
736 // their inverse property.
737 const referenced
= storage
.get(referencedIdentifier
);
739 // Try removing this `Tag`.
740 referenced
.#data
[inverse
].delete(persistedIdentifier
);
741 storage
.set(referencedIdentifier
, referenced
);
743 // Removal failed, possibly because the other tag was
748 for (const referencedIdentifier
of diffs
[term
].new) {
749 const referenced
= storage
.get(referencedIdentifier
);
751 // Try adding this `Tag`.
752 referenced
.#data
[inverse
].add(persistedIdentifier
);
753 storage
.set(referencedIdentifier
, referenced
);
755 // Adding failed, possibly because the other tag was deleted.
762 // This is a silent persist.
765 // This is not a silent persist; an activity needs to be
766 // generated if a change was made.
768 "@context": taggingDiscoveryContext
,
771 identifier
== null ? "Create" : "Update",
773 context
: `${system.iri}`,
774 object
: `${this.iri}`,
775 endTime
: new Date().toISOString(),
781 const { unstates
, states
} = statements
;
782 if (identifier
== null) {
783 // This is a Create activity.
784 states
.push({ predicate
: "a", object
: `${this.kind}` });
786 // This is an Update activity.
793 }] of Object
.entries(diffs
)
795 // Iterate over the diffs of each term and state/unstate
797 for (const oldValue
of oldValues
) {
798 // Iterate over removals and unstate them.
799 if (term
in dataProperties
) {
800 // This is a literal term; push it.
803 object
: { ...oldValue
},
806 // This is a named term; attempt to get its I·R·I and
809 // Attempt to resolve the value and push the change.
810 const tag
= storage
.get(oldValue
);
811 if (!this.#isTagInStorage(tag
)) {
812 // The value did not resolve to a tag in storage.
815 // The value resolved; push its I·R·I.
822 // Value resolution failed for some reason; perhaps the
828 for (const newValue
of newValues
) {
829 // Iterate over additions and state them.
830 if (term
in dataProperties
) {
831 // This is a literal term; push it.
834 object
: { ...newValue
},
837 // This is a named term; attempt to get its I·R·I and
840 // Attempt to resolve the value and push the change.
841 const tag
= storage
.get(newValue
);
842 if (!this.#isTagInStorage(tag
)) {
843 // The value did not resolve to a tag in storage.
846 // The value resolved; push its I·R·I.
853 // Value resolution failed for some reason; perhaps the
860 if (unstates
.length
== 0) {
861 // Nothing was unstated.
862 delete statements
.unstates
;
864 // Things were stated.
867 if (states
.length
== 0) {
868 // Nothing was stated.
869 delete statements
.states
;
871 // Things were stated.
878 !Object
.hasOwn(activity
, "states") &&
879 !Object
.hasOwn(activity
, "unstates")
881 // No meaningful changes were actually persisted.
884 // There were meaningful changes persisted regarding this `Tag`.
890 /** Returns the preferred label for this `Tag`. */
892 return this.#data
.prefLabel
;
895 /** Sets the preferred label of this `Tag` to the provided label. */
897 this.#data
.prefLabel
= langString($);
900 /** Returns the Tag U·R·I for this `Tag`. */
902 const { identifier
} = this;
903 return identifier
== null
905 : `tag:${this.taggingEntity}:${identifier}`;
908 /** Returns the tagging entity (domain and date) for this `Tag`. */
909 get taggingEntity() {
910 return this.#system
.taggingEntity
;
913 /** Returns the string form of the preferred label of this `Tag`. */
915 return `${this.#data.prefLabel}`;
919 * Returns a new object whose enumerable own properties contain the
920 * data from this object needed for storage.
922 * ※ This method is not really intended for public usage.
924 [Storage
.toObject
]() {
925 const data
= this.#data
;
926 return Object
.assign(Object
.create(null), {
935 * Returns whether the provided schema, subject class, object
936 * property, and object class are consistent.
938 * This is hardly a full reasoner; it is tuned to the abilites and
939 * needs of this module.
943 const cachedClassAndSuperclasses
= new WeakMap();
944 const cachedClassRestrictions
= new WeakMap();
945 const cachedPredicateRestrictions
= new WeakMap();
947 const classAndSuperclasses
= function* (
952 if (baseClass
== "Thing" || touched
.has(baseClass
)) {
956 touched
.add(baseClass
);
957 const subClassOf
= classes
[baseClass
]?.subClassOf
?? "Thing";
959 const superclass
of (
960 typeof subClassOf
== "string"
962 : Array
.from(subClassOf
)
963 ).filter(($) => typeof $ == "string")
965 yield* classAndSuperclasses(classes
, superclass
, touched
);
970 const getClassAndSuperclasses
= (schema
, baseClass
) => {
971 const schemaCache
= cachedClassAndSuperclasses
.get(schema
);
972 const cached
= schemaCache
?.[baseClass
];
973 if (cached
!= null) {
976 const { classes
} = schema
;
977 const result
= [...classAndSuperclasses(classes
, baseClass
)];
979 schemaCache
[baseClass
] = result
;
981 cachedClassRestrictions
.set(
983 Object
.assign(Object
.create(null), { [baseClass
]: result
}),
990 const getClassRestrictions
= (schema
, domain
) => {
991 const schemaCache
= cachedClassRestrictions
.get(schema
);
992 const cached
= schemaCache
?.[domain
];
993 if (cached
!= null) {
996 const { classes
} = schema
;
997 const restrictions
= Object
.create(null);
998 const subClassOf
= classes
[domain
]?.subClassOf
?? "Thing";
1000 const superclass
of (
1001 typeof subClassOf
== "string"
1003 : Array
.from(subClassOf
)
1004 ).filter(($) => Object($) === $)
1006 const { onProperty
, allValuesFrom
} = superclass
;
1007 restrictions
[onProperty
] = processSpace(allValuesFrom
);
1010 schemaCache
[domain
] = restrictions
;
1012 cachedClassRestrictions
.set(
1014 Object
.assign(Object
.create(null), {
1015 [domain
]: restrictions
,
1019 return restrictions
;
1023 const getPredicateRestrictions
= (schema
, predicate
) => {
1024 const schemaCache
= cachedPredicateRestrictions
.get(schema
);
1025 const cached
= schemaCache
?.[predicate
];
1026 if (cached
!= null) {
1029 const { objectProperties
} = schema
;
1030 const restrictions
= [
1031 ...predicateRestrictions(objectProperties
, predicate
),
1033 (result
, { domainIntersection
, rangeIntersection
}) => {
1034 result
.domainIntersection
.push(...domainIntersection
);
1035 result
.rangeIntersection
.push(...rangeIntersection
);
1038 Object
.assign(Object
.create(null), {
1039 domainIntersection
: [],
1040 rangeIntersection
: [],
1044 schemaCache
[predicate
] = restrictions
;
1046 cachedPredicateRestrictions
.set(
1048 Object
.assign(Object
.create(null), {
1049 [predicate
]: restrictions
,
1053 return restrictions
;
1057 const processSpace
= (space
) =>
1058 Object(space
) === space
1063 Object(subspace
) === subspace
1064 ? Array
.from(subspace
.unionOf
)
1067 : [Array
.from(space
.unionOf
)]
1070 const predicateRestrictions
= function* (
1073 touched
= new Set(),
1075 if (predicate
== "Property" || touched
.has(predicate
)) {
1078 const { domain
, range
, subPropertyOf
} =
1079 objectProperties
[predicate
];
1080 yield Object
.assign(Object
.create(null), {
1081 domainIntersection
: processSpace(domain
?? "Thing"),
1082 rangeIntersection
: processSpace(range
?? "Thing"),
1084 touched
.add(predicate
);
1086 const superproperty
of (
1087 subPropertyOf
== null
1089 : typeof subPropertyOf
== "string"
1091 : Array
.from(subPropertyOf
)
1094 yield* predicateRestrictions(
1104 isObjectPredicateOK
: (
1110 const { objectProperties
} = schema
;
1111 const predicateDefinition
= objectProperties
[predicate
];
1112 const isInverse
= "inverseOf" in predicateDefinition
;
1113 const usedPredicate
= isInverse
1114 ? predicateDefinition
.inverseOf
1116 const domain
= isInverse
? objectClass
: subjectClass
;
1117 const domains
= new Set(getClassAndSuperclasses(schema
, domain
));
1118 const ranges
= new Set(getClassAndSuperclasses(
1120 isInverse
? subjectClass
: objectClass
,
1122 const predicateRestrictions
= getPredicateRestrictions(
1126 const { domainIntersection
} = predicateRestrictions
;
1127 const rangeIntersection
= [
1128 ...predicateRestrictions
.rangeIntersection
,
1130 for (const domain
of domains
) {
1131 const classRestrictionOnPredicate
=
1132 getClassRestrictions(schema
, domain
)[usedPredicate
];
1133 if (classRestrictionOnPredicate
!= null) {
1134 yield* classRestrictionOnPredicate
;
1141 return domainIntersection
.every((domainUnion
) =>
1142 domainUnion
.some((domain
) =>
1143 domain
== "Thing" || domains
.has(domain
)
1146 rangeIntersection
.every((rangeUnion
) =>
1147 rangeUnion
.some((range
) =>
1148 range
== "Thing" || ranges
.has(range
)
1157 * Returns the provided value converted into a `String` object with
1158 * `.["@value"]` and `.["@language"]` properties.
1160 * The same object will be returned for every call with an equivalent
1163 * TODO: Ideally this would be extracted more fully into an R·D·F
1166 * ※ This function is not exposed.
1171 * Returns the language string object corresponding to the provided
1172 * value and language.
1174 const getLangString
= (value
, language
= "") => {
1175 const valueMap
= languageMap
[language
] ??= Object
.create(null);
1176 const literal
= valueMap
[value
]?.deref();
1177 if (literal
!= null) {
1178 // There is already an object corresponding to the provided value
1182 // No object already exists corresponding to the provided value
1183 // and language; create one.
1184 const result
= Object
.preventExtensions(
1185 Object
.create(String
.prototype, {
1191 enumerable
: !!language
,
1192 value
: language
|| null,
1194 language
: { enumerable
: false, get: getLanguage
},
1195 toString
: { enumerable
: false, value
: toString
},
1196 valueOf
: { enumerable
: false, value
: valueOf
},
1199 const ref
= new WeakRef(result
);
1200 langStringRegistry
.register(result
, { ref
, language
, value
});
1201 valueMap
[value
] = ref
;
1206 /** Returns the `.["@language"]` of this object. */
1207 const getLanguage
= Object
.defineProperty(
1209 return this["@language"] || null;
1212 { value
: "get language" },
1216 * A `FinalizationRegistry` for language string objects.
1218 * This simply cleans up the corresponding `WeakRef` in the language
1221 const langStringRegistry
= new FinalizationRegistry(
1222 ({ ref
, language
, value
}) => {
1223 const valueMap
= languageMap
[language
];
1224 if (valueMap
?.[value
] === ref
) {
1225 delete valueMap
[value
];
1233 * An object whose own values are an object mapping values to
1234 * language string objects for the language specified by the key.
1236 const languageMap
= Object
.create(null);
1238 /** Returns the `.["@value"]` of this object. */
1239 const toString = function () {
1240 return this["@value"];
1244 * Returns this object if it has a `.["@language"]`; otherwise, its
1247 const valueOf = function () {
1248 return this["@language"] ? this : this["@value"];
1258 `${$["@language"] ?? ""}`,
1260 : getLangString(`${$["@value"]}`)
1262 ? getLangString(`${$}`, `${$.language ?? ""}`)
1263 : getLangString(`${$}`)
1264 : getLangString(`${$ ?? ""}`),
1269 * Returns a normalized tag data object derived from the provided
1272 * ※ The properties of this function need to match the term names used
1273 * in the ActivityStreams serialization.
1275 * ※ This function is not exposed.
1277 const tagData
= ($) => {
1278 const data
= Object($);
1280 // prefLabel intentionally not set here
1290 let prefLabel
= langString(data
.prefLabel
);
1291 return Object
.preventExtensions(Object
.create(null, {
1294 get: () => prefLabel
,
1296 prefLabel
= langString($);
1303 ? Array
.from(altLabel
, langString
)
1311 ? Array
.from(hiddenLabel
, langString
)
1319 ? Array
.from(broader
, toIdentifier
)
1327 ? Array
.from(narrower
, toIdentifier
)
1335 ? Array
.from(inCanon
, toIdentifier
)
1343 ? Array
.from(hasInCanon
, toIdentifier
)
1351 ? Array
.from(involves
, toIdentifier
)
1359 ? Array
.from(involvedIn
, toIdentifier
)
1367 * Returns an identifier corresponding to the provided object.
1369 * This is either the value of its `.identifier` or its string value.
1371 * ※ This function is not exposed.
1373 const toIdentifier
= ($) =>
1376 : Object($) === $ && "identifier" in $
1381 * A tag system, with storage.
1383 * The `::Tag` constructor available on any `TagSystem` instance can be
1384 * used to create new `Tag`s within the system.
1386 export class TagSystem
{
1387 /** The cached bound `Tag` constructor for this `TagSystem`. */
1390 /** The domain of this `TagSystem`. */
1393 /** The date of this `TagSystem`. */
1396 /** The identifier of this `TagSystem`. */
1399 /** The internal `Storage` of this `TagSystem`. */
1400 #storage
= new Storage();
1403 * Constructs a new `TagSystem` with the provided domain and date.
1405 * Only actual, lowercased domain names are allowed for the domain,
1406 * and the date must be “full” (include month and day components).
1407 * This is for alignment with general best practices for Tag URI’s.
1409 * ☡ This constructor throws if provided with an invalid date.
1411 constructor(domain
, date
, identifier
= "") {
1412 const domainString
= `${domain}`;
1413 const dateString
= `${date}`;
1414 this.#identifier
= `${identifier}`;
1416 // If the identifier is a valid storage I·D, reserve it.
1417 this.#storage
.delete(this.#identifier
);
1419 // The identifier is not a valid storage I·D, so no worries.
1423 !/^[a-z](?:[\da-z-]*[\da-z])?(?:\.[a-z](?:[\da-z-]*[\da-z])?)*$/u
1426 // ☡ The domain is invalid.
1427 throw new RangeError(`Invalid domain: ${domain}.`);
1429 !/^\d{4}-\d{2}-\d{2}$/u.test(dateString
) ||
1430 dateString
!= new Date(dateString
).toISOString().split("T")[0]
1432 // ☡ The date is invalid.
1433 throw new RangeError(`Invalid date: ${date}.`);
1435 // The domain and date are 🆗.
1436 this.#domain
= domainString
;
1437 this.#date
= dateString
;
1442 * Returns a bound constructor for constructing `Tags` in this
1446 if (this.#Tag
!= null) {
1447 // A bound constructor has already been generated; return it.
1450 // No bound constructor has been created yet.
1451 const storage
= this.#storage
;
1452 return this.#Tag
= Tag
.For(this, storage
, schema
);
1456 /** Returns the authority name (domain) for this `TagSystem`. */
1457 get authorityName() {
1458 return this.#domain
;
1461 /** Returns the date of this `TagSystem`, as a string. */
1467 * Yields the entities in this `TagSystem`.
1469 * ※ Entities can hypothetically be anything. If you specifically
1470 * want the `Tag`s, use `::Tag.all` instead.
1473 yield* this.#storage
.values();
1477 * Returns the identifier of this `TagSystem`.
1479 * ※ Often this is just the empty string.
1482 return this.#identifier
;
1485 /** Yields the identifiers in use in this `TagSystem`. */
1487 yield* this.#storage
.keys();
1490 /** Returns the I·R·I for this `TagSystem`. */
1492 return `${this.iriSpace}${this.identifier}`;
1496 * Returns the prefix used for I·R·I’s of `Tag`s in this `TagSystem`.
1499 return `https://${this.authorityName}/tag:${this.taggingEntity}:`;
1502 /** Returns the Tag U·R·I for this `TagSystem`. */
1504 return `tag:${this.taggingEntity}:${this.identifier}`;
1508 * Returns the tagging entity (domain and date) for this `TagSystem`.
1510 get taggingEntity() {
1511 return `${this.authorityName},${this.date}`;