Repository: digitalbazaar/jsonld.js (JavaScript)

Issue:
JavaScript keywords are dropped
Vocabulary keys matching JavaScript keywords are incorrectly dropped during expansion, compaction, etc. Consider the following:
```js
require('jsonld').expand({
  "@context": {
    "valueOf": "http://example.org/valueOf",
    "toString": "http://example.org/toString"
  },
  "valueOf": "first",
  "toString": "second"
}).then(result => console.log(JSON.stringify(result, null, 2)))
```
This returns the completely empty...
```json
[]
```
… instead of the expected...
```json
[
  {
    "http://example.org/toString": [
      {
        "@value": "second"
      }
    ],
    "http://example.org/valueOf": [
      {
        "@value": "first"
      }
    ]
  }
]
```


Discussion:
Yeah, that's a bug.  Somewhat horrifying that such a bug has slipped through for so long!  Thanks for pointing it out.  We'll release some test cases and a fix soon.

It's an issue of using a regular `Object` and `if(term in obj)` vs `if(obj.hasOwnProperty(term))`.  We had wanted to change code using checks like that to use `Map` which would avoid such issues.  But due to some active context implementation details that is a complex change so was put aside.  Will have to just selectively fix it for now.
Fix is in 1.6.0.  Thanks.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ce3d138..f6b002ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,19 @@
 # jsonld ChangeLog
 
+## 1.6.0 - 2019-03-xx
+
 ### Fixed
 - Testing: Use explicit id and description skipping regexes.
+- Usage of JavaScript Object property names in data.
+  - **NOTE**: A class of bugs was causing term names such as `toString`,
+    `valueOf`, and others to be dropped or produce bogus output. The fix could
+    cause output triples to differ from previous versions if those special
+    names were used.
+  - Specifically, the problem was using `x in obj` instead of
+    `obj.hasOwnProperty(x)` or a `Map`.
+  - Fixed usage in contexts for expand and compact.
+  - Attempted fixes in other parts of the code with similar `x in obj` usage.
+    Finding actual realistic failing test cases proved difficult.
 
 ### Changed
 - Testing: Improve skip logging.
@@ -9,7 +21,8 @@
 ### Added
 - Testing: `skip` and `only` flags in manifests.
 - Testing: `VERBOSE_SKIP=true` env var to debug skipping.
-- Support `@protected` and `protectedMode`.
+- Support `@protected`.
+- Support experimental non-standard `protectedMode` option.
 
 ## 1.5.4 - 2019-02-28
 
@@ -37,7 +50,7 @@
 
 ### Changed
 - [rdf-canonize][] updated:
-  - **BREAKING**: A fix was applied that makes the canoncial output format
+  - **BREAKING**: A fix was applied that makes the canonical output format
     properly match the N-Triples canoncial format. This fixes the format to no
     longer escape tabs in literals. This may cause canonical output from
     `jsonld.normalize()`/`jsonld.canonize()` to differ from previous versions
@@ -228,7 +241,7 @@
 ### Changed
 - **BREAKING**: Use RDF JS (rdf.js.org) interfaces for internal
   representation of dataset and quads. This should only break
-  code that was using undocumented internal datastructures,
+  code that was using undocumented internal data structures,
   backwards-compat code exists to handle external RDF parsers.
 - Update `rdf-canonize` to dependency with native support.
 
@@ -457,7 +470,7 @@
 - "request" extension moved to [jsonld-request][]. This was done to simplify
   the core JSON-LD processing library. In particular, the extension pulled in
   RDFa processing code and related dependencies. The old method of using this
-  exension of `jsonld.use('request')` is deprecated in favor of using the new
+  extension of `jsonld.use('request')` is deprecated in favor of using the new
   module directly.
 - The `jsonld` tool moved to [jsonld-cli][]. This was also done to simplify the
   core JSON-LD processing library and because it uses the [jsonld-request][]
