From 60f3bc2b9a13eac91c646ea57880f2e64b516e34 Mon Sep 17 00:00:00 2001
From: Tobias Looker <tobias.looker@mattr.global>
Date: Mon, 17 May 2021 16:49:40 +1200
Subject: [PATCH 01/16] feat: extend expansion map for detecting relative IRIs
 for @id and @type terms

---
 lib/expand.js |  47 ++++++++++--
 tests/misc.js | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 245 insertions(+), 6 deletions(-)

diff --git a/lib/expand.js b/lib/expand.js
index 44c5102f..50a455fa 100644
--- a/lib/expand.js
+++ b/lib/expand.js
@@ -505,10 +505,27 @@ async function _expandObject({
         }
       }
 
+      const expandedValues = _asArray(value).map(v =>
+        _isString(v) ? _expandIri(activeCtx, v, {base: true}, options) : v);
+
+      for(const i in expandedValues) {
+        if(_isString(expandedValues[i]) && !_isAbsoluteIri(expandedValues[i])) {
+          const expansionMapResult = await expansionMap({
+            relativeIri: {type: key, value: expandedValues[i]},
+            activeCtx,
+            activeProperty,
+            options,
+            insideList
+          });
+          if(expansionMapResult !== undefined) {
+            expandedValues[i] = expansionMapResult;
+          }
+        }
+      }
+
       _addValue(
         expandedParent, '@id',
-        _asArray(value).map(v =>
-          _isString(v) ? _expandIri(activeCtx, v, {base: true}, options) : v),
+        expandedValues,
         {propertyIsArray: options.isFrame});
       continue;
     }
@@ -525,12 +542,30 @@ async function _expandObject({
         ]));
       }
       _validateTypeValue(value, options.isFrame);
+
+      const expandedValues = _asArray(value).map(v =>
+        _isString(v) ?
+          _expandIri(typeScopedContext, v,
+            {base: true, vocab: true}, options) : v);
+
+      for(const i in expandedValues) {
+        if(_isString(expandedValues[i]) && !_isAbsoluteIri(expandedValues[i])) {
+          const expansionMapResult = await expansionMap({
+            relativeIri: {type: key, value: expandedValues[i]},
+            activeCtx,
+            activeProperty,
+            options,
+            insideList
+          });
+          if(expansionMapResult !== undefined) {
+            expandedValues[i] = expansionMapResult;
+          }
+        }
+      }
+
       _addValue(
         expandedParent, '@type',
-        _asArray(value).map(v =>
-          _isString(v) ?
-            _expandIri(typeScopedContext, v,
-              {base: true, vocab: true}, options) : v),
+        expandedValues,
         {propertyIsArray: options.isFrame});
       continue;
     }
diff --git a/tests/misc.js b/tests/misc.js
index 1c3758e0..c7156209 100644
--- a/tests/misc.js
+++ b/tests/misc.js
@@ -478,3 +478,207 @@ describe('literal JSON', () => {
     });
   });
 });
+
+describe('expansionMap', () => {
+  it('should be called on un-mapped term', async () => {
+    const docWithUnMappedTerm = {
+      '@context': {
+        'definedTerm': 'https://example.com#definedTerm'
+      },
+      definedTerm: "is defined",
+      testUndefined: "is undefined"
+    };
+
+    let expansionMapCalled = false;
+    const expansionMap = info => {
+      if(info.unmappedProperty === 'testUndefined') {
+        expansionMapCalled = true;
+      }
+    };
+
+    await jsonld.expand(docWithUnMappedTerm, {expansionMap});
+
+    assert.equal(expansionMapCalled, true);
+  });
+
+  it('should be called on nested un-mapped term', async () => {
+    const docWithUnMappedTerm = {
+      '@context': {
+        'definedTerm': 'https://example.com#definedTerm'
+      },
+      definedTerm: {
+        testUndefined: "is undefined"
+      }
+    };
+
+    let expansionMapCalled = false;
+    const expansionMap = info => {
+      if(info.unmappedProperty === 'testUndefined') {
+        expansionMapCalled = true;
+      }
+    };
+
+    await jsonld.expand(docWithUnMappedTerm, {expansionMap});
+
+    assert.equal(expansionMapCalled, true);
+  });
+
+  it('should be called on relative iri for id term', async () => {
+    const docWithRelativeIriId = {
+      '@context': {
+        'definedTerm': 'https://example.com#definedTerm'
+      },
+      '@id': "relativeiri",
+      definedTerm: "is defined"
+    };
+
+    let expansionMapCalled = false;
+    const expansionMap = info => {
+      if(info.relativeIri && info.relativeIri.value === 'relativeiri') {
+        expansionMapCalled = true;
+      }
+    };
+
+    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+    assert.equal(expansionMapCalled, true);
+  });
+
+  it('should be called on relative iri for id term (nested)', async () => {
+    const docWithRelativeIriId = {
+      '@context': {
+        'definedTerm': 'https://example.com#definedTerm'
+      },
+      '@id': "urn:absoluteIri",
+      definedTerm: {
+        '@id': "relativeiri"
+      }
+    };
+
+    let expansionMapCalled = false;
+    const expansionMap = info => {
+      if(info.relativeIri && info.relativeIri.value === 'relativeiri') {
+        expansionMapCalled = true;
+      }
+    };
+
+    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+    assert.equal(expansionMapCalled, true);
+  });
+
+  it('should be called on relative iri for aliased id term', async () => {
+    const docWithRelativeIriId = {
+      '@context': {
+        'id': '@id',
+        'definedTerm': 'https://example.com#definedTerm'
+      },
+      'id': "relativeiri",
+      definedTerm: "is defined"
+    };
+
+    let expansionMapCalled = false;
+    const expansionMap = info => {
+      if(info.relativeIri && info.relativeIri.value === 'relativeiri') {
+        expansionMapCalled = true;
+      }
+    };
+
+    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+    assert.equal(expansionMapCalled, true);
+  });
+
+  it('should be called on relative iri for type term', async () => {
+    const docWithRelativeIriId = {
+      '@context': {
+        'definedTerm': 'https://example.com#definedTerm'
+      },
+      'id': "urn:absoluteiri",
+      '@type': "relativeiri",
+      definedTerm: "is defined"
+    };
+
+    let expansionMapCalled = false;
+    const expansionMap = info => {
+      if(info.relativeIri && info.relativeIri.value === 'relativeiri') {
+        expansionMapCalled = true;
+      }
+    };
+
+    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+    assert.equal(expansionMapCalled, true);
+  });
+
+  it('should be called on relative iri for \
+  type term with multiple relative iri types', async () => {
+    const docWithRelativeIriId = {
+      '@context': {
+        'definedTerm': 'https://example.com#definedTerm'
+      },
+      'id': "urn:absoluteiri",
+      '@type': ["relativeiri", "anotherRelativeiri" ],
+      definedTerm: "is defined"
+    };
+
+    let expansionMapCalledTimes = 0;
+    const expansionMap = info => {
+      if(info.relativeIri &&
+        (info.relativeIri.value === 'relativeiri' ||
+         info.relativeIri.value === 'anotherRelativeiri')) {
+        expansionMapCalledTimes++;
+      }
+    };
+
+    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+    assert.equal(expansionMapCalledTimes, 2);
+  });
+
+  it('should be called on relative iri for \
+  type term with multiple types', async () => {
+    const docWithRelativeIriId = {
+      '@context': {
+        'definedTerm': 'https://example.com#definedTerm'
+      },
+      'id': "urn:absoluteiri",
+      '@type': ["relativeiri", "definedTerm" ],
+      definedTerm: "is defined"
+    };
+
+    let expansionMapCalled = false;
+    const expansionMap = info => {
+      if(info.relativeIri && info.relativeIri.value === 'relativeiri') {
+        expansionMapCalled = true;
+      }
+    };
+
+    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+    assert.equal(expansionMapCalled, true);
+  });
+
+  it('should be called on relative iri for aliased type term', async () => {
+    const docWithRelativeIriId = {
+      '@context': {
+        'type': "@type",
+        'definedTerm': 'https://example.com#definedTerm'
+      },
+      'id': "urn:absoluteiri",
+      'type': "relativeiri",
+      definedTerm: "is defined"
+    };
+
+    let expansionMapCalled = false;
+    const expansionMap = info => {
+      if(info.relativeIri && info.relativeIri.value === 'relativeiri') {
+        expansionMapCalled = true;
+      }
+    };
+
+    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+    assert.equal(expansionMapCalled, true);
+  });
+});

