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(`${$ ?? ""}`),
};
})();
});
it("[[Construct]] defaults the preferred label to the empty string", () => {
- assertStrictEquals(new Tag().prefLabel, "");
+ assertEquals({ ...new Tag().prefLabel }, { "@value": "" });
});
it("[[Construct]] correctly sets the preferred label to a simple string", () => {
- assertStrictEquals(
- new Tag("RelationshipTag", "Shadow, Me").prefLabel,
- "Shadow, Me",
+ assertEquals(
+ { ...new Tag("RelationshipTag", "Shadow, Me").prefLabel },
+ { "@value": "Shadow, Me" },
);
});
});
it("[[Call]] throws if passed an invalid I·R·I", () => {
- assertThrows(() => {Tag.fromIRI(`bad iri`)});
+ assertThrows(() => {
+ Tag.fromIRI(`bad iri`);
+ });
});
});
{ "@value": "three", "@language": "en" },
);
assertEquals(
- Array.from(
- tag.altLabels(),
- ($) => typeof $ == "string" ? $ : { ...$ },
- ),
+ Array.from(tag.altLabels(), ($) => ({ ...$ })),
[
- "one",
- "two",
+ { "@value": "one" },
+ { "@value": "two" },
{ "@value": "three", "@language": "en" },
],
);
{ "@value": "three", "@language": "en" },
);
assertEquals(
- Array.from(
- tag.hiddenLabels(),
- ($) => typeof $ == "string" ? $ : { ...$ },
- ),
+ Array.from(tag.hiddenLabels(), ($) => ({ ...$ })),
[
- "one",
- "two",
+ { "@value": "one" },
+ { "@value": "two" },
{ "@value": "three", "@language": "en" },
],
);
const tag = new Tag();
tag.addAltLabel("etaoin");
tag.deleteAltLabel();
- assertEquals([...tag.altLabels()], ["etaoin"]);
+ assertEquals(
+ Array.from(tag.altLabels(), ($) => ({ ...$ })),
+ [{ "@value": "etaoin" }],
+ );
});
it("[[Call]] deletes only the provided hidden labels", () => {
{ "@value": "three", "@language": "en" },
{ "@value": "four", "@language": "en" },
);
- assertEquals([...tag.altLabels()], ["four"]);
+ assertEquals(
+ Array.from(tag.altLabels(), ($) => ({ ...$ })),
+ [{ "@value": "four" }],
+ );
});
it("[[Call]] returns this", () => {
const tag = new Tag();
tag.addHiddenLabel("etaoin");
tag.deleteHiddenLabel();
- assertEquals([...tag.hiddenLabels()], ["etaoin"]);
+ assertEquals(
+ Array.from(tag.hiddenLabels(), ($) => ({ ...$ })),
+ [{ "@value": "etaoin" }],
+ );
});
it("[[Call]] deletes only the provided alternative labels", () => {
{ "@value": "three", "@language": "en" },
{ "@value": "four", "@language": "en" },
);
- assertEquals([...tag.hiddenLabels()], ["four"]);
+ assertEquals(
+ Array.from(tag.hiddenLabels(), ($) => ({ ...$ })),
+ [{ "@value": "four" }],
+ );
});
it("[[Call]] returns this", () => {
it("[[Set]] sets the preferred label", () => {
const tag = new Tag();
tag.prefLabel = "one";
- assertStrictEquals(tag.prefLabel, "one");
+ assertEquals({ ...tag.prefLabel }, { "@value": "one" });
tag.prefLabel = { "@value": "two" };
- assertStrictEquals(tag.prefLabel, "two");
+ assertEquals({ ...tag.prefLabel }, { "@value": "two" });
tag.prefLabel = { "@value": "three", "@language": "en" };
assertEquals(
{ ...tag.prefLabel },