diff --git a/lib/compact.js b/lib/compact.js
index 479fdbfe..7e52fa85 100644
--- a/lib/compact.js
+++ b/lib/compact.js
@@ -113,7 +113,8 @@ api.compact = ({
 
   // recursively compact object
   if(_isObject(element)) {
-    if(options.link && '@id' in element && element['@id'] in options.link) {
+    if(options.link && '@id' in element &&
+      options.link.hasOwnProperty(element['@id'])) {
       // check for a linked element to reuse
       const linked = options.link[element['@id']];
       for(let i = 0; i < linked.length; ++i) {
@@ -129,7 +130,7 @@ api.compact = ({
         api.compactValue({activeCtx, activeProperty, value: element, options});
       if(options.link && _isSubjectReference(element)) {
         // store linked element
-        if(!(element['@id'] in options.link)) {
+        if(!(options.link.hasOwnProperty(element['@id']))) {
           options.link[element['@id']] = [];
         }
         options.link[element['@id']].push({expanded: element, compacted: rval});
@@ -144,7 +145,7 @@ api.compact = ({
 
     if(options.link && '@id' in element) {
       // store linked element
-      if(!(element['@id'] in options.link)) {
+      if(!options.link.hasOwnProperty(element['@id'])) {
         options.link[element['@id']] = [];
       }
       options.link[element['@id']].push({expanded: element, compacted: rval});
@@ -208,8 +209,8 @@ api.compact = ({
 
         // handle double-reversed properties
         for(const compactedProperty in compactedValue) {
-          if(activeCtx.mappings[compactedProperty] &&
-            activeCtx.mappings[compactedProperty].reverse) {
+          if(activeCtx.mappings.has(compactedProperty) &&
+            activeCtx.mappings.get(compactedProperty).reverse) {
             const value = compactedValue[compactedProperty];
             const container = _getContextValue(
               activeCtx, compactedProperty, '@container') || [];
@@ -297,8 +298,8 @@ api.compact = ({
           relativeTo: {vocab: true},
           reverse: insideReverse
         });
-        const nestProperty = (itemActiveProperty in activeCtx.mappings) ?
-          activeCtx.mappings[itemActiveProperty]['@nest'] : null;
+        const nestProperty = activeCtx.mappings.has(itemActiveProperty) ?
+          activeCtx.mappings.get(itemActiveProperty)['@nest'] : null;
         let nestResult = rval;
         if(nestProperty) {
           _checkNestProperty(activeCtx, nestProperty, options);
@@ -326,8 +327,8 @@ api.compact = ({
 
         // if itemActiveProperty is a @nest property, add values to nestResult,
         // otherwise rval
-        const nestProperty = (itemActiveProperty in activeCtx.mappings) ?
-          activeCtx.mappings[itemActiveProperty]['@nest'] : null;
+        const nestProperty = activeCtx.mappings.has(itemActiveProperty) ?
+          activeCtx.mappings.get(itemActiveProperty)['@nest'] : null;
         let nestResult = rval;
         if(nestProperty) {
           _checkNestProperty(activeCtx, nestProperty, options);
@@ -384,7 +385,7 @@ api.compact = ({
                 relativeTo: {vocab: true}
               })] = expandedItem['@index'];
             }
-          } else if(itemActiveProperty in nestResult) {
+          } else if(nestResult.hasOwnProperty(itemActiveProperty)) {
             // can't use @list container for more than 1 list
             throw new JsonLdError(
               'JSON-LD compact error; property has a "@list" @container ' +
@@ -401,7 +402,7 @@ api.compact = ({
             container.includes('@index') && _isSimpleGraph(expandedItem))) {
             // get or create the map object
             let mapObject;
-            if(itemActiveProperty in nestResult) {
+            if(nestResult.hasOwnProperty(itemActiveProperty)) {
               mapObject = nestResult[itemActiveProperty];
             } else {
               nestResult[itemActiveProperty] = mapObject = {};
@@ -472,7 +473,7 @@ api.compact = ({
           // handle language and index maps
           // get or create the map object
           let mapObject;
-          if(itemActiveProperty in nestResult) {
+          if(nestResult.hasOwnProperty(itemActiveProperty)) {
             mapObject = nestResult[itemActiveProperty];
           } else {
             nestResult[itemActiveProperty] = mapObject = {};
@@ -736,7 +737,7 @@ api.compactIri = ({
       if(iri.indexOf(vocab) === 0 && iri !== vocab) {
         // use suffix as relative iri if it is not a term in the active context
         const suffix = iri.substr(vocab.length);
-        if(!(suffix in activeCtx.mappings)) {
+        if(!activeCtx.mappings.has(suffix)) {
           return suffix;
         }
       }
@@ -767,9 +768,9 @@ api.compactIri = ({
       // 2. value is null, which means we're not compacting an @value, AND
       //   the mapping matches the IRI
       const curie = term + ':' + iri.substr(entry.iri.length);
-      const isUsableCurie = (activeCtx.mappings[term]._prefix &&
-        (!(curie in activeCtx.mappings) ||
-        (value === null && activeCtx.mappings[curie]['@id'] === iri)));
+      const isUsableCurie = (activeCtx.mappings.get(term)._prefix &&
+        (!activeCtx.mappings.has(curie) ||
+        (value === null && activeCtx.mappings.get(curie)['@id'] === iri)));
 
       // select curie if it is shorter or the same length but lexicographically
       // less than the current choice
@@ -834,8 +835,8 @@ api.compactValue = ({activeCtx, activeProperty, value, options}) => {
       (keyCount === 2 && '@index' in value && !preserveIndex));
     const hasDefaultLanguage = ('@language' in activeCtx);
     const isValueString = _isString(value['@value']);
-    const hasNullMapping = (activeCtx.mappings[activeProperty] &&
-      activeCtx.mappings[activeProperty]['@language'] === null);
+    const hasNullMapping = (activeCtx.mappings.has(activeProperty) &&
+      activeCtx.mappings.get(activeProperty)['@language'] === null);
     if(isValueOnlyKey &&
       (!hasDefaultLanguage || !isValueString || hasNullMapping)) {
       return value['@value'];
@@ -948,9 +949,9 @@ api.removePreserve = (ctx, input, options) => {
       iri: '@id',
       relativeTo: {vocab: true}
     });
-    if(idAlias in input) {
+    if(input.hasOwnProperty(idAlias)) {
       const id = input[idAlias];
-      if(id in options.link) {
+      if(options.link.hasOwnProperty(id)) {
         const idx = options.link[id].indexOf(input);
         if(idx !== -1) {
           // already visited
@@ -1020,9 +1021,9 @@ function _selectTerm(
     // try to compact value to a term
     const term = api.compactIri(
       {activeCtx, iri: value['@id'], relativeTo: {vocab: true}});
-    if(term in activeCtx.mappings &&
-      activeCtx.mappings[term] &&
-      activeCtx.mappings[term]['@id'] === value['@id']) {
+    if(activeCtx.mappings.has(term) &&
+      activeCtx.mappings.get(term) &&
+      activeCtx.mappings.get(term)['@id'] === value['@id']) {
       // prefer @vocab
       prefs.push.apply(prefs, ['@vocab', '@id']);
     } else {
diff --git a/lib/context.js b/lib/context.js
index b3619801..792c70f5 100644
--- a/lib/context.js
+++ b/lib/context.js
@@ -139,7 +139,7 @@ api.process = (
     rval = rval.clone();
 
     // define context mappings for keys in local context
-    const defined = {};
+    const defined = new Map();
 
     // handle @version
     if('@version' in ctx) {
@@ -159,7 +159,7 @@ api.process = (
       }
       rval.processingMode = 'json-ld-1.1';
       rval['@version'] = ctx['@version'];
-      defined['@version'] = true;
+      defined.set('@version', true);
     }
 
     // if not set explicitly, set processingMode to "json-ld-1.0"
@@ -184,7 +184,7 @@ api.process = (
       }
 
       rval['@base'] = base;
-      defined['@base'] = true;
+      defined.set('@base', true);
     }
 
     // handle @vocab
@@ -205,7 +205,7 @@ api.process = (
       } else {
         rval['@vocab'] = value;
       }
-      defined['@vocab'] = true;
+      defined.set('@vocab', true);
     }
 
     // handle @language
@@ -222,13 +222,13 @@ api.process = (
       } else {
         rval['@language'] = value.toLowerCase();
       }
-      defined['@language'] = true;
+      defined.set('@language', true);
     }
 
     // handle @protected; determine whether this sub-context is declaring
     // all its terms to be "protected" (exceptions can be made on a
     // per-definition basis)
-    defined['@protected'] = ctx['@protected'] || false;
+    defined.set('@protected', ctx['@protected'] || false);
 
     // process all other keys
     for(const key in ctx) {
@@ -263,9 +263,9 @@ api.process = (
 api.createTermDefinition = (
   activeCtx, localCtx, term, defined, options,
   isPropertyTermScopedContext = false) => {
-  if(term in defined) {
+  if(defined.has(term)) {
     // term already defined
-    if(defined[term]) {
+    if(defined.get(term)) {
       return;
     }
     // cycle detected
@@ -276,7 +276,7 @@ api.createTermDefinition = (
   }
 
   // now defining term
-  defined[term] = false;
+  defined.set(term, false);
 
   if(api.isKeyword(term)) {
     throw new JsonLdError(
@@ -293,7 +293,8 @@ api.createTermDefinition = (
   }
 
   // FIXME if(1.1) ... ?
-  if(term in activeCtx.protected && !isPropertyTermScopedContext) {
+  if(activeCtx.protected.hasOwnProperty(term) &&
+    !isPropertyTermScopedContext) {
     const protectedMode = (options && options.protectedMode) || 'error';
     if(protectedMode === 'error') {
       throw new JsonLdError(
@@ -313,17 +314,20 @@ api.createTermDefinition = (
   }
 
   // remove old mapping
-  if(activeCtx.mappings[term]) {
-    delete activeCtx.mappings[term];
+  if(activeCtx.mappings.has(term)) {
+    activeCtx.mappings.delete(term);
   }
 
   // get context term value
-  let value = localCtx[term];
+  let value;
+  if(localCtx.hasOwnProperty(term)) {
+    value = localCtx[term];
+  }
 
   // clear context entry
   if(value === null || (_isObject(value) && value['@id'] === null)) {
-    activeCtx.mappings[term] = null;
-    defined[term] = true;
+    activeCtx.mappings.set(term, null);
+    defined.set(term, true);
     return;
   }
 
@@ -343,7 +347,8 @@ api.createTermDefinition = (
   }
 
   // create new mapping
-  const mapping = activeCtx.mappings[term] = {};
+  const mapping = {};
+  activeCtx.mappings.set(term, mapping);
   mapping.reverse = false;
 
   // make sure term definition only has expected keywords
@@ -431,15 +436,15 @@ api.createTermDefinition = (
     // see if the term has a prefix
     if(mapping._termHasColon) {
       const prefix = term.substr(0, colon);
-      if(prefix in localCtx) {
+      if(localCtx.hasOwnProperty(prefix)) {
         // define parent prefix
         api.createTermDefinition(activeCtx, localCtx, prefix, defined, options);
       }
 
-      if(activeCtx.mappings[prefix]) {
+      if(activeCtx.mappings.has(prefix)) {
         // set @id based on prefix parent
         const suffix = term.substr(colon + 1);
-        mapping['@id'] = activeCtx.mappings[prefix]['@id'] + suffix;
+        mapping['@id'] = activeCtx.mappings.get(prefix)['@id'] + suffix;
       } else {
         // term is an absolute IRI
         mapping['@id'] = term;
@@ -459,12 +464,12 @@ api.createTermDefinition = (
 
   // Handle term protection
   if(value['@protected'] === true ||
-    (defined['@protected'] === true && value['@protected'] !== false)) {
+    (defined.get('@protected') === true && value['@protected'] !== false)) {
     activeCtx.protected[term] = true;
   }
 
   // IRI mapping now defined
-  defined[term] = true;
+  defined.set(term, true);
 
   if('@type' in value) {
     let type = value['@type'];
@@ -674,13 +679,14 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
   }
 
   // define term dependency if not defined
-  if(localCtx && value in localCtx && defined[value] !== true) {
+  if(localCtx && localCtx.hasOwnProperty(value) &&
+    defined.get(value) !== true) {
     api.createTermDefinition(activeCtx, localCtx, value, defined, options);
   }
 
   relativeTo = relativeTo || {};
   if(relativeTo.vocab) {
-    const mapping = activeCtx.mappings[value];
+    const mapping = activeCtx.mappings.get(value);
 
     // value is explicitly ignored with a null mapping
     if(mapping === null) {
@@ -706,13 +712,13 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
     }
 
     // prefix dependency not defined, define it
-    if(localCtx && prefix in localCtx) {
+    if(localCtx && localCtx.hasOwnProperty(prefix)) {
       api.createTermDefinition(activeCtx, localCtx, prefix, defined, options);
     }
 
     // use mapping if prefix is defined
-    const mapping = activeCtx.mappings[prefix];
-    if(mapping) {
+    if(activeCtx.mappings.has(prefix)) {
+      const mapping = activeCtx.mappings.get(prefix);
       return mapping['@id'] + suffix;
     }
 
@@ -752,7 +758,7 @@ api.getInitialContext = options => {
   const initialContext = {
     '@base': base,
     processingMode: options.processingMode,
-    mappings: {},
+    mappings: new Map(),
     inverse: null,
     getInverse: _createInverseContext,
     clone: _cloneActiveContext,
@@ -792,10 +798,9 @@ api.getInitialContext = options => {
     // create term selections for each mapping in the context, ordered by
     // shortest and then lexicographically least
     const mappings = activeCtx.mappings;
-    const terms = Object.keys(mappings).sort(_compareShortestLeast);
-    for(let i = 0; i < terms.length; ++i) {
-      const term = terms[i];
-      const mapping = mappings[term];
+    const terms = [...mappings.keys()].sort(_compareShortestLeast);
+    for(const term of terms) {
+      const mapping = mappings.get(term);
       if(mapping === null) {
         continue;
       }
@@ -805,8 +810,7 @@ api.getInitialContext = options => {
 
       // iterate over every IRI in the mapping
       const ids = _asArray(mapping['@id']);
-      for(let ii = 0; ii < ids.length; ++ii) {
-        const iri = ids[ii];
+      for(const iri of ids) {
         let entry = inverse[iri];
         const isKeyword = api.isKeyword(iri);
 
@@ -885,17 +889,17 @@ api.getInitialContext = options => {
 
     let iri;
     let letter;
-    for(let i = 0; i < entries.length; ++i) {
-      iri = entries[i].iri;
+    for(const entry of entries) {
+      iri = entry.iri;
       if(idx >= iri.length) {
         letter = '';
       } else {
         letter = iri[idx];
       }
       if(letter in next) {
-        next[letter].push(entries[i]);
+        next[letter].push(entry);
       } else {
-        next[letter] = [entries[i]];
+        next[letter] = [entry];
       }
     }
 
@@ -915,7 +919,7 @@ api.getInitialContext = options => {
    * @param typeOrLanguageValue the key in the entry to add to.
    */
   function _addPreferredTerm(term, entry, typeOrLanguageValue) {
-    if(!(typeOrLanguageValue in entry)) {
+    if(!entry.hasOwnProperty(typeOrLanguageValue)) {
       entry[typeOrLanguageValue] = term;
     }
   }
@@ -964,21 +968,21 @@ api.getContextValue = (ctx, key, type) => {
   }
 
   // get specific entry information
-  if(ctx.mappings[key]) {
-    const entry = ctx.mappings[key];
+  if(ctx.mappings.has(key)) {
+    const entry = ctx.mappings.get(key);
 
     if(_isUndefined(type)) {
       // return whole entry
       return entry;
     }
-    if(type in entry) {
+    if(entry.hasOwnProperty(type)) {
       // return entry value for type
       return entry[type];
     }
   }
 
   // get default language
-  if(type === '@language' && (type in ctx)) {
+  if(type === '@language' && ctx.hasOwnProperty(type)) {
     return ctx[type];
   }
 
diff --git a/lib/expand.js b/lib/expand.js
index ab9666bd..6dcb4990 100644
--- a/lib/expand.js
+++ b/lib/expand.js
@@ -679,7 +679,7 @@ function _expandObject({
 
     // FIXME: can this be merged with code above to simplify?
     // merge in reverse properties
-    if(termCtx.mappings[key] && termCtx.mappings[key].reverse) {
+    if(termCtx.mappings.has(key) && termCtx.mappings.get(key).reverse) {
       const reverseMap =
         expandedParent['@reverse'] = expandedParent['@reverse'] || {};
       expandedValue = _asArray(expandedValue);
diff --git a/lib/util.js b/lib/util.js
index 4af7452e..a41e6e7b 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -45,8 +45,8 @@ if(typeof process === 'object' && typeof process.nextTick === 'function') {
 api.setImmediate = _setImmediate ? _delay : api.nextTick;
 
 /**
- * Clones an object, array, or string/number. If a typed JavaScript object
- * is given, such as a Date, it will be converted to a string.
+ * Clones an object, array, Map, Set, or string/number. If a typed JavaScript
+ * object is given, such as a Date, it will be converted to a string.
  *
  * @param value the value to clone.
  *
@@ -60,6 +60,16 @@ api.clone = function(value) {
       for(let i = 0; i < value.length; ++i) {
         rval[i] = api.clone(value[i]);
       }
+    } else if(value instanceof Map) {
+      rval = new Map();
+      for(const [k, v] of value) {
+        rval.set(k, api.clone(v));
+      }
+    } else if(value instanceof Set) {
+      rval = new Set();
+      for(const v of value) {
+        rval.add(api.clone(v));
+      }
     } else if(types.isObject(value)) {
       rval = {};
       for(const key in value) {
@@ -142,7 +152,7 @@ api.parseLinkHeader = header => {
     const rel = result['rel'] || '';
     if(Array.isArray(rval[rel])) {
       rval[rel].push(result);
-    } else if(rel in rval) {
+    } else if(rval.hasOwnProperty(rel)) {
       rval[rel] = [rval[rel], result];
     } else {
       rval[rel] = result;
@@ -192,7 +202,7 @@ api.validateTypeValue = v => {
  * @return true if the subject has the given property, false if not.
  */
 api.hasProperty = (subject, property) => {
-  if(property in subject) {
+  if(subject.hasOwnProperty(property)) {
     const value = subject[property];
     return (!types.isArray(value) || value.length > 0);
   }
@@ -253,13 +263,13 @@ api.addValue = (subject, property, value, options) => {
 
   if(types.isArray(value)) {
     if(value.length === 0 && options.propertyIsArray &&
-      !(property in subject)) {
+      !subject.hasOwnProperty(property)) {
       subject[property] = [];
     }
     for(let i = 0; i < value.length; ++i) {
       api.addValue(subject, property, value[i], options);
     }
-  } else if(property in subject) {
+  } else if(subject.hasOwnProperty(property)) {
     // check if subject already has value if duplicates not allowed
     const hasValue = (!options.allowDuplicate &&
       api.hasValue(subject, property, value));


Test patch:
diff --git a/tests/misc.js b/tests/misc.js
index 3a0c128b..52af9e41 100644
--- a/tests/misc.js
+++ b/tests/misc.js
@@ -364,3 +364,148 @@ describe('url tests', () => {
     done();
   });
 });
+
+describe('js keywords', () => {
+  it('expand js valueOf/toString keywords (top ctx)', async () => {
+    const d =
+{
+  "@context": {
+    "valueOf": "http://example.org/valueOf",
+    "toString": "http://example.org/toString"
+  },
+  "valueOf": "first",
+  "toString": "second"
+}
+;
+    const ex =
+[
+  {
+    "http://example.org/toString": [
+      {
+        "@value": "second"
+      }
+    ],
+    "http://example.org/valueOf": [
+      {
+        "@value": "first"
+      }
+    ]
+  }
+]
+;
+    const e = await jsonld.expand(d);
+    assert.deepStrictEqual(e, ex);
+  });
+
+  it('expand js valueOf/toString keywords (sub ctx)', async () => {
+    const d =
+{
+  "@context": {
+    "@version": 1.1,
+    "ex:thing": {
+      "@context": {
+        "valueOf": "http://example.org/valueOf",
+        "toString": "http://example.org/toString"
+      }
+    }
+  },
+  "ex:thing": {
+    "valueOf": "first",
+    "toString": "second"
+  }
+}
+;
+    const ex =
+[
+  {
+    "ex:thing": [
+      {
+        "http://example.org/toString": [
+          {
+            "@value": "second"
+          }
+        ],
+        "http://example.org/valueOf": [
+          {
+            "@value": "first"
+          }
+        ]
+      }
+    ]
+  }
+]
+;
+    const e = await jsonld.expand(d);
+    assert.deepStrictEqual(e, ex);
+  });
+
+  it('compact js valueOf/toString keywords', async () => {
+    const d =
+{
+  "@context": {
+    "valueOf": "http://example.org/valueOf",
+    "toString": "http://example.org/toString"
+  },
+  "valueOf": "first",
+  "toString": "second"
+}
+;
+    const ctx =
+{
+  "@context": {
+    "valueOf": "http://example.org/valueOf",
+    "toString": "http://example.org/toString"
+  }
+}
+;
+    const ex =
+{
+  "@context": {
+    "valueOf": "http://example.org/valueOf",
+    "toString": "http://example.org/toString"
+  },
+  "valueOf": "first",
+  "toString": "second"
+}
+;
+    const e = await jsonld.compact(d, ctx);
+    assert.deepStrictEqual(e, ex);
+  });
+
+  it('frame js valueOf/toString keywords', async () => {
+    const d =
+{
+  "@context": {
+    "@vocab": "http://example.org/"
+  },
+  "toString": {
+    "valueOf": "thing"
+  }
+}
+;
+    const frame =
+{
+  "@context": {
+    "@vocab": "http://example.org/"
+  },
+  "toString": {}
+}
+;
+    const ex =
+{
+  "@context": {
+    "@vocab": "http://example.org/"
+  },
+  "@graph": [
+    {
+      "toString": {
+        "valueOf": "thing"
+      }
+    }
+  ]
+}
+;
+    const e = await jsonld.frame(d, frame);
+    assert.deepStrictEqual(e, ex);
+  });
+});