From 9a8117ff49fc9438699791a14e46f0b33335d8af Mon Sep 17 00:00:00 2001
From: Tobias Looker <tobias.looker@mattr.global>
Date: Mon, 17 May 2021 16:49:49 +1200
Subject: [PATCH 02/16] fix: linting

---
 lib/util.js          | 2 +-
 tests/test-common.js | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/lib/util.js b/lib/util.js
index 77da8f61..1458005a 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -130,7 +130,7 @@ api.parseLinkHeader = header => {
     while((match = REGEX_LINK_HEADER_PARAMS.exec(params))) {
       result[match[1]] = (match[2] === undefined) ? match[3] : match[2];
     }
-    const rel = result['rel'] || '';
+    const rel = result.rel || '';
     if(Array.isArray(rval[rel])) {
       rval[rel].push(result);
     } else if(rval.hasOwnProperty(rel)) {
diff --git a/tests/test-common.js b/tests/test-common.js
index b9982182..9cc520c7 100644
--- a/tests/test-common.js
+++ b/tests/test-common.js
@@ -398,7 +398,7 @@ function addManifest(manifest, parent) {
  */
 function addTest(manifest, test, tests) {
   // expand @id and input base
-  const test_id = test['@id'] || test['id'];
+  const test_id = test['@id'] || test.id;
   //var number = test_id.substr(2);
   test['@id'] =
     manifest.baseIri +
@@ -958,10 +958,10 @@ function createDocumentLoader(test) {
         }
 
         // If not JSON-LD, alternate may point there
-        if(linkHeaders['alternate'] &&
-          linkHeaders['alternate'].type == 'application/ld+json' &&
+        if(linkHeaders.alternate &&
+          linkHeaders.alternate.type == 'application/ld+json' &&
           !(contentType || '').match(/^application\/(\w*\+)?json$/)) {
-          doc.documentUrl = prependBase(url, linkHeaders['alternate'].target);
+          doc.documentUrl = prependBase(url, linkHeaders.alternate.target);
         }
       }
     }

From 15775b06eea60a48106e3eec14460bd4261195cc Mon Sep 17 00:00:00 2001
From: Tobias Looker <tobias.looker@mattr.global>
Date: Tue, 18 May 2021 11:05:39 +1200
Subject: [PATCH 03/16] fix: call expansion map from _expandIri instead of
 _expandObject

---
 lib/context.js | 13 ++++++++++++-
 lib/expand.js  |  3 +++
 tests/misc.js  | 19 +++++++++----------
 3 files changed, 24 insertions(+), 11 deletions(-)

diff --git a/lib/context.js b/lib/context.js
index 1a0161b5..0ad282ef 100644
--- a/lib/context.js
+++ b/lib/context.js
@@ -1053,7 +1053,18 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
       return prependBase(prependBase(options.base, activeCtx['@base']), value);
     }
   } else if(relativeTo.base) {
-    return prependBase(options.base, value);
+    value = prependBase(options.base, value);
+  }
+
+  if(!_isAbsoluteIri(value) && options.expansionMap) {
+    // TODO: use `await` to support async
+    const expandedResult = options.expansionMap({
+      relativeIri: value,
+      activeCtx,
+    });
+    if(expandedResult !== undefined) {
+      value = expandedResult;
+    }
   }
 
   return value;
diff --git a/lib/expand.js b/lib/expand.js
index 50a455fa..a9caa6e9 100644
--- a/lib/expand.js
+++ b/lib/expand.js
@@ -420,6 +420,9 @@ async function _expandObject({
   const nests = [];
   let unexpandedValue;
 
+  // Add expansion map to the processing options
+  options = {...options, expansionMap};
+
   // Figure out if this is the type for a JSON literal
   const isJsonType = element[typeKey] &&
     _expandIri(activeCtx,
diff --git a/tests/misc.js b/tests/misc.js
index c7156209..bc8c7aac 100644
--- a/tests/misc.js
+++ b/tests/misc.js
@@ -534,7 +534,7 @@ describe('expansionMap', () => {
 
     let expansionMapCalled = false;
     const expansionMap = info => {
-      if(info.relativeIri && info.relativeIri.value === 'relativeiri') {
+      if(info.relativeIri === 'relativeiri') {
         expansionMapCalled = true;
       }
     };
@@ -557,7 +557,7 @@ describe('expansionMap', () => {
 
     let expansionMapCalled = false;
     const expansionMap = info => {
-      if(info.relativeIri && info.relativeIri.value === 'relativeiri') {
+      if(info.relativeIri === 'relativeiri') {
         expansionMapCalled = true;
       }
     };
@@ -579,7 +579,7 @@ describe('expansionMap', () => {
 
     let expansionMapCalled = false;
     const expansionMap = info => {
-      if(info.relativeIri && info.relativeIri.value === 'relativeiri') {
+      if(info.relativeIri === 'relativeiri') {
         expansionMapCalled = true;
       }
     };
@@ -601,7 +601,7 @@ describe('expansionMap', () => {
 
     let expansionMapCalled = false;
     const expansionMap = info => {
-      if(info.relativeIri && info.relativeIri.value === 'relativeiri') {
+      if(info.relativeIri === 'relativeiri') {
         expansionMapCalled = true;
       }
     };
@@ -624,16 +624,15 @@ describe('expansionMap', () => {
 
     let expansionMapCalledTimes = 0;
     const expansionMap = info => {
-      if(info.relativeIri &&
-        (info.relativeIri.value === 'relativeiri' ||
-         info.relativeIri.value === 'anotherRelativeiri')) {
+      if(info.relativeIri === 'relativeiri' ||
+         info.relativeIri === 'anotherRelativeiri') {
         expansionMapCalledTimes++;
       }
     };
 
     await jsonld.expand(docWithRelativeIriId, {expansionMap});
 
-    assert.equal(expansionMapCalledTimes, 2);
+    assert.equal(expansionMapCalledTimes, 3);
   });
 
   it('should be called on relative iri for \
@@ -649,7 +648,7 @@ describe('expansionMap', () => {
 
     let expansionMapCalled = false;
     const expansionMap = info => {
-      if(info.relativeIri && info.relativeIri.value === 'relativeiri') {
+      if(info.relativeIri === 'relativeiri') {
         expansionMapCalled = true;
       }
     };
@@ -672,7 +671,7 @@ describe('expansionMap', () => {
 
     let expansionMapCalled = false;
     const expansionMap = info => {
-      if(info.relativeIri && info.relativeIri.value === 'relativeiri') {
+      if(info.relativeIri === 'relativeiri') {
         expansionMapCalled = true;
       }
     };

From 53048bab9a7675eac871da424ed453fb250edc76 Mon Sep 17 00:00:00 2001
From: Tobias Looker <tobias.looker@mattr.global>
Date: Tue, 18 May 2021 11:11:25 +1200
Subject: [PATCH 04/16] fix: duplicate expansionMap logic

---
 lib/expand.js | 46 ++++++----------------------------------------
 1 file changed, 6 insertions(+), 40 deletions(-)

diff --git a/lib/expand.js b/lib/expand.js
index a9caa6e9..6c852294 100644
--- a/lib/expand.js
+++ b/lib/expand.js
@@ -508,27 +508,10 @@ async function _expandObject({
         }
       }
 
-      const expandedValues = _asArray(value).map(v =>
-        _isString(v) ? _expandIri(activeCtx, v, {base: true}, options) : v);
-
-      for(const i in expandedValues) {
-        if(_isString(expandedValues[i]) && !_isAbsoluteIri(expandedValues[i])) {
-          const expansionMapResult = await expansionMap({
-            relativeIri: {type: key, value: expandedValues[i]},
-            activeCtx,
-            activeProperty,
-            options,
-            insideList
-          });
-          if(expansionMapResult !== undefined) {
-            expandedValues[i] = expansionMapResult;
-          }
-        }
-      }
-
       _addValue(
         expandedParent, '@id',
-        expandedValues,
+        _asArray(value).map(v =>
+          _isString(v) ? _expandIri(activeCtx, v, {base: true}, options) : v),
         {propertyIsArray: options.isFrame});
       continue;
     }
@@ -546,29 +529,12 @@ async function _expandObject({
       }
       _validateTypeValue(value, options.isFrame);
 
-      const expandedValues = _asArray(value).map(v =>
-        _isString(v) ?
-          _expandIri(typeScopedContext, v,
-            {base: true, vocab: true}, options) : v);
-
-      for(const i in expandedValues) {
-        if(_isString(expandedValues[i]) && !_isAbsoluteIri(expandedValues[i])) {
-          const expansionMapResult = await expansionMap({
-            relativeIri: {type: key, value: expandedValues[i]},
-            activeCtx,
-            activeProperty,
-            options,
-            insideList
-          });
-          if(expansionMapResult !== undefined) {
-            expandedValues[i] = expansionMapResult;
-          }
-        }
-      }
-
       _addValue(
         expandedParent, '@type',
-        expandedValues,
+        _asArray(value).map(v =>
+          _isString(v) ?
+            _expandIri(typeScopedContext, v,
+              {base: true, vocab: true}, options) : v),
         {propertyIsArray: options.isFrame});
       continue;
     }

From 229bdb15d9fb248b68e8f045fd1e7ba8a02c6b48 Mon Sep 17 00:00:00 2001
From: Tobias Looker <tobias.looker@mattr.global>
Date: Tue, 18 May 2021 11:46:58 +1200
Subject: [PATCH 05/16] feat: add support for @base being './'

---
 lib/context.js |  2 +-
 lib/expand.js  |  1 -
 tests/misc.js  | 20 ++++++++++++++++++++
 3 files changed, 21 insertions(+), 2 deletions(-)

diff --git a/lib/context.js b/lib/context.js
index 0ad282ef..d3a613a1 100644
--- a/lib/context.js
+++ b/lib/context.js
@@ -1050,7 +1050,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
   if(relativeTo.base && '@base' in activeCtx) {
     if(activeCtx['@base']) {
       // The null case preserves value as potentially relative
-      return prependBase(prependBase(options.base, activeCtx['@base']), value);
+      value = prependBase(prependBase(options.base, activeCtx['@base']), value);
     }
   } else if(relativeTo.base) {
     value = prependBase(options.base, value);
diff --git a/lib/expand.js b/lib/expand.js
index 6c852294..f0236e30 100644
--- a/lib/expand.js
+++ b/lib/expand.js
@@ -528,7 +528,6 @@ async function _expandObject({
         ]));
       }
       _validateTypeValue(value, options.isFrame);
-
       _addValue(
         expandedParent, '@type',
         _asArray(value).map(v =>
diff --git a/tests/misc.js b/tests/misc.js
index bc8c7aac..72509fe7 100644
--- a/tests/misc.js
+++ b/tests/misc.js
@@ -680,4 +680,24 @@ describe('expansionMap', () => {
 
     assert.equal(expansionMapCalled, true);
   });
+
+  it("should be called on relative iri when @base value is './'", async () => {
+    const docWithRelativeIriId = {
+      '@context': {
+        "@base": "./",
+      },
+      '@id': "absoluteiri",
+    };
+
+    let expansionMapCalled = false;
+    const expansionMap = info => {
+      if(info.relativeIri === '/absoluteiri') {
+        expansionMapCalled = true;
+      }
+    };
+
+    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+    assert.equal(expansionMapCalled, true);
+  });
 });

From 72bb834f9419361f1141726c9b607fbda17eec43 Mon Sep 17 00:00:00 2001
From: Tobias Looker <tobias.looker@mattr.global>
Date: Thu, 20 May 2021 13:52:12 +1200
Subject: [PATCH 06/16] fix: add support for when @vocab results in relative
 iri

---
 lib/context.js |  5 +++--
 lib/expand.js  |  4 ++++
 tests/misc.js  | 44 ++++++++++++++++++++++++++++++++++++++++++--
 3 files changed, 49 insertions(+), 4 deletions(-)

diff --git a/lib/context.js b/lib/context.js
index d3a613a1..60db67ea 100644
--- a/lib/context.js
+++ b/lib/context.js
@@ -1043,11 +1043,11 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
 
   // prepend vocab
   if(relativeTo.vocab && '@vocab' in activeCtx) {
-    return activeCtx['@vocab'] + value;
+    value = activeCtx['@vocab'] + value;
   }
 
   // prepend base
-  if(relativeTo.base && '@base' in activeCtx) {
+  else if(relativeTo.base && '@base' in activeCtx) {
     if(activeCtx['@base']) {
       // The null case preserves value as potentially relative
       value = prependBase(prependBase(options.base, activeCtx['@base']), value);
@@ -1061,6 +1061,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
     const expandedResult = options.expansionMap({
       relativeIri: value,
       activeCtx,
+      options
     });
     if(expandedResult !== undefined) {
       value = expandedResult;
diff --git a/lib/expand.js b/lib/expand.js
index f0236e30..6201661e 100644
--- a/lib/expand.js
+++ b/lib/expand.js
@@ -75,6 +75,10 @@ api.expand = async ({
   typeScopedContext = null,
   expansionMap = () => undefined
 }) => {
+
+  // Add expansion map to the processing options
+  options = { ...options, expansionMap };
+
   // nothing to expand
   if(element === null || element === undefined) {
     return null;
diff --git a/tests/misc.js b/tests/misc.js
index 72509fe7..73aca8ff 100644
--- a/tests/misc.js
+++ b/tests/misc.js
@@ -686,12 +686,52 @@ describe('expansionMap', () => {
       '@context': {
         "@base": "./",
       },
-      '@id': "absoluteiri",
+      '@id': "relativeiri",
+    };
+
+    let expansionMapCalled = false;
+    const expansionMap = info => {
+      if(info.relativeIri === '/relativeiri') {
+        expansionMapCalled = true;
+      }
+    };
+
+    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+    assert.equal(expansionMapCalled, true);
+  });
+
+  it("should be called on relative iri when @base value is './'", async () => {
+    const docWithRelativeIriId = {
+      '@context': {
+        "@base": "./",
+      },
+      '@id': "relativeiri",
+    };
+
+    let expansionMapCalled = false;
+    const expansionMap = info => {
+      if(info.relativeIri === '/relativeiri') {
+        expansionMapCalled = true;
+      }
+    };
+
+    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+    assert.equal(expansionMapCalled, true);
+  });
+
+  it("should be called on relative iri when @vocab value is './'", async () => {
+    const docWithRelativeIriId = {
+      '@context': {
+        "@vocab": "./",
+      },
+      '@type': "relativeiri",
     };
 
     let expansionMapCalled = false;
     const expansionMap = info => {
-      if(info.relativeIri === '/absoluteiri') {
+      if(info.relativeIri === '/relativeiri') {
         expansionMapCalled = true;
       }
     };

From bc55956800ec57c141049f853793edcffee3c5e8 Mon Sep 17 00:00:00 2001
From: Tobias Looker <tplooker@gmail.com>
Date: Fri, 21 May 2021 05:05:47 +1200
Subject: [PATCH 07/16] Update lib/expand.js

Co-authored-by: Dave Longley <dlongley@digitalbazaar.com>
---
 lib/expand.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/expand.js b/lib/expand.js
index 6201661e..518f1428 100644
--- a/lib/expand.js
+++ b/lib/expand.js
@@ -76,7 +76,7 @@ api.expand = async ({
   expansionMap = () => undefined
 }) => {
 
-  // Add expansion map to the processing options
+  // add expansion map to the processing options
   options = { ...options, expansionMap };
 
   // nothing to expand

From 1910df919a664e623b8f78eb602cf7a6390b350a Mon Sep 17 00:00:00 2001
From: Tobias Looker <tplooker@gmail.com>
Date: Fri, 21 May 2021 05:06:14 +1200
Subject: [PATCH 08/16] Update lib/expand.js

Co-authored-by: Dave Longley <dlongley@digitalbazaar.com>
---
 lib/expand.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/expand.js b/lib/expand.js
index 518f1428..62e41e59 100644
--- a/lib/expand.js
+++ b/lib/expand.js
@@ -424,7 +424,7 @@ async function _expandObject({
   const nests = [];
   let unexpandedValue;
 
-  // Add expansion map to the processing options
+  // add expansion map to the processing options
   options = {...options, expansionMap};
 
   // Figure out if this is the type for a JSON literal

From 24c1e30a2038cb9f10b8a335a6320fda9b72b5cb Mon Sep 17 00:00:00 2001
From: Tobias Looker <tplooker@gmail.com>
Date: Fri, 21 May 2021 05:06:24 +1200
Subject: [PATCH 09/16] Update tests/misc.js

Co-authored-by: Dave Longley <dlongley@digitalbazaar.com>
---
 tests/misc.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/misc.js b/tests/misc.js
index 73aca8ff..f98f0abd 100644
--- a/tests/misc.js
+++ b/tests/misc.js
@@ -480,7 +480,7 @@ describe('literal JSON', () => {
 });
 
 describe('expansionMap', () => {
-  it('should be called on un-mapped term', async () => {
+  it('should be called on unmapped term', async () => {
     const docWithUnMappedTerm = {
       '@context': {
         'definedTerm': 'https://example.com#definedTerm'

From 560b72941a1b1143ee92e86184e704a361b353c3 Mon Sep 17 00:00:00 2001
From: Tobias Looker <tplooker@gmail.com>
Date: Fri, 21 May 2021 05:06:32 +1200
Subject: [PATCH 10/16] Update tests/misc.js

Co-authored-by: Dave Longley <dlongley@digitalbazaar.com>
---
 tests/misc.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/misc.js b/tests/misc.js
index f98f0abd..39b44cfa 100644
--- a/tests/misc.js
+++ b/tests/misc.js
@@ -501,7 +501,7 @@ describe('expansionMap', () => {
     assert.equal(expansionMapCalled, true);
   });
 
-  it('should be called on nested un-mapped term', async () => {
+  it('should be called on nested unmapped term', async () => {
     const docWithUnMappedTerm = {
       '@context': {
         'definedTerm': 'https://example.com#definedTerm'

From 6045c7a63b5c574056bbe0344f855778c23f731d Mon Sep 17 00:00:00 2001
From: Tobias Looker <tobias.looker@mattr.global>
Date: Fri, 21 May 2021 05:58:16 +1200
Subject: [PATCH 11/16] fix: style nit

---
 lib/context.js | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/lib/context.js b/lib/context.js
index 60db67ea..308c4445 100644
--- a/lib/context.js
+++ b/lib/context.js
@@ -1041,13 +1041,11 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
     }
   }
 
-  // prepend vocab
   if(relativeTo.vocab && '@vocab' in activeCtx) {
+    // prepend vocab
     value = activeCtx['@vocab'] + value;
-  }
-
-  // prepend base
-  else if(relativeTo.base && '@base' in activeCtx) {
+  } else if(relativeTo.base && '@base' in activeCtx) {
+    // prepend base
     if(activeCtx['@base']) {
       // The null case preserves value as potentially relative
       value = prependBase(prependBase(options.base, activeCtx['@base']), value);

From 5c3dad0d36829d4b484e7dafc73b95ec8d7c1b7c Mon Sep 17 00:00:00 2001
From: Tobias Looker <tobias.looker@mattr.global>
Date: Fri, 21 May 2021 10:05:35 +1200
Subject: [PATCH 12/16] feat: add prependIri expansionMap hook and tests

---
 lib/context.js |  88 ++++++-
 lib/expand.js  |   2 +-
 tests/misc.js  | 609 ++++++++++++++++++++++++++++++++-----------------
 3 files changed, 486 insertions(+), 213 deletions(-)

diff --git a/lib/context.js b/lib/context.js
index 308c4445..f6fdd85e 100644
--- a/lib/context.js
+++ b/lib/context.js
@@ -1043,18 +1043,96 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
 
   if(relativeTo.vocab && '@vocab' in activeCtx) {
     // prepend vocab
-    value = activeCtx['@vocab'] + value;
+    const prependedResult = activeCtx['@vocab'] + value;
+    let expansionMapResult = undefined;
+    if(options && options.expansionMap) {
+      // if we are about to expand the value by prepending
+      // @vocab then call the expansion map to inform
+      // interested callers that this is occurring
+
+      // TODO: use `await` to support async
+      expansionMapResult = options.expansionMap({
+        prependedIri: {
+          type: '@vocab',
+          vocab: activeCtx['@vocab'],
+          value,
+          result: prependedResult
+        },
+        activeCtx,
+        options
+      });
+
+    }
+    if(expansionMapResult !== undefined) {
+      value = expansionMapResult;
+    } else {
+      // the null case preserves value as potentially relative
+      value = prependedResult;
+    }
   } else if(relativeTo.base && '@base' in activeCtx) {
     // prepend base
     if(activeCtx['@base']) {
-      // The null case preserves value as potentially relative
-      value = prependBase(prependBase(options.base, activeCtx['@base']), value);
+      const prependedResult = prependBase(
+        prependBase(options.base, activeCtx['@base']), value);
+
+      let expansionMapResult = undefined;
+      if(options && options.expansionMap) {
+        // if we are about to expand the value by prepending
+        // @base then call the expansion map to inform
+        // interested callers that this is occurring
+
+        // TODO: use `await` to support async
+        expansionMapResult = options.expansionMap({
+          prependedIri: {
+            type: '@base',
+            base: activeCtx['@base'],
+            value,
+            result: prependedResult
+          },
+          activeCtx,
+          options
+        });
+      }
+      if(expansionMapResult !== undefined) {
+        value = expansionMapResult;
+      } else {
+        // the null case preserves value as potentially relative
+        value = prependedResult;
+      }
     }
   } else if(relativeTo.base) {
-    value = prependBase(options.base, value);
+    const prependedResult = prependBase(options.base, value);
+    let expansionMapResult = undefined;
+    if(options && options.expansionMap) {
+      // if we are about to expand the value by prepending
+      // @base then call the expansion map to inform
+      // interested callers that this is occurring
+
+      // TODO: use `await` to support async
+      expansionMapResult = options.expansionMap({
+        prependedIri: {
+          type: '@base',
+          base: options.base,
+          value,
+          result: prependedResult
+        },
+        activeCtx,
+        options
+      });
+    }
+    if(expansionMapResult !== undefined) {
+      value = expansionMapResult;
+    } else {
+      value = prependedResult;
+    }
   }
 
-  if(!_isAbsoluteIri(value) && options.expansionMap) {
+  if(!_isAbsoluteIri(value) && options && options.expansionMap) {
+    // if the result of the expansion is not an absolute iri then
+    // call the expansion map to inform interested callers that
+    // the resulting value is a relative iri, which can result in
+    // it being dropped when converting to other RDF representations
+
     // TODO: use `await` to support async
     const expandedResult = options.expansionMap({
       relativeIri: value,
diff --git a/lib/expand.js b/lib/expand.js
index 62e41e59..0cf50746 100644
--- a/lib/expand.js
+++ b/lib/expand.js
@@ -77,7 +77,7 @@ api.expand = async ({
 }) => {
 
   // add expansion map to the processing options
-  options = { ...options, expansionMap };
+  options = {...options, expansionMap};
 
   // nothing to expand
   if(element === null || element === undefined) {
diff --git a/tests/misc.js b/tests/misc.js
index 39b44cfa..4ac9578e 100644
--- a/tests/misc.js
+++ b/tests/misc.js
@@ -480,264 +480,459 @@ describe('literal JSON', () => {
 });
 
 describe('expansionMap', () => {
-  it('should be called on unmapped term', async () => {
-    const docWithUnMappedTerm = {
-      '@context': {
-        'definedTerm': 'https://example.com#definedTerm'
-      },
-      definedTerm: "is defined",
-      testUndefined: "is undefined"
-    };
+  describe('unmappedProperty', () => {
+    it('should be called on unmapped term', async () => {
+      const docWithUnMappedTerm = {
+        '@context': {
+          'definedTerm': 'https://example.com#definedTerm'
+        },
+        definedTerm: "is defined",
+        testUndefined: "is undefined"
+      };
 
-    let expansionMapCalled = false;
-    const expansionMap = info => {
-      if(info.unmappedProperty === 'testUndefined') {
-        expansionMapCalled = true;
-      }
-    };
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.unmappedProperty === 'testUndefined') {
+          expansionMapCalled = true;
+        }
+      };
 
-    await jsonld.expand(docWithUnMappedTerm, {expansionMap});
+      await jsonld.expand(docWithUnMappedTerm, {expansionMap});
 
-    assert.equal(expansionMapCalled, true);
-  });
+      assert.equal(expansionMapCalled, true);
+    });
 
-  it('should be called on nested unmapped term', async () => {
-    const docWithUnMappedTerm = {
-      '@context': {
-        'definedTerm': 'https://example.com#definedTerm'
-      },
-      definedTerm: {
-        testUndefined: "is undefined"
-      }
-    };
+    it('should be called on nested unmapped term', async () => {
+      const docWithUnMappedTerm = {
+        '@context': {
+          'definedTerm': 'https://example.com#definedTerm'
+        },
+        definedTerm: {
+          testUndefined: "is undefined"
+        }
+      };
 
-    let expansionMapCalled = false;
-    const expansionMap = info => {
-      if(info.unmappedProperty === 'testUndefined') {
-        expansionMapCalled = true;
-      }
-    };
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.unmappedProperty === 'testUndefined') {
+          expansionMapCalled = true;
+        }
+      };
 
-    await jsonld.expand(docWithUnMappedTerm, {expansionMap});
+      await jsonld.expand(docWithUnMappedTerm, {expansionMap});
 
-    assert.equal(expansionMapCalled, true);
+      assert.equal(expansionMapCalled, true);
+    });
   });
 
-  it('should be called on relative iri for id term', async () => {
-    const docWithRelativeIriId = {
-      '@context': {
-        'definedTerm': 'https://example.com#definedTerm'
-      },
-      '@id': "relativeiri",
-      definedTerm: "is defined"
-    };
+  describe('relativeIri', () => {
+    it('should be called on relative iri for id term', async () => {
+      const docWithRelativeIriId = {
+        '@context': {
+          'definedTerm': 'https://example.com#definedTerm'
+        },
+        '@id': "relativeiri",
+        definedTerm: "is defined"
+      };
 
-    let expansionMapCalled = false;
-    const expansionMap = info => {
-      if(info.relativeIri === 'relativeiri') {
-        expansionMapCalled = true;
-      }
-    };
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.relativeIri === 'relativeiri') {
+          expansionMapCalled = true;
+        }
+      };
 
-    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+      await jsonld.expand(docWithRelativeIriId, {expansionMap});
 
-    assert.equal(expansionMapCalled, true);
-  });
+      assert.equal(expansionMapCalled, true);
+    });
 
-  it('should be called on relative iri for id term (nested)', async () => {
-    const docWithRelativeIriId = {
-      '@context': {
-        'definedTerm': 'https://example.com#definedTerm'
-      },
-      '@id': "urn:absoluteIri",
-      definedTerm: {
-        '@id': "relativeiri"
-      }
-    };
+    it('should be called on relative iri for id term (nested)', async () => {
+      const docWithRelativeIriId = {
+        '@context': {
+          'definedTerm': 'https://example.com#definedTerm'
+        },
+        '@id': "urn:absoluteIri",
+        definedTerm: {
+          '@id': "relativeiri"
+        }
+      };
 
-    let expansionMapCalled = false;
-    const expansionMap = info => {
-      if(info.relativeIri === 'relativeiri') {
-        expansionMapCalled = true;
-      }
-    };
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.relativeIri === 'relativeiri') {
+          expansionMapCalled = true;
+        }
+      };
 
-    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+      await jsonld.expand(docWithRelativeIriId, {expansionMap});
 
-    assert.equal(expansionMapCalled, true);
-  });
+      assert.equal(expansionMapCalled, true);
+    });
 
-  it('should be called on relative iri for aliased id term', async () => {
-    const docWithRelativeIriId = {
-      '@context': {
-        'id': '@id',
-        'definedTerm': 'https://example.com#definedTerm'
-      },
-      'id': "relativeiri",
-      definedTerm: "is defined"
-    };
+    it('should be called on relative iri for aliased id term', async () => {
+      const docWithRelativeIriId = {
+        '@context': {
+          'id': '@id',
+          'definedTerm': 'https://example.com#definedTerm'
+        },
+        'id': "relativeiri",
+        definedTerm: "is defined"
+      };
 
-    let expansionMapCalled = false;
-    const expansionMap = info => {
-      if(info.relativeIri === 'relativeiri') {
-        expansionMapCalled = true;
-      }
-    };
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.relativeIri === 'relativeiri') {
+          expansionMapCalled = true;
+        }
+      };
 
-    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+      await jsonld.expand(docWithRelativeIriId, {expansionMap});
 
-    assert.equal(expansionMapCalled, true);
-  });
+      assert.equal(expansionMapCalled, true);
+    });
 
-  it('should be called on relative iri for type term', async () => {
-    const docWithRelativeIriId = {
-      '@context': {
-        'definedTerm': 'https://example.com#definedTerm'
-      },
-      'id': "urn:absoluteiri",
-      '@type': "relativeiri",
-      definedTerm: "is defined"
-    };
+    it('should be called on relative iri for type term', async () => {
+      const docWithRelativeIriId = {
+        '@context': {
+          'definedTerm': 'https://example.com#definedTerm'
+        },
+        'id': "urn:absoluteiri",
+        '@type': "relativeiri",
+        definedTerm: "is defined"
+      };
 
-    let expansionMapCalled = false;
-    const expansionMap = info => {
-      if(info.relativeIri === 'relativeiri') {
-        expansionMapCalled = true;
-      }
-    };
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.relativeIri === 'relativeiri') {
+          expansionMapCalled = true;
+        }
+      };
 
-    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+      await jsonld.expand(docWithRelativeIriId, {expansionMap});
 
-    assert.equal(expansionMapCalled, true);
-  });
+      assert.equal(expansionMapCalled, true);
+    });
 
-  it('should be called on relative iri for \
-  type term with multiple relative iri types', async () => {
-    const docWithRelativeIriId = {
-      '@context': {
-        'definedTerm': 'https://example.com#definedTerm'
-      },
-      'id': "urn:absoluteiri",
-      '@type': ["relativeiri", "anotherRelativeiri" ],
-      definedTerm: "is defined"
-    };
+    it('should be called on relative iri for \
+    type term with multiple relative iri types', async () => {
+      const docWithRelativeIriId = {
+        '@context': {
+          'definedTerm': 'https://example.com#definedTerm'
+        },
+        'id': "urn:absoluteiri",
+        '@type': ["relativeiri", "anotherRelativeiri" ],
+        definedTerm: "is defined"
+      };
 
-    let expansionMapCalledTimes = 0;
-    const expansionMap = info => {
-      if(info.relativeIri === 'relativeiri' ||
-         info.relativeIri === 'anotherRelativeiri') {
-        expansionMapCalledTimes++;
-      }
-    };
+      let expansionMapCalledTimes = 0;
+      const expansionMap = info => {
+        if(info.relativeIri === 'relativeiri' ||
+           info.relativeIri === 'anotherRelativeiri') {
+          expansionMapCalledTimes++;
+        }
+      };
 
-    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+      await jsonld.expand(docWithRelativeIriId, {expansionMap});
 
-    assert.equal(expansionMapCalledTimes, 3);
-  });
+      assert.equal(expansionMapCalledTimes, 3);
+    });
 
-  it('should be called on relative iri for \
-  type term with multiple types', async () => {
-    const docWithRelativeIriId = {
-      '@context': {
-        'definedTerm': 'https://example.com#definedTerm'
-      },
-      'id': "urn:absoluteiri",
-      '@type': ["relativeiri", "definedTerm" ],
-      definedTerm: "is defined"
-    };
+    it('should be called on relative iri for \
+    type term with multiple types', async () => {
+      const docWithRelativeIriId = {
+        '@context': {
+          'definedTerm': 'https://example.com#definedTerm'
+        },
+        'id': "urn:absoluteiri",
+        '@type': ["relativeiri", "definedTerm" ],
+        definedTerm: "is defined"
+      };
 
-    let expansionMapCalled = false;
-    const expansionMap = info => {
-      if(info.relativeIri === 'relativeiri') {
-        expansionMapCalled = true;
-      }
-    };
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.relativeIri === 'relativeiri') {
+          expansionMapCalled = true;
+        }
+      };
 
-    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+      await jsonld.expand(docWithRelativeIriId, {expansionMap});
 
-    assert.equal(expansionMapCalled, true);
-  });
+      assert.equal(expansionMapCalled, true);
+    });
 
-  it('should be called on relative iri for aliased type term', async () => {
-    const docWithRelativeIriId = {
-      '@context': {
-        'type': "@type",
-        'definedTerm': 'https://example.com#definedTerm'
-      },
-      'id': "urn:absoluteiri",
-      'type': "relativeiri",
-      definedTerm: "is defined"
-    };
+    it('should be called on relative iri for aliased type term', async () => {
+      const docWithRelativeIriId = {
+        '@context': {
+          'type': "@type",
+          'definedTerm': 'https://example.com#definedTerm'
+        },
+        'id': "urn:absoluteiri",
+        'type': "relativeiri",
+        definedTerm: "is defined"
+      };
 
-    let expansionMapCalled = false;
-    const expansionMap = info => {
-      if(info.relativeIri === 'relativeiri') {
-        expansionMapCalled = true;
-      }
-    };
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.relativeIri === 'relativeiri') {
+          expansionMapCalled = true;
+        }
+      };
+
+      await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+      assert.equal(expansionMapCalled, true);
+    });
+
+    it("should be called on relative iri when \
+    @base value is './'", async () => {
+      const docWithRelativeIriId = {
+        '@context': {
+          "@base": "./",
+        },
+        '@id': "relativeiri",
+      };
+
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.relativeIri === '/relativeiri') {
+          expansionMapCalled = true;
+        }
+      };
 
-    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+      await jsonld.expand(docWithRelativeIriId, {expansionMap});
 
-    assert.equal(expansionMapCalled, true);
+      assert.equal(expansionMapCalled, true);
+    });
+
+    it("should be called on relative iri when \
+    @base value is './'", async () => {
+      const docWithRelativeIriId = {
+        '@context': {
+          "@base": "./",
+        },
+        '@id': "relativeiri",
+      };
+
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.relativeIri === '/relativeiri') {
+          expansionMapCalled = true;
+        }
+      };
+
+      await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+      assert.equal(expansionMapCalled, true);
+    });
+
+    it("should be called on relative iri when \
+    @vocab value is './'", async () => {
+      const docWithRelativeIriId = {
+        '@context': {
+          "@vocab": "./",
+        },
+        '@type': "relativeiri",
+      };
+
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.relativeIri === '/relativeiri') {
+          expansionMapCalled = true;
+        }
+      };
+
+      await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+      assert.equal(expansionMapCalled, true);
+    });
   });
 
-  it("should be called on relative iri when @base value is './'", async () => {
-    const docWithRelativeIriId = {
-      '@context': {
-        "@base": "./",
-      },
-      '@id': "relativeiri",
-    };
+  describe('prependedIri', () => {
+    it("should be called when property is \
+    being expanded with `@vocab`", async () => {
+      const doc = {
+        '@context': {
+          "@vocab": "http://example.com/",
+        },
+        'term': "termValue",
+      };
 
-    let expansionMapCalled = false;
-    const expansionMap = info => {
-      if(info.relativeIri === '/relativeiri') {
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        assert.deepStrictEqual(info.prependedIri, {
+          type: '@vocab',
+          vocab: 'http://example.com/',
+          value: 'term',
+          result: 'http://example.com/term'
+        });
         expansionMapCalled = true;
-      }
-    };
+      };
 
-    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+      await jsonld.expand(doc, {expansionMap});
 
-    assert.equal(expansionMapCalled, true);
-  });
+      assert.equal(expansionMapCalled, true);
+    });
 
-  it("should be called on relative iri when @base value is './'", async () => {
-    const docWithRelativeIriId = {
-      '@context': {
-        "@base": "./",
-      },
-      '@id': "relativeiri",
-    };
+    it("should be called when '@type' is \
+    being expanded with `@vocab`", async () => {
+      const doc = {
+        '@context': {
+          "@vocab": "http://example.com/",
+        },
+        '@type': "relativeIri",
+      };
 
-    let expansionMapCalled = false;
-    const expansionMap = info => {
-      if(info.relativeIri === '/relativeiri') {
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        assert.deepStrictEqual(info.prependedIri, {
+          type: '@vocab',
+          vocab: 'http://example.com/',
+          value: 'relativeIri',
+          result: 'http://example.com/relativeIri'
+        });
         expansionMapCalled = true;
-      }
-    };
+      };
 
-    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+      await jsonld.expand(doc, {expansionMap});
 
-    assert.equal(expansionMapCalled, true);
-  });
+      assert.equal(expansionMapCalled, true);
+    });
 
-  it("should be called on relative iri when @vocab value is './'", async () => {
-    const docWithRelativeIriId = {
-      '@context': {
-        "@vocab": "./",
-      },
-      '@type': "relativeiri",
-    };
+    it("should be called when aliased '@type' is \
+    being expanded with `@vocab`", async () => {
+      const doc = {
+        '@context': {
+          "@vocab": "http://example.com/",
+          "type": "@type"
+        },
+        'type': "relativeIri",
+      };
 
-    let expansionMapCalled = false;
-    const expansionMap = info => {
-      if(info.relativeIri === '/relativeiri') {
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        assert.deepStrictEqual(info.prependedIri, {
+          type: '@vocab',
+          vocab: 'http://example.com/',
+          value: 'relativeIri',
+          result: 'http://example.com/relativeIri'
+        });
         expansionMapCalled = true;
-      }
-    };
+      };
+
+      await jsonld.expand(doc, {expansionMap});
 
-    await jsonld.expand(docWithRelativeIriId, {expansionMap});
+      assert.equal(expansionMapCalled, true);
+    });
+
+    it("should be called when '@id' is being \
+    expanded with `@base`", async () => {
+      const doc = {
+        '@context': {
+          "@base": "http://example.com/",
+        },
+        '@id': "relativeIri",
+      };
+
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.prependedIri) {
+          assert.deepStrictEqual(info.prependedIri, {
+            type: '@base',
+            base: 'http://example.com/',
+            value: 'relativeIri',
+            result: 'http://example.com/relativeIri'
+          });
+          expansionMapCalled = true;
+        }
+      };
 
-    assert.equal(expansionMapCalled, true);
+      await jsonld.expand(doc, {expansionMap});
+
+      assert.equal(expansionMapCalled, true);
+    });
+
+    it("should be called when aliased '@id' \
+    is being expanded with `@base`", async () => {
+      const doc = {
+        '@context': {
+          "@base": "http://example.com/",
+          "id": "@id"
+        },
+        'id': "relativeIri",
+      };
+
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.prependedIri) {
+          assert.deepStrictEqual(info.prependedIri, {
+            type: '@base',
+            base: 'http://example.com/',
+            value: 'relativeIri',
+            result: 'http://example.com/relativeIri'
+          });
+          expansionMapCalled = true;
+        }
+      };
+
+      await jsonld.expand(doc, {expansionMap});
+
+      assert.equal(expansionMapCalled, true);
+    });
+
+    it("should be called when '@type' is \
+    being expanded with `@base`", async () => {
+      const doc = {
+        '@context': {
+          "@base": "http://example.com/",
+        },
+        '@type': "relativeIri",
+      };
+
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.prependedIri) {
+          assert.deepStrictEqual(info.prependedIri, {
+            type: '@base',
+            base: 'http://example.com/',
+            value: 'relativeIri',
+            result: 'http://example.com/relativeIri'
+          });
+          expansionMapCalled = true;
+        }
+      };
+
+      await jsonld.expand(doc, {expansionMap});
+
+      assert.equal(expansionMapCalled, true);
+    });
+
+    it("should be called when aliased '@type' is \
+    being expanded with `@base`", async () => {
+      const doc = {
+        '@context': {
+          "@base": "http://example.com/",
+          "type": "@type"
+        },
+        'type': "relativeIri",
+      };
+
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.prependedIri) {
+          assert.deepStrictEqual(info.prependedIri, {
+            type: '@base',
+            base: 'http://example.com/',
+            value: 'relativeIri',
+            result: 'http://example.com/relativeIri'
+          });
+          expansionMapCalled = true;
+        }
+      };
+
+      await jsonld.expand(doc, {expansionMap});
+
+      assert.equal(expansionMapCalled, true);
+    });
   });
 });

From fd132f5b2bd1ee8945c4355fb11449620ba8057b Mon Sep 17 00:00:00 2001
From: Tobias Looker <tobias.looker@mattr.global>
Date: Wed, 26 May 2021 10:37:24 +1200
Subject: [PATCH 13/16] fix: refactor @base expansionLogic

---
 lib/context.js | 49 +++++++++++++++----------------------------------
 1 file changed, 15 insertions(+), 34 deletions(-)

diff --git a/lib/context.js b/lib/context.js
index f6fdd85e..9e559c09 100644
--- a/lib/context.js
+++ b/lib/context.js
@@ -1069,42 +1069,22 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
       // the null case preserves value as potentially relative
       value = prependedResult;
     }
-  } else if(relativeTo.base && '@base' in activeCtx) {
+  } else if(relativeTo.base) {
     // prepend base
-    if(activeCtx['@base']) {
-      const prependedResult = prependBase(
-        prependBase(options.base, activeCtx['@base']), value);
-
-      let expansionMapResult = undefined;
-      if(options && options.expansionMap) {
-        // if we are about to expand the value by prepending
-        // @base then call the expansion map to inform
-        // interested callers that this is occurring
-
-        // TODO: use `await` to support async
-        expansionMapResult = options.expansionMap({
-          prependedIri: {
-            type: '@base',
-            base: activeCtx['@base'],
-            value,
-            result: prependedResult
-          },
-          activeCtx,
-          options
-        });
-      }
-      if(expansionMapResult !== undefined) {
-        value = expansionMapResult;
-      } else {
-        // the null case preserves value as potentially relative
-        value = prependedResult;
+    let prependedResult;
+    let expansionMapResult;
+    let base;
+    if('@base' in activeCtx) {
+      if(activeCtx['@base']) {
+        base = prependBase(options.base, activeCtx['@base']);
+        prependedResult = prependBase(base, value);
       }
+    } else {
+      base = options.base;
+      prependedResult = prependBase(options.base, value);
     }
-  } else if(relativeTo.base) {
-    const prependedResult = prependBase(options.base, value);
-    let expansionMapResult = undefined;
     if(options && options.expansionMap) {
-      // if we are about to expand the value by prepending
+      // if we are about to expand the value by pre-pending
       // @base then call the expansion map to inform
       // interested callers that this is occurring
 
@@ -1112,7 +1092,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
       expansionMapResult = options.expansionMap({
         prependedIri: {
           type: '@base',
-          base: options.base,
+          base,
           value,
           result: prependedResult
         },
@@ -1122,7 +1102,8 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
     }
     if(expansionMapResult !== undefined) {
       value = expansionMapResult;
-    } else {
+    } else if(prependedResult !== undefined) {
+      // the null case preserves value as potentially relative
       value = prependedResult;
     }
   }

From f0b704528150c4fca49b86536bd8fedd59b710cb Mon Sep 17 00:00:00 2001
From: Tobias Looker <tobias.looker@mattr.global>
Date: Wed, 9 Jun 2021 12:06:47 +1200
Subject: [PATCH 14/16] fix: add hook for typeExpansion, cover @base null

---
 lib/context.js | 20 ++++++++++++---
 lib/expand.js  |  8 +++---
 tests/misc.js  | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 90 insertions(+), 7 deletions(-)

diff --git a/lib/context.js b/lib/context.js
index 9e559c09..cc2aefa1 100644
--- a/lib/context.js
+++ b/lib/context.js
@@ -1041,6 +1041,14 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
     }
   }
 
+  // A flag that captures whether the iri being expanded is
+  // the value for an @type
+  let typeExpansion = false;
+
+  if (options !== undefined && options.typeExpansion !== undefined) {
+    typeExpansion = options.typeExpansion;
+  }
+
   if(relativeTo.vocab && '@vocab' in activeCtx) {
     // prepend vocab
     const prependedResult = activeCtx['@vocab'] + value;
@@ -1056,7 +1064,8 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
           type: '@vocab',
           vocab: activeCtx['@vocab'],
           value,
-          result: prependedResult
+          result: prependedResult,
+          typeExpansion,
         },
         activeCtx,
         options
@@ -1078,6 +1087,9 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
       if(activeCtx['@base']) {
         base = prependBase(options.base, activeCtx['@base']);
         prependedResult = prependBase(base, value);
+      } else {
+        base = activeCtx['@base'];
+        prependedResult = value;
       }
     } else {
       base = options.base;
@@ -1094,7 +1106,8 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
           type: '@base',
           base,
           value,
-          result: prependedResult
+          result: prependedResult,
+          typeExpansion,
         },
         activeCtx,
         options
@@ -1102,7 +1115,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
     }
     if(expansionMapResult !== undefined) {
       value = expansionMapResult;
-    } else if(prependedResult !== undefined) {
+    } else {
       // the null case preserves value as potentially relative
       value = prependedResult;
     }
@@ -1118,6 +1131,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
     const expandedResult = options.expansionMap({
       relativeIri: value,
       activeCtx,
+      typeExpansion,
       options
     });
     if(expandedResult !== undefined) {
diff --git a/lib/expand.js b/lib/expand.js
index 0cf50746..5503555b 100644
--- a/lib/expand.js
+++ b/lib/expand.js
@@ -431,7 +431,7 @@ async function _expandObject({
   const isJsonType = element[typeKey] &&
     _expandIri(activeCtx,
       (_isArray(element[typeKey]) ? element[typeKey][0] : element[typeKey]),
-      {vocab: true}, options) === '@json';
+      {vocab: true}, { ...options, typeExpansion: true }) === '@json';
 
   for(const key of keys) {
     let value = element[key];
@@ -527,7 +527,7 @@ async function _expandObject({
         value = Object.fromEntries(Object.entries(value).map(([k, v]) => [
           _expandIri(typeScopedContext, k, {vocab: true}),
           _asArray(v).map(vv =>
-            _expandIri(typeScopedContext, vv, {base: true, vocab: true})
+            _expandIri(typeScopedContext, vv, {base: true, vocab: true}, { ...options, typeExpansion: true })
           )
         ]));
       }
@@ -537,7 +537,7 @@ async function _expandObject({
         _asArray(value).map(v =>
           _isString(v) ?
             _expandIri(typeScopedContext, v,
-              {base: true, vocab: true}, options) : v),
+              {base: true, vocab: true}, { ...options, typeExpansion: true }) : v),
         {propertyIsArray: options.isFrame});
       continue;
     }
@@ -937,7 +937,7 @@ function _expandValue({activeCtx, activeProperty, value, options}) {
   if(expandedProperty === '@id') {
     return _expandIri(activeCtx, value, {base: true}, options);
   } else if(expandedProperty === '@type') {
-    return _expandIri(activeCtx, value, {vocab: true, base: true}, options);
+    return _expandIri(activeCtx, value, {vocab: true, base: true}, { ...options, typeExpansion: true });
   }
 
   // get type definition from context
diff --git a/tests/misc.js b/tests/misc.js
index 4ac9578e..aace2768 100644
--- a/tests/misc.js
+++ b/tests/misc.js
@@ -614,6 +614,36 @@ describe('expansionMap', () => {
       assert.equal(expansionMapCalled, true);
     });
 
+    it('should be called on relative iri for type term in scoped context', async () => {
+      const docWithRelativeIriId = {
+        '@context': {
+          'definedType': {
+            '@id': 'https://example.com#definedType',
+            '@context': {
+              'definedTerm': 'https://example.com#definedTerm'
+
+            }
+          }
+        },
+        'id': "urn:absoluteiri",
+        '@type': "definedType",
+        definedTerm: {
+          '@type': 'relativeiri'
+        }
+      };
+
+      let expansionMapCalled = false;
+      const expansionMap = info => {
+        if(info.relativeIri === 'relativeiri') {
+          expansionMapCalled = true;
+        }
+      };
+
+      await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+      assert.equal(expansionMapCalled, true);
+    });
+
     it('should be called on relative iri for \
     type term with multiple relative iri types', async () => {
       const docWithRelativeIriId = {
@@ -638,6 +668,38 @@ describe('expansionMap', () => {
       assert.equal(expansionMapCalledTimes, 3);
     });
 
+    it('should be called on relative iri for \
+    type term with multiple relative iri types in scoped context', async () => {
+      const docWithRelativeIriId = {
+        '@context': {
+          'definedType': {
+            '@id': 'https://example.com#definedType',
+            '@context': {
+              'definedTerm': 'https://example.com#definedTerm'
+
+            }
+          }
+        },
+        'id': "urn:absoluteiri",
+        '@type': "definedType",
+        definedTerm: {
+          '@type': ["relativeiri", "anotherRelativeiri" ]
+        }
+      };
+
+      let expansionMapCalledTimes = 0;
+      const expansionMap = info => {
+        if(info.relativeIri === 'relativeiri' ||
+           info.relativeIri === 'anotherRelativeiri') {
+          expansionMapCalledTimes++;
+        }
+      };
+
+      await jsonld.expand(docWithRelativeIriId, {expansionMap});
+
+      assert.equal(expansionMapCalledTimes, 3);
+    });
+
     it('should be called on relative iri for \
     type term with multiple types', async () => {
       const docWithRelativeIriId = {
@@ -764,6 +826,7 @@ describe('expansionMap', () => {
           type: '@vocab',
           vocab: 'http://example.com/',
           value: 'term',
+          typeExpansion: false,
           result: 'http://example.com/term'
         });
         expansionMapCalled = true;
@@ -789,6 +852,7 @@ describe('expansionMap', () => {
           type: '@vocab',
           vocab: 'http://example.com/',
           value: 'relativeIri',
+          typeExpansion: true,
           result: 'http://example.com/relativeIri'
         });
         expansionMapCalled = true;
@@ -815,6 +879,7 @@ describe('expansionMap', () => {
           type: '@vocab',
           vocab: 'http://example.com/',
           value: 'relativeIri',
+          typeExpansion: true,
           result: 'http://example.com/relativeIri'
         });
         expansionMapCalled = true;
@@ -841,6 +906,7 @@ describe('expansionMap', () => {
             type: '@base',
             base: 'http://example.com/',
             value: 'relativeIri',
+            typeExpansion: false,
             result: 'http://example.com/relativeIri'
           });
           expansionMapCalled = true;
@@ -869,6 +935,7 @@ describe('expansionMap', () => {
             type: '@base',
             base: 'http://example.com/',
             value: 'relativeIri',
+            typeExpansion: false,
             result: 'http://example.com/relativeIri'
           });
           expansionMapCalled = true;
@@ -896,6 +963,7 @@ describe('expansionMap', () => {
             type: '@base',
             base: 'http://example.com/',
             value: 'relativeIri',
+            typeExpansion: true,
             result: 'http://example.com/relativeIri'
           });
           expansionMapCalled = true;
@@ -924,6 +992,7 @@ describe('expansionMap', () => {
             type: '@base',
             base: 'http://example.com/',
             value: 'relativeIri',
+            typeExpansion: true,
             result: 'http://example.com/relativeIri'
           });
           expansionMapCalled = true;

From 57ae8a66207a9f5f04bf9028d5a9ddf27feee416 Mon Sep 17 00:00:00 2001
From: Tobias Looker <tplooker@gmail.com>
Date: Wed, 9 Jun 2021 13:35:16 +1200
Subject: [PATCH 15/16] Update lib/context.js

Co-authored-by: Dave Longley <dlongley@digitalbazaar.com>
---
 lib/context.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/context.js b/lib/context.js
index cc2aefa1..5f0de789 100644
--- a/lib/context.js
+++ b/lib/context.js
@@ -1045,7 +1045,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) {
   // the value for an @type
   let typeExpansion = false;
 
-  if (options !== undefined && options.typeExpansion !== undefined) {
+  if(options !== undefined && options.typeExpansion !== undefined) {
     typeExpansion = options.typeExpansion;
   }
 

From 38144732a63c34ba2130ced4646f71e4a2f1822a Mon Sep 17 00:00:00 2001
From: Tobias Looker <tobias.looker@mattr.global>
Date: Wed, 9 Jun 2021 13:38:15 +1200
Subject: [PATCH 16/16] fix: minor linting issues

---
 lib/expand.js | 11 +++++++----
 tests/misc.js |  3 ++-
 2 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/lib/expand.js b/lib/expand.js
index 5503555b..737def7b 100644
--- a/lib/expand.js
+++ b/lib/expand.js
@@ -431,7 +431,7 @@ async function _expandObject({
   const isJsonType = element[typeKey] &&
     _expandIri(activeCtx,
       (_isArray(element[typeKey]) ? element[typeKey][0] : element[typeKey]),
-      {vocab: true}, { ...options, typeExpansion: true }) === '@json';
+      {vocab: true}, {...options, typeExpansion: true}) === '@json';
 
   for(const key of keys) {
     let value = element[key];
@@ -527,7 +527,8 @@ async function _expandObject({
         value = Object.fromEntries(Object.entries(value).map(([k, v]) => [
           _expandIri(typeScopedContext, k, {vocab: true}),
           _asArray(v).map(vv =>
-            _expandIri(typeScopedContext, vv, {base: true, vocab: true}, { ...options, typeExpansion: true })
+            _expandIri(typeScopedContext, vv, {base: true, vocab: true},
+              {...options, typeExpansion: true})
           )
         ]));
       }
@@ -537,7 +538,8 @@ async function _expandObject({
         _asArray(value).map(v =>
           _isString(v) ?
             _expandIri(typeScopedContext, v,
-              {base: true, vocab: true}, { ...options, typeExpansion: true }) : v),
+              {base: true, vocab: true},
+              {...options, typeExpansion: true}) : v),
         {propertyIsArray: options.isFrame});
       continue;
     }
@@ -937,7 +939,8 @@ function _expandValue({activeCtx, activeProperty, value, options}) {
   if(expandedProperty === '@id') {
     return _expandIri(activeCtx, value, {base: true}, options);
   } else if(expandedProperty === '@type') {
-    return _expandIri(activeCtx, value, {vocab: true, base: true}, { ...options, typeExpansion: true });
+    return _expandIri(activeCtx, value, {vocab: true, base: true},
+      {...options, typeExpansion: true});
   }
 
   // get type definition from context
diff --git a/tests/misc.js b/tests/misc.js
index aace2768..d9dae4fc 100644
--- a/tests/misc.js
+++ b/tests/misc.js
@@ -614,7 +614,8 @@ describe('expansionMap', () => {
       assert.equal(expansionMapCalled, true);
     });
 
-    it('should be called on relative iri for type term in scoped context', async () => {
+    it('should be called on relative iri for type\
+     term in scoped context', async () => {
       const docWithRelativeIriId = {
         '@context': {
           'definedType': {