]> Lady’s Gitweb - Etiquette/blobdiff - model.js
Always return (the same) objects for langstrings
[Etiquette] / model.js
index 76bf615bc2aaf4a5318af3cd75ec40bde4be7894..ba71361890494f3e3c7aaa4369af83d50b8e8440 100644 (file)
--- a/model.js
+++ b/model.js
@@ -239,15 +239,19 @@ class Tag {
    * 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);
@@ -298,7 +302,7 @@ class Tag {
     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.
@@ -385,7 +389,10 @@ class Tag {
     });
   }
 
-  /** Adds the provided label(s) to this `Tag` as alternate labels. */
+  /**
+   * Adds the provided label(s) to this `Tag` as alternate labels, then
+   * returns this `Tag`.
+   */
   addAltLabel(...labels) {
     const altLabels = this.#data.altLabel;
     let objectLabels = null; // initialized on first use
@@ -428,11 +435,12 @@ class Tag {
         altLabels.add(literal);
       }
     }
+    return this;
   }
 
   /**
    * Adds the provided tags to the list of tags that this `Tag` is
-   * narrower than.
+   * narrower than, then returns this `Tag`.
    *
    * Arguments may be string identifiers or objects with an
    * `.identifier` property.
@@ -474,9 +482,13 @@ class Tag {
         }
       }
     }
+    return this;
   }
 
-  /** Adds the provided label(s) to this `Tag` as hidden labels. */
+  /**
+   * Adds the provided label(s) to this `Tag` as hidden labels, then
+   * returns this `Tag`.
+   */
   addHiddenLabel(...labels) {
     const hiddenLabels = this.#data.hiddenLabel;
     let objectLabels = null; // initialized on first use
@@ -519,11 +531,12 @@ class Tag {
         hiddenLabels.add(literal);
       }
     }
+    return this;
   }
 
   /**
    * Adds the provided tags to the list of tags that this `Tag` is in
-   * canon with.
+   * canon with, then returns this `Tag`.
    *
    * Arguments may be string identifiers or objects with an
    * `.identifier` property.
@@ -584,11 +597,12 @@ class Tag {
         }
       }
     }
+    return this;
   }
 
   /**
    * Adds the provided tags to the list of tags that this `Tag`
-   * involves.
+   * involves, then returns this `Tag`.
    *
    * Arguments may be string identifiers or objects with an
    * `.identifier` property.
@@ -650,6 +664,7 @@ class Tag {
         }
       }
     }
+    return this;
   }
 
   /** Yields the alternative labels of this `Tag`. */
@@ -726,7 +741,7 @@ class Tag {
 
   /**
    * Removes the provided string label(s) from this `Tag` as alternate
-   * labels.
+   * labels, then returns this `Tag`.
    */
   deleteAltLabel(...labels) {
     const altLabels = this.#data.altLabel;
@@ -761,11 +776,12 @@ class Tag {
         altLabels.delete(literal);
       }
     }
+    return this;
   }
 
   /**
    * Removes the provided tags from the list of tags that this `Tag` is
-   * narrower than.
+   * narrower than, then returns this `Tag`.
    *
    * Arguments may be string identifiers or objects with an
    * `.identifier` property.
@@ -776,11 +792,12 @@ class Tag {
       // Iterate over the provided tags and delete them.
       broader.delete(toIdentifier($));
     }
+    return this;
   }
 
   /**
    * Removes the provided string label(s) from this `Tag` as hidden
-   * labels.
+   * labels, then returns this `Tag`.
    */
   deleteHiddenLabel(...labels) {
     const hiddenLabels = this.#data.hiddenLabel;
@@ -815,11 +832,12 @@ class Tag {
         hiddenLabels.delete(literal);
       }
     }
+    return this;
   }
 
   /**
    * Removes the provided tags from the list of tags that this `Tag` is
-   * in canon with.
+   * in canon with, then returns this `Tag`.
    *
    * Arguments may be string identifiers or objects with an
    * `.identifier` property.
@@ -830,11 +848,12 @@ class Tag {
       // Iterate over the provided tags and delete them.
       inCanon.delete(toIdentifier($));
     }
+    return this;
   }
 
   /**
    * Removes the provided tags from the list of tags that this `Tag`
-   * involves.
+   * involves, then returns this `Tag`.
    *
    * Arguments may be string identifiers or objects with an
    * `.identifier` property.
@@ -845,6 +864,7 @@ class Tag {
       // Iterate over the provided tags and delete them.
       involves.delete(toIdentifier($));
     }
+    return this;
   }
 
   /** Yields `Tag`s that are in canon of this `Tag`. */
@@ -957,10 +977,13 @@ class Tag {
 
   /** 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`. */
@@ -1351,9 +1374,11 @@ class 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.
@@ -1362,23 +1387,85 @@ const {
    */
   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 {
@@ -1386,37 +1473,15 @@ const {
       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(`${$ ?? ""}`),
   };
 })();
 
@@ -1684,7 +1749,14 @@ export class TagSystem {
 
   /** 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`. */
This page took 0.030648 seconds and 4 git commands to generate.