* generating the value of `TagSystem::Tag`. It isn’t possible to
* access this function in its unbound form from outside this module.
*
+ * ☡ This function throws if the I·R·I is not in the `.iriSpace` of
+ * the `TagSystem` associated with this constructor.
+ *
* ※ If the I·R·I is not recognized, this function returns `null`.
*/
static fromIRI(system, storage, iri) {
const name = `${iri}`;
- const prefix =
- `https://${system.authorityName}/tag:${system.taggingEntity}:`;
+ const prefix = `${system.iriSpace}`;
if (!name.startsWith(prefix)) {
// The I·R·I does not begin with the expected prefix.
- return null;
+ throw new RangeError(
+ `I·R·I did not begin with the expected prefix: ${iri}`,
+ );
} else {
// The I·R·I begins with the expected prefix.
const identifier = name.substring(prefix.length);
if (!tagName.startsWith(tagPrefix)) {
// The Tag U·R·I does not begin with the expected prefix.
throw new RangeError(
- `Tag U·R·I did not begin with the expected prefix: ${tagName}`,
+ `Tag U·R·I did not begin with the expected prefix: ${tagURI}`,
);
} else {
// The I·R·I begins with the expected prefix.
/** Returns the I·R·I for this `Tag`. */
get iri() {
- const tagURI = this.tagURI;
- return tagURI == null
- ? null
- : `https://${this.authorityName}/${tagURI}`;
+ const { identifier, iriSpace } = this;
+ return identifier == null ? null : `${iriSpace}${identifier}`;
+ }
+
+ /** Returns the I·R·I space for this `Tag`. */
+ get iriSpace() {
+ return this.#system.iriSpace;
}
/** Returns the kind of this `Tag`. */
const {
/**
- * Returns the provided value converted into either a plain string
- * primitive or an object with `.["@value"]` and `.["@language"]`
- * properties.
+ * Returns the provided value converted into a `String` object with
+ * `.["@value"]` and `.["@language"]` properties.
+ *
+ * The same object will be returned for every call with an equivalent
+ * value.
*
* TODO: Ideally this would be extracted more fully into an R·D·F
* library.
*/
langString,
} = (() => {
+ /**
+ * Returns the language string object corresponding to the provided
+ * value and language.
+ */
+ const getLangString = (value, language = "") => {
+ const valueMap = languageMap[language] ??= Object.create(null);
+ const literal = valueMap[value]?.deref();
+ if (literal != null) {
+ // There is already an object corresponding to the provided value
+ // and language.
+ return literal;
+ } else {
+ // No object already exists corresponding to the provided value
+ // and language; create one.
+ const result = Object.preventExtensions(
+ Object.create(String.prototype, {
+ "@value": {
+ enumerable: true,
+ value,
+ },
+ "@language": {
+ enumerable: !!language,
+ value: language || null,
+ },
+ language: { enumerable: false, get: getLanguage },
+ toString: { enumerable: false, value: toString },
+ valueOf: { enumerable: false, value: valueOf },
+ }),
+ );
+ const ref = new WeakRef(result);
+ langStringRegistry.register(result, { ref, language, value });
+ valueMap[value] = ref;
+ return result;
+ }
+ };
+
/** Returns the `.["@language"]` of this object. */
const getLanguage = Object.defineProperty(
function () {
- return this["@language"];
+ return this["@language"] || null;
},
"name",
{ value: "get language" },
);
+ /**
+ * A `FinalizationRegistry` for language string objects.
+ *
+ * This simply cleans up the corresponding `WeakRef` in the language
+ * map.
+ */
+ const langStringRegistry = new FinalizationRegistry(
+ ({ ref, language, value }) => {
+ const valueMap = languageMap[language];
+ if (valueMap?.[value] === ref) {
+ delete valueMap[value];
+ } else {
+ /* do nothing */
+ }
+ },
+ );
+
+ /**
+ * An object whose own values are an object mapping values to
+ * language string objects for the language specified by the key.
+ */
+ const languageMap = Object.create(null);
+
/** Returns the `.["@value"]` of this object. */
const toString = function () {
return this["@value"];
};
- /** Returns the `.["@value"]` of this object. */
+ /**
+ * Returns this object if it has a `.["@language"]`; otherwise, its
+ * `.["@value"]`.
+ */
const valueOf = function () {
- return this["@value"];
+ return this["@language"] ? this : this["@value"];
};
return {
Object($) === $
? "@value" in $
? "@language" in $
- ? Object.preventExtensions(
- Object.create(String.prototype, {
- "@value": {
- enumerable: true,
- value: `${$["@value"]}`,
- },
- "@language": {
- enumerable: true,
- value: `${$["@language"]}`,
- },
- language: { enumerable: false, get: getLanguage },
- toString: { enumerable: false, value: toString },
- valueOf: { enumerable: false, value: valueOf },
- }),
+ ? getLangString(
+ `${$["@value"]}`,
+ `${$["@language"] ?? ""}`,
)
- : `${$["@value"]}`
+ : getLangString(`${$["@value"]}`)
: "language" in $
- ? Object.preventExtensions(
- Object.create(String.prototype, {
- "@value": { enumerable: true, value: `${$}` },
- "@language": {
- enumerable: true,
- value: `${$.language}`,
- },
- language: { enumerable: false, get: getLanguage },
- toString: { enumerable: false, value: toString },
- valueOf: { enumerable: false, value: valueOf },
- }),
- )
- : `${$}`
- : `${$ ?? ""}`,
+ ? getLangString(`${$}`, `${$.language ?? ""}`)
+ : getLangString(`${$}`)
+ : getLangString(`${$ ?? ""}`),
};
})();
/** Returns the I·R·I for this `TagSystem`. */
get iri() {
- return `https://${this.authorityName}/${this.tagURI}`;
+ return `${this.iriSpace}${this.identifier}`;
+ }
+
+ /**
+ * Returns the prefix used for I·R·I’s of `Tag`s in this `TagSystem`.
+ */
+ get iriSpace() {
+ return `https://${this.authorityName}/tag:${this.taggingEntity}:`;
}
/** Returns the Tag U·R·I for this `TagSystem`. */