From 2117c3b2264e2d8cf2eccf39a19e6aa962fb81f4 Mon Sep 17 00:00:00 2001
From: "David I. Lehn" <dlehn@digitalbazaar.com>
Date: Fri, 17 Nov 2023 21:13:57 -0500
Subject: [PATCH 1/4] Drop support for Node.js < 18.

---
 .github/workflows/main.yml | 20 ++++++++++----------
 CHANGELOG.md               |  5 +++++
 package.json               |  2 +-
 3 files changed, 16 insertions(+), 11 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 08332857..b0e6f903 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -8,9 +8,9 @@ jobs:
     timeout-minutes: 10
     strategy:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Use Node.js ${{ matrix.node-version }}
       uses: actions/setup-node@v3
       with:
@@ -23,9 +23,9 @@ jobs:
     timeout-minutes: 10
     strategy:
       matrix:
-        node-version: [14.x, 16.x, 18.x, 20.x]
+        node-version: [18.x, 20.x]
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Use Node.js ${{ matrix.node-version }}
       uses: actions/setup-node@v3
       with:
@@ -41,10 +41,10 @@ jobs:
     timeout-minutes: 10
     strategy:
       matrix:
-        node-version: [16.x]
+        node-version: [20.x]
         bundler: [webpack, browserify]
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Use Node.js ${{ matrix.node-version }}
       uses: actions/setup-node@v3
       with:
@@ -62,9 +62,9 @@ jobs:
     timeout-minutes: 10
     strategy:
       matrix:
-        node-version: [16.x]
+        node-version: [20.x]
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Use Node.js ${{ matrix.node-version }}
       uses: actions/setup-node@v3
       with:
@@ -78,9 +78,9 @@ jobs:
     timeout-minutes: 10
     strategy:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Use Node.js ${{ matrix.node-version }}
       uses: actions/setup-node@v3
       with:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c0926a8..f9838c2c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # jsonld ChangeLog
 
+## 9.0.0 - 2023-xx-xx
+
+### Changed
+- **BREAKING**: Drop support for Node.js < 18.
+
 ## 8.3.2 - 2023-12-06
 
 ### Fixed
diff --git a/package.json b/package.json
index d1e84e37..474df3c0 100644
--- a/package.json
+++ b/package.json
@@ -79,7 +79,7 @@
     "webpack-merge": "^5.8.0"
   },
   "engines": {
-    "node": ">=14"
+    "node": ">=18"
   },
   "keywords": [
     "JSON",

From 20fa0cf0e0e734b5f281dc8026afd3df04cc457f Mon Sep 17 00:00:00 2001
From: "David I. Lehn" <dlehn@digitalbazaar.com>
Date: Fri, 17 Nov 2023 22:52:10 -0500
Subject: [PATCH 2/4] Update to `rdf-canonize@4`.

- **BREAKING**: See the `rdf-canonize` 4.0.0 changelog for **important**
  changes and upgrade notes.
- Update to handle different RDF/JS dataset `BlankNode` format.
- Enable pass through of numerous possible `rdf-canonize` options in a
  `canonize()` `canonizeOptions` parameter.
- Update to use `rdf-canonize` options.
- The `URDNA2015` default algorithm has been changed to `RDFC-1.0` from
  `rdf-canon`.
- Complexity control defaults `maxWorkFactor` or `maxDeepIterations` may
  need to be adjusted to process graphs with certain blank node
  constructs.
- A `signal` option is available to use an `AbortSignal` to limit
  resource usage.
- The internal digest algorithm can be changed.
---
 CHANGELOG.md   |  10 ++
 lib/fromRdf.js |  43 +++++--
 lib/jsonld.js  |  46 ++++---
 lib/toRdf.js   |  66 ++++++----
 package.json   |   2 +-
 tests/misc.js  |   2 +-
 tests/test.js  | 339 +++++++++++++++++++++++++++++++------------------
 7 files changed, 330 insertions(+), 178 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f9838c2c..68289422 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,16 @@
 
 ### Changed
 - **BREAKING**: Drop support for Node.js < 18.
+- **BREAKING**: Upgrade dependencies.
+  - `rdf-canonize@4`: See the [rdf-canonize][] 4.0.0 changelog for
+    **important** changes and upgrade notes. Of note:
+    - The `URDNA2015` default algorithm has been changed to `RDFC-1.0` from
+      [rdf-canon][].
+    - Complexity control defaults `maxWorkFactor` or `maxDeepIterations` may
+      need to be adjusted to process graphs with certain blank node constructs.
+    - A `signal` option is available to use an `AbortSignal` to limit resource
+      usage.
+    - The internal digest algorithm can be changed.
 
 ## 8.3.2 - 2023-12-06
 
diff --git a/lib/fromRdf.js b/lib/fromRdf.js
index 01098353..795ec9af 100644
--- a/lib/fromRdf.js
+++ b/lib/fromRdf.js
@@ -89,7 +89,7 @@ api.fromRDF = async (
     const nodeMap = graphMap[name];
 
     // get subject, predicate, object
-    const s = quad.subject.value;
+    const s = _nodeId(quad.subject);
     const p = quad.predicate.value;
     const o = quad.object;
 
@@ -98,13 +98,14 @@ api.fromRDF = async (
     }
     const node = nodeMap[s];
 
-    const objectIsNode = o.termType.endsWith('Node');
-    if(objectIsNode && !(o.value in nodeMap)) {
-      nodeMap[o.value] = {'@id': o.value};
+    const objectNodeId = _nodeId(o);
+    const objectIsNode = !!objectNodeId;
+    if(objectIsNode && !(objectNodeId in nodeMap)) {
+      nodeMap[objectNodeId] = {'@id': objectNodeId};
     }
 
     if(p === RDF_TYPE && !useRdfType && objectIsNode) {
-      _addValue(node, '@type', o.value, {propertyIsArray: true});
+      _addValue(node, '@type', objectNodeId, {propertyIsArray: true});
       continue;
     }
 
@@ -114,9 +115,9 @@ api.fromRDF = async (
     // object may be an RDF list/partial list node but we can't know easily
     // until all triples are read
     if(objectIsNode) {
-      if(o.value === RDF_NIL) {
+      if(objectNodeId === RDF_NIL) {
         // track rdf:nil uniquely per graph
-        const object = nodeMap[o.value];
+        const object = nodeMap[objectNodeId];
         if(!('usages' in object)) {
           object.usages = [];
         }
@@ -125,12 +126,12 @@ api.fromRDF = async (
           property: p,
           value
         });
-      } else if(o.value in referencedOnce) {
+      } else if(objectNodeId in referencedOnce) {
         // object referenced more than once
-        referencedOnce[o.value] = false;
+        referencedOnce[objectNodeId] = false;
       } else {
         // keep track of single reference
-        referencedOnce[o.value] = {
+        referencedOnce[objectNodeId] = {
           node,
           property: p,
           value
@@ -303,8 +304,9 @@ api.fromRDF = async (
  */
 function _RDFToObject(o, useNativeTypes, rdfDirection, options) {
   // convert NamedNode/BlankNode object to JSON-LD
-  if(o.termType.endsWith('Node')) {
-    return {'@id': o.value};
+  const nodeId = _nodeId(o);
+  if(nodeId) {
+    return {'@id': nodeId};
   }
 
   // convert literal to JSON-LD
@@ -397,3 +399,20 @@ function _RDFToObject(o, useNativeTypes, rdfDirection, options) {
 
   return rval;
 }
+
+/**
+ * Return id for a term. Handles BlankNodes and NamedNodes. Adds a '_:' prefix
+ * for BlanksNodes.
+ *
+ * @param term a term object.
+ *
+ * @return the Node term id or null.
+ */
+function _nodeId(term) {
+  if(term.termType === 'NamedNode') {
+    return term.value;
+  } else if(term.termType === 'BlankNode') {
+    return '_:' + term.value;
+  }
+  return null;
+}
diff --git a/lib/jsonld.js b/lib/jsonld.js
index c6931aeb..a2a563bb 100644
--- a/lib/jsonld.js
+++ b/lib/jsonld.js
@@ -523,7 +523,7 @@ jsonld.link = async function(input, ctx, options) {
 /**
  * Performs RDF dataset normalization on the given input. The input is JSON-LD
  * unless the 'inputFormat' option is used. The output is an RDF dataset
- * unless the 'format' option is used.
+ * unless a non-null 'format' option is used.
  *
  * Note: Canonicalization sets `safe` to `true` and `base` to `null` by
  * default in order to produce safe outputs and "fail closed" by default. This
@@ -531,25 +531,31 @@ jsonld.link = async function(input, ctx, options) {
  * allow unsafe defaults (for cryptographic usage) in order to comply with the
  * JSON-LD 1.1 specification.
  *
- * @param input the input to normalize as JSON-LD or as a format specified by
- *          the 'inputFormat' option.
+ * @param input the input to normalize as JSON-LD given as an RDF dataset or as
+ *          a format specified by the 'inputFormat' option.
  * @param [options] the options to use:
- *          [algorithm] the normalization algorithm to use, `URDNA2015` or
- *            `URGNA2012` (default: `URDNA2015`).
  *          [base] the base IRI to use (default: `null`).
  *          [expandContext] a context to expand with.
  *          [skipExpansion] true to assume the input is expanded and skip
  *            expansion, false not to, defaults to false. Some well-formed
  *            and safe-mode checks may be omitted.
- *          [inputFormat] the format if input is not JSON-LD:
- *            'application/n-quads' for N-Quads.
- *          [format] the format if output is a string:
- *            'application/n-quads' for N-Quads.
+ *          [inputFormat] the input format. null for a JSON-LD object,
+ *            'application/n-quads' for N-Quads. (default: null)
+ *          [format] the output format. null for an RDF dataset,
+ *            'application/n-quads' for an N-Quads string. (default: N-Quads)
  *          [documentLoader(url, options)] the document loader.
- *          [useNative] true to use a native canonize algorithm
  *          [rdfDirection] null or 'i18n-datatype' to support RDF
  *             transformation of @direction (default: null).
  *          [safe] true to use safe mode. (default: true).
+ *          [canonizeOptions] options to pass to rdf-canonize canonize(). See
+ *            rdf-canonize for more details. Commonly used options, and their
+ *            defaults, are:
+ *            algorithm="RDFC-1.0",
+ *            messageDigestAlgorithm="sha256",
+ *            canonicalIdMap,
+ *            maxWorkFactor=1,
+ *            maxDeepIterations=-1,
+ *            and signal=null.
  *          [contextResolver] internal use only.
  *
  * @return a Promise that resolves to the normalized output.
@@ -559,15 +565,19 @@ jsonld.normalize = jsonld.canonize = async function(input, options) {
     throw new TypeError('Could not canonize, too few arguments.');
   }
 
-  // set default options
+  // set toRDF options
   options = _setDefaults(options, {
-    base: _isString(input) ? input : null,
-    algorithm: 'URDNA2015',
     skipExpansion: false,
     safe: true,
     contextResolver: new ContextResolver(
       {sharedCache: _resolvedContextCache})
   });
+
+  // set canonize options
+  const canonizeOptions = Object.assign({}, {
+    algorithm: 'RDFC-1.0'
+  }, options.canonizeOptions || null);
+
   if('inputFormat' in options) {
     if(options.inputFormat !== 'application/n-quads' &&
       options.inputFormat !== 'application/nquads') {
@@ -579,17 +589,18 @@ jsonld.normalize = jsonld.canonize = async function(input, options) {
     const parsedInput = NQuads.parse(input);
 
     // do canonicalization
-    return canonize.canonize(parsedInput, options);
+    return canonize.canonize(parsedInput, canonizeOptions);
   }
 
   // convert to RDF dataset then do normalization
   const opts = {...options};
   delete opts.format;
+  delete opts.canonizeOptions;
   opts.produceGeneralizedRdf = false;
   const dataset = await jsonld.toRDF(input, opts);
 
   // do canonicalization
-  return canonize.canonize(dataset, options);
+  return canonize.canonize(dataset, canonizeOptions);
 };
 
 /**
@@ -653,8 +664,8 @@ jsonld.fromRDF = async function(dataset, options) {
  *          [skipExpansion] true to assume the input is expanded and skip
  *            expansion, false not to, defaults to false. Some well-formed
  *            and safe-mode checks may be omitted.
- *          [format] the format to use to output a string:
- *            'application/n-quads' for N-Quads.
+ *          [format] the output format. null for an RDF dataset,
+ *            'application/n-quads' for an N-Quads string. (default: null)
  *          [produceGeneralizedRdf] true to output generalized RDF, false
  *            to produce only standard RDF (default: false).
  *          [documentLoader(url, options)] the document loader.
@@ -672,7 +683,6 @@ jsonld.toRDF = async function(input, options) {
 
   // set default options
   options = _setDefaults(options, {
-    base: _isString(input) ? input : '',
     skipExpansion: false,
     contextResolver: new ContextResolver(
       {sharedCache: _resolvedContextCache})
diff --git a/lib/toRdf.js b/lib/toRdf.js
index 53f20af4..e8a54844 100644
--- a/lib/toRdf.js
+++ b/lib/toRdf.js
@@ -63,12 +63,7 @@ api.toRDF = (input, options) => {
     if(graphName === '@default') {
       graphTerm = {termType: 'DefaultGraph', value: ''};
     } else if(_isAbsoluteIri(graphName)) {
-      if(graphName.startsWith('_:')) {
-        graphTerm = {termType: 'BlankNode'};
-      } else {
-        graphTerm = {termType: 'NamedNode'};
-      }
-      graphTerm.value = graphName;
+      graphTerm = _makeTerm(graphName);
     } else {
       // skip relative IRIs (not valid RDF)
       if(options.eventHandler) {
@@ -119,10 +114,7 @@ function _graphToRDF(dataset, graph, graphTerm, issuer, options) {
 
       for(const item of items) {
         // RDF subject
-        const subject = {
-          termType: id.startsWith('_:') ? 'BlankNode' : 'NamedNode',
-          value: id
-        };
+        const subject = _makeTerm(id);
 
         // skip relative IRI subjects (not valid RDF)
         if(!_isAbsoluteIri(id)) {
@@ -144,10 +136,7 @@ function _graphToRDF(dataset, graph, graphTerm, issuer, options) {
         }
 
         // RDF predicate
-        const predicate = {
-          termType: property.startsWith('_:') ? 'BlankNode' : 'NamedNode',
-          value: property
-        };
+        const predicate = _makeTerm(property);
 
         // skip relative IRI predicates (not valid RDF)
         if(!_isAbsoluteIri(property)) {
@@ -226,13 +215,16 @@ function _listToRDF(list, issuer, dataset, graphTerm, rdfDirection, options) {
 
   const last = list.pop();
   // Result is the head of the list
-  const result = last ? {termType: 'BlankNode', value: issuer.getId()} : nil;
+  const result = last ? {
+    termType: 'BlankNode',
+    value: issuer.getId().slice(2)
+  } : nil;
   let subject = result;
 
   for(const item of list) {
     const object = _objectToRDF(
       item, issuer, dataset, graphTerm, rdfDirection, options);
-    const next = {termType: 'BlankNode', value: issuer.getId()};
+    const next = {termType: 'BlankNode', value: issuer.getId().slice(2)};
     dataset.push({
       subject,
       predicate: first,
@@ -284,14 +276,16 @@ function _listToRDF(list, issuer, dataset, graphTerm, rdfDirection, options) {
 function _objectToRDF(
   item, issuer, dataset, graphTerm, rdfDirection, options
 ) {
-  const object = {};
+  let object;
 
   // convert value object to RDF
   if(graphTypes.isValue(item)) {
-    object.termType = 'Literal';
-    object.value = undefined;
-    object.datatype = {
-      termType: 'NamedNode'
+    object = {
+      termType: 'Literal',
+      value: undefined,
+      datatype: {
+        termType: 'NamedNode'
+      }
     };
     let value = item['@value'];
     const datatype = item['@type'] || null;
@@ -374,13 +368,14 @@ function _objectToRDF(
   } else if(graphTypes.isList(item)) {
     const _list = _listToRDF(
       item['@list'], issuer, dataset, graphTerm, rdfDirection, options);
-    object.termType = _list.termType;
-    object.value = _list.value;
+    object = {
+      termType: _list.termType,
+      value: _list.value
+    };
   } else {
     // convert string/node object to RDF
     const id = types.isObject(item) ? item['@id'] : item;
-    object.termType = id.startsWith('_:') ? 'BlankNode' : 'NamedNode';
-    object.value = id;
+    object = _makeTerm(id);
   }
 
   // skip relative IRIs, not valid RDF
@@ -404,3 +399,24 @@ function _objectToRDF(
 
   return object;
 }
+
+/**
+ * Make a term from an id. Handles BlankNodes and NamedNodes based on a
+ * possible '_:' id prefix. The prefix is removed for BlankNodes.
+ *
+ * @param id a term id.
+ *
+ * @return a term object.
+ */
+function _makeTerm(id) {
+  if(id.startsWith('_:')) {
+    return {
+      termType: 'BlankNode',
+      value: id.slice(2)
+    };
+  }
+  return {
+    termType: 'NamedNode',
+    value: id
+  };
+}
diff --git a/package.json b/package.json
index 474df3c0..51285da0 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,7 @@
     "@digitalbazaar/http-client": "^3.4.1",
     "canonicalize": "^1.0.1",
     "lru-cache": "^6.0.0",
-    "rdf-canonize": "^3.4.0"
+    "rdf-canonize": "^4.0.1"
   },
   "devDependencies": {
     "@babel/core": "^7.21.8",
diff --git a/tests/misc.js b/tests/misc.js
index 908052cd..21b3d25e 100644
--- a/tests/misc.js
+++ b/tests/misc.js
@@ -4030,7 +4030,7 @@ _:b0 <ex:p> "v" .
 ]
 ;
       const nq = `\
-_:b0 <_:b1> "v" .
+_:b0 _:b1 "v" .
 `;
 
       await _test({
diff --git a/tests/test.js b/tests/test.js
index a0af0eba..e9aa9f51 100644
--- a/tests/test.js
+++ b/tests/test.js
@@ -349,38 +349,73 @@ const TEST_TYPES = {
     ],
     compare: compareCanonizedExpectedNQuads
   },
-  'rdfc:Urgna2012EvalTest': {
-    fn: 'normalize',
+  'rdfc:RDFC10EvalTest': {
+    skip: {
+      // NOTE: idRegex format:
+      // /manifest-urdna2015#testNNN$/,
+      // FIXME
+      idRegex: [
+        // Unsupported U escape
+        // /manifest-urdna2015#test060/
+      ]
+    },
+    fn: 'canonize',
     params: [
       readTestNQuads('action'),
       createTestOptions({
-        algorithm: 'URGNA2012',
+        algorithm: 'RDFC-1.0',
         inputFormat: 'application/n-quads',
         format: 'application/n-quads'
       })
     ],
     compare: compareExpectedNQuads
   },
-  'rdfc:Urdna2015EvalTest': {
+  'rdfc:RDFC10NegativeEvalTest': {
     skip: {
       // NOTE: idRegex format:
-      // /manifest-urdna2015#testNNN$/,
-      // FIXME
-      idRegex: [
-        // Unsupported U escape
-        /manifest-urdna2015#test060/
-      ]
+      // /manifest-rdfc10#testNNN$/,
+      idRegex: []
     },
     fn: 'canonize',
     params: [
       readTestNQuads('action'),
       createTestOptions({
-        algorithm: 'URDNA2015',
+        algorithm: 'RDFC-1.0',
+        inputFormat: 'application/n-quads',
+        format: 'application/n-quads'
+      })
+    ]
+  },
+  'rdfc:RDFC10MapTest': {
+    skip: {
+      // NOTE: idRegex format:
+      // /manifest-rdfc10#testNNN$/,
+      idRegex: []
+    },
+    fn: 'canonize',
+    params: [
+      readTestNQuads('action'),
+      createTestOptions({
+        algorithm: 'RDFC-1.0',
         inputFormat: 'application/n-quads',
         format: 'application/n-quads'
       })
     ],
-    compare: compareExpectedNQuads
+    preRunAdjustParams: ({params, extra}) => {
+      // add canonicalIdMap
+      const m = new Map();
+      extra.canonicalIdMap = m;
+      params[1].canonizeOptions = params[1].canonizeOptions || {};
+      params[1].canonizeOptions.canonicalIdMap = m;
+      return params;
+    },
+    postRunAdjustParams: ({params}) => {
+      // restore output param to empty map
+      const m = new Map();
+      params[1].canonizeOptions = params[1].canonizeOptions || {};
+      params[1].canonizeOptions.canonicalIdMap = m;
+    },
+    compare: compareExpectedCanonicalIdMap
   }
 };
 
@@ -429,8 +464,6 @@ if(options.earl && options.earl.filename) {
   }
 }
 
-return new Promise(resolve => {
-
 // async generated tests
 // _tests => [{suite}, ...]
 // suite => {
@@ -440,21 +473,20 @@ return new Promise(resolve => {
 // }
 const _tests = [];
 
-return addManifest(manifest, _tests)
-  .then(() => {
-    return _testsToMocha(_tests);
-  }).then(result => {
-    if(options.earl.report) {
-      describe('Writing EARL report to: ' + options.earl.filename, function() {
-        // print out EARL even if .only was used
-        const _it = result.hadOnly ? it.only : it;
-        _it('should print the earl report', function() {
-          return options.writeFile(
-            options.earl.filename, options.earl.report.reportJson());
-        });
-      });
-    }
-  }).then(() => resolve());
+await addManifest(manifest, _tests);
+const result = _testsToMocha(_tests);
+if(options.earl.report) {
+  describe('Writing EARL report to: ' + options.earl.filename, function() {
+    // print out EARL even if .only was used
+    const _it = result.hadOnly ? it.only : it;
+    _it('should print the earl report', function() {
+      return options.writeFile(
+        options.earl.filename, options.earl.report.reportJson());
+    });
+  });
+}
+
+return;
 
 // build mocha tests from local test structure
 function _testsToMocha(tests) {
@@ -485,89 +517,102 @@ function _testsToMocha(tests) {
   };
 }
 
-});
-
 /**
  * Adds the tests for all entries in the given manifest.
  *
- * @param manifest {Object} the manifest.
- * @param parent {Object} the parent test structure
- * @return {Promise}
+ * @param {object} manifest - The manifest.
+ * @param {object} parent - The parent test structure.
+ * @returns {Promise} - A promise with no value.
  */
-function addManifest(manifest, parent) {
-  return new Promise((resolve, reject) => {
-    // create test structure
-    const suite = {
-      title: manifest.name || manifest.label,
-      tests: [],
-      suites: [],
-      imports: []
-    };
-    parent.push(suite);
-
-    // get entries and sequence (alias for entries)
-    const entries = [].concat(
-      getJsonLdValues(manifest, 'entries'),
-      getJsonLdValues(manifest, 'sequence')
-    );
-
-    const includes = getJsonLdValues(manifest, 'include');
-    // add includes to sequence as jsonld files
-    for(let i = 0; i < includes.length; ++i) {
-      entries.push(includes[i] + '.jsonld');
+async function addManifest(manifest, parent) {
+  // create test structure
+  const suite = {
+    title: manifest.name || manifest.label,
+    tests: [],
+    suites: [],
+    imports: []
+  };
+  parent.push(suite);
+
+  // get entries and sequence (alias for entries)
+  const entries = [].concat(
+    getJsonLdValues(manifest, 'entries'),
+    getJsonLdValues(manifest, 'sequence')
+  );
+
+  const includes = getJsonLdValues(manifest, 'include');
+  // add includes to sequence as jsonld files
+  for(let i = 0; i < includes.length; ++i) {
+    entries.push(includes[i] + '.jsonld');
+  }
+
+  // resolve all entry promises and process
+  for await (const entry of await Promise.all(entries)) {
+    if(typeof entry === 'string' && entry.endsWith('js')) {
+      // process later as a plain JavaScript file
+      suite.imports.push(entry);
+      continue;
+    } else if(typeof entry === 'function') {
+      // process as a function that returns a promise
+      const childSuite = await entry(options);
+      if(suite) {
+        suite.suites.push(childSuite);
+      }
+      continue;
+    }
+    const manifestEntry = await readManifestEntry(manifest, entry);
+    if(isJsonLdType(manifestEntry, '__SKIP__')) {
+      // special local skip logic
+      suite.tests.push(manifestEntry);
+    } else if(isJsonLdType(manifestEntry, 'mf:Manifest')) {
+      // entry is another manifest
+      await addManifest(manifestEntry, suite.suites);
+    } else {
+      // assume entry is a test
+      await addTest(manifest, manifestEntry, suite.tests);
     }
+  }
+}
 
-    // resolve all entry promises and process
-    Promise.all(entries).then(entries => {
-      let p = Promise.resolve();
-      entries.forEach(entry => {
-        if(typeof entry === 'string' && entry.endsWith('js')) {
-          // process later as a plain JavaScript file
-          suite.imports.push(entry);
-          return;
-        } else if(typeof entry === 'function') {
-          // process as a function that returns a promise
-          p = p.then(() => {
-            return entry(options);
-          }).then(childSuite => {
-            if(suite) {
-              suite.suites.push(childSuite);
-            }
-          });
-          return;
-        }
-        p = p.then(() => {
-          return readManifestEntry(manifest, entry);
-        }).then(entry => {
-          if(isJsonLdType(entry, '__SKIP__')) {
-            // special local skip logic
-            suite.tests.push(entry);
-          } else if(isJsonLdType(entry, 'mf:Manifest')) {
-            // entry is another manifest
-            return addManifest(entry, suite.suites);
-          } else {
-            // assume entry is a test
-            return addTest(manifest, entry, suite.tests);
-          }
-        });
-      });
-      return p;
-    }).then(() => {
-      resolve();
-    }).catch(err => {
-      console.error(err);
-      reject(err);
-    });
-  });
+/**
+ * Common adjust params helper.
+ *
+ * @param {object} params - The param to adjust.
+ * @param {object} test - The test.
+ */
+function _commonAdjustParams(params, test) {
+  if(isJsonLdType(test, 'rdfc:RDFC10EvalTest') ||
+    isJsonLdType(test, 'rdfc:RDFC10MapTest') ||
+    isJsonLdType(test, 'rdfc:RDFC10NegativeEvalTest')) {
+    if(test.hashAlgorithm) {
+      params.canonizeOptions = params.canonizeOptions || {};
+      params.canonizeOptions.messageDigestAlgorithm = test.hashAlgorithm;
+    }
+    if(test.computationalComplexity === 'low') {
+      // simple test cases
+      params.canonizeOptions = params.canonizeOptions || {};
+      params.canonizeOptions.maxWorkFactor = 0;
+    }
+    if(test.computationalComplexity === 'medium') {
+      // tests between O(n) and O(n^2)
+      params.canonizeOptions = params.canonizeOptions || {};
+      params.canonizeOptions.maxWorkFactor = 2;
+    }
+    if(test.computationalComplexity === 'high') {
+      // poison tests between O(n^2) and O(n^3)
+      params.canonizeOptions = params.canonizeOptions || {};
+      params.canonizeOptions.maxWorkFactor = 3;
+    }
+  }
 }
 
 /**
  * Adds a test.
  *
- * @param manifest {Object} the manifest.
- * @param test {Object} the test.
- * @param tests {Array} the list of tests to add to.
- * @return {Promise}
+ * @param {object} manifest - The manifest.
+ * @param {object} test - The test.
+ * @param {Array} tests - The list of tests to add to.
+ * @returns {Promise} - A promise with no value.
  */
 async function addTest(manifest, test, tests) {
   // expand @id and input base
@@ -597,6 +642,10 @@ async function addTest(manifest, test, tests) {
       title: description + ` (jobs=${jobs})`,
       f: makeFn({
         test,
+        adjustParams: params => {
+          _commonAdjustParams(params[1], test);
+          return params;
+        },
         run: ({/*test, */testInfo, params}) => {
           // skip Promise.all
           if(jobs === 1 && fast1) {
@@ -770,7 +819,14 @@ function makeFn({
       });
     });
 
-    const params = adjustParams(testInfo.params.map(param => param(test)));
+    let params = testInfo.params.map(param => param(test));
+    const extra = {};
+    // type specific pre run adjustments
+    if(testInfo.preRunAdjustParams) {
+      params = testInfo.preRunAdjustParams({params, extra});
+    }
+    // general adjustments
+    params = adjustParams(params);
     // resolve test data
     const values = await Promise.all(params);
     // copy used to check inputs do not change
@@ -780,6 +836,10 @@ function makeFn({
     // run and capture errors and results
     try {
       result = await run({test, testInfo, params: values});
+      // type specific post run adjustments
+      if(testInfo.postRunAdjustParams) {
+        testInfo.postRunAdjustParams({params: values, extra});
+      }
       // check input not changed
       assert.deepStrictEqual(valuesOrig, values);
     } catch(e) {
@@ -789,21 +849,25 @@ function makeFn({
     try {
       if(isJsonLdType(test, 'jld:NegativeEvaluationTest')) {
         if(!isBenchmark) {
-          await compareExpectedError(test, err);
+          await compareExpectedError({test, err});
+        }
+      } else if(isJsonLdType(test, 'rdfc:RDFC10NegativeEvalTest')) {
+        if(!isBenchmark) {
+          await checkError({test, err});
         }
       } else if(isJsonLdType(test, 'jld:PositiveEvaluationTest') ||
-        isJsonLdType(test, 'rdfc:Urgna2012EvalTest') ||
-        isJsonLdType(test, 'rdfc:Urdna2015EvalTest')) {
+        isJsonLdType(test, 'rdfc:RDFC10EvalTest') ||
+        isJsonLdType(test, 'rdfc:RDFC10MapTest')) {
         if(err) {
           throw err;
         }
         if(!isBenchmark) {
-          await testInfo.compare(test, result);
+          await testInfo.compare({test, result, extra});
         }
       } else if(isJsonLdType(test, 'jld:PositiveSyntaxTest')) {
         // no checks
       } else {
-        throw Error('Unknown test type: ' + test.type);
+        throw new Error(`Unknown test type: "${test.type}"`);
       }
 
       let benchmarkResult = null;
@@ -1013,11 +1077,11 @@ function _getExpectProperty(test) {
   } else if('result' in test) {
     return 'result';
   } else {
-    throw Error('No expected output property found');
+    throw new Error('No expected output property found');
   }
 }
 
-async function compareExpectedJson(test, result) {
+async function compareExpectedJson({test, result}) {
   let expect;
   try {
     expect = await readTestJson(_getExpectProperty(test))(test);
@@ -1032,7 +1096,7 @@ async function compareExpectedJson(test, result) {
   }
 }
 
-async function compareExpectedNQuads(test, result) {
+async function compareExpectedNQuads({test, result}) {
   let expect;
   try {
     expect = await readTestNQuads(_getExpectProperty(test))(test);
@@ -1047,11 +1111,15 @@ async function compareExpectedNQuads(test, result) {
   }
 }
 
-async function compareCanonizedExpectedNQuads(test, result) {
+async function compareCanonizedExpectedNQuads({test, result}) {
   let expect;
   try {
     expect = await readTestNQuads(_getExpectProperty(test))(test);
-    const opts = {algorithm: 'URDNA2015'};
+    const opts = {
+      algorithm: 'RDFC-1.0',
+      // some tests need this: expand 0027 and 0062
+      maxWorkFactor: 2
+    };
     const expectDataset = rdfCanonize.NQuads.parse(expect);
     const expectCmp = await rdfCanonize.canonize(expectDataset, opts);
     const resultDataset = rdfCanonize.NQuads.parse(result);
@@ -1067,7 +1135,35 @@ async function compareCanonizedExpectedNQuads(test, result) {
   }
 }
 
-async function compareExpectedError(test, err) {
+async function compareExpectedCanonicalIdMap({test, result, extra}) {
+  let expect;
+  try {
+    expect = await readTestJson(_getExpectProperty(test))(test);
+    const expectMap = new Map(Object.entries(expect));
+    assert.deepStrictEqual(extra.canonicalIdMap, expectMap);
+  } catch(err) {
+    if(options.bailOnError) {
+      console.log('\nTEST FAILED\n');
+      console.log('EXPECTED:\n ' + JSON.stringify(expect, null, 2));
+      console.log('ACTUAL:\n' + JSON.stringify(result, null, 2));
+    }
+    throw err;
+  }
+}
+
+async function checkError({/*test,*/ err}) {
+  try {
+    assert.ok(err, 'no error present');
+  } catch(_err) {
+    if(options.bailOnError) {
+      console.log('\nTEST FAILED\n');
+      console.log('EXPECTED ERROR');
+    }
+    throw _err;
+  }
+}
+
+async function compareExpectedError({test, err}) {
   let expect;
   let result;
   try {
@@ -1088,10 +1184,7 @@ async function compareExpectedError(test, err) {
 }
 
 function isJsonLdType(node, type) {
-  const nodeType = [].concat(
-    getJsonLdValues(node, '@type'),
-    getJsonLdValues(node, 'type')
-  );
+  const nodeType = getJsonLdType(node);
   type = Array.isArray(type) ? type : [type];
   for(let i = 0; i < type.length; ++i) {
     if(nodeType.indexOf(type[i]) !== -1) {
@@ -1101,13 +1194,17 @@ function isJsonLdType(node, type) {
   return false;
 }
 
+function getJsonLdType(node) {
+  return [].concat(
+    getJsonLdValues(node, '@type'),
+    getJsonLdValues(node, 'type')
+  );
+}
+
 function getJsonLdValues(node, property) {
   let rval = [];
   if(property in node) {
-    rval = node[property];
-    if(!Array.isArray(rval)) {
-      rval = [rval];
-    }
+    rval = [].concat(node[property]);
   }
   return rval;
 }

From 2409091d7043e760dbb9c5d79e5f9041e0ca4934 Mon Sep 17 00:00:00 2001
From: "David I. Lehn" <dlehn@digitalbazaar.com>
Date: Fri, 17 Nov 2023 22:58:39 -0500
Subject: [PATCH 3/4] Remove support for `application/nquads` alias.

---
 CHANGELOG.md  |  3 +++
 lib/jsonld.js |  7 ++-----
 tests/misc.js | 31 ++++++++++++-------------------
 3 files changed, 17 insertions(+), 24 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68289422..e4db0bfb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,9 @@
       usage.
     - The internal digest algorithm can be changed.
 
+### Removed
+- **BREAKING**: Remove `application/nquads` alias for `application/n-quads`.
+
 ## 8.3.2 - 2023-12-06
 
 ### Fixed
diff --git a/lib/jsonld.js b/lib/jsonld.js
index a2a563bb..0a001b72 100644
--- a/lib/jsonld.js
+++ b/lib/jsonld.js
@@ -579,8 +579,7 @@ jsonld.normalize = jsonld.canonize = async function(input, options) {
   }, options.canonizeOptions || null);
 
   if('inputFormat' in options) {
-    if(options.inputFormat !== 'application/n-quads' &&
-      options.inputFormat !== 'application/nquads') {
+    if(options.inputFormat !== 'application/n-quads') {
       throw new JsonLdError(
         'Unknown canonicalization input format.',
         'jsonld.CanonizeError');
@@ -700,8 +699,7 @@ jsonld.toRDF = async function(input, options) {
   // output RDF dataset
   const dataset = _toRDF(expanded, options);
   if(options.format) {
-    if(options.format === 'application/n-quads' ||
-      options.format === 'application/nquads') {
+    if(options.format === 'application/n-quads') {
       return NQuads.serialize(dataset);
     }
     throw new JsonLdError(
@@ -1007,7 +1005,6 @@ jsonld.unregisterRDFParser = function(contentType) {
 
 // register the N-Quads RDF parser
 jsonld.registerRDFParser('application/n-quads', NQuads.parse);
-jsonld.registerRDFParser('application/nquads', NQuads.parse);
 
 /* URL API */
 jsonld.url = require('./url');
diff --git a/tests/misc.js b/tests/misc.js
index 21b3d25e..b9ebbbc3 100644
--- a/tests/misc.js
+++ b/tests/misc.js
@@ -143,19 +143,18 @@ describe('other toRDF tests', () => {
     });
   });
 
-  it('should handle deprecated N-Quads format', done => {
+  it('should fail for deprecated N-Quads format', done => {
     const doc = {
       "@id": "https://example.com/",
       "https://example.com/test": "test"
     };
     const p = jsonld.toRDF(doc, {format: 'application/nquads'});
     assert(p instanceof Promise);
-    p.catch(e => {
-      assert.ifError(e);
-    }).then(output => {
-      assert.equal(
-        output,
-        '<https://example.com/> <https://example.com/test> "test" .\n');
+    p.then(() => {
+      assert.fail();
+    }).catch(e => {
+      assert(e);
+      assert.equal(e.name, 'jsonld.UnknownFormat');
       done();
     });
   });
@@ -232,21 +231,15 @@ describe('other fromRDF tests', () => {
     });
   });
 
-  it('should handle deprecated N-Quads format', done => {
+  it('should fail for deprecated N-Quads format', done => {
     const nq = '<https://example.com/> <https://example.com/test> "test" .\n';
     const p = jsonld.fromRDF(nq, {format: 'application/nquads'});
     assert(p instanceof Promise);
-    p.catch(e => {
-      assert.ifError(e);
-    }).then(output => {
-      assert.deepEqual(
-        output,
-        [{
-          "@id": "https://example.com/",
-          "https://example.com/test": [{
-            "@value": "test"
-          }]
-        }]);
+    p.then(() => {
+      assert.fail();
+    }).catch(e => {
+      assert(e);
+      assert.equal(e.name, 'jsonld.UnknownFormat');
       done();
     });
   });

From 735795989a1d2e94964fe8361e199ced240b3fec Mon Sep 17 00:00:00 2001
From: "David I. Lehn" <dlehn@digitalbazaar.com>
Date: Fri, 17 Nov 2023 22:59:17 -0500
Subject: [PATCH 4/4] Update dependencies.

---
 CHANGELOG.md | 2 ++
 package.json | 4 ++--
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4db0bfb..11908faf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@
 ### Changed
 - **BREAKING**: Drop support for Node.js < 18.
 - **BREAKING**: Upgrade dependencies.
+  - `@digitalbazaar/http-client@4`.
+  - `canonicalize@2`.
   - `rdf-canonize@4`: See the [rdf-canonize][] 4.0.0 changelog for
     **important** changes and upgrade notes. Of note:
     - The `URDNA2015` default algorithm has been changed to `RDFC-1.0` from
diff --git a/package.json b/package.json
index 51285da0..488fc4b8 100644
--- a/package.json
+++ b/package.json
@@ -29,8 +29,8 @@
     "lib/**/*.js"
   ],
   "dependencies": {
-    "@digitalbazaar/http-client": "^3.4.1",
-    "canonicalize": "^1.0.1",
+    "@digitalbazaar/http-client": "^4.0.0",
+    "canonicalize": "^2.0.0",
     "lru-cache": "^6.0.0",
     "rdf-canonize": "^4.0.1"
   },