text: /**
   * A JavaScript implementation of the JSON-LD API.
   *
   * @author Dave Longley
   *
   * @license BSD 3-Clause License
   * Copyright (c) 2011-2017 Digital Bazaar, Inc.
   * All rights reserved.
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions are met:
   *
   * Redistributions of source code must retain the above copyright notice,
   * this list of conditions and the following disclaimer.
   *
   * Redistributions in binary form must reproduce the above copyright
   * notice, this list of conditions and the following disclaimer in the
   * documentation and/or other materials provided with the distribution.
   *
   * Neither the name of the Digital Bazaar, Inc. nor the names of its
   * contributors may be used to endorse or promote products derived from
   * this software without specific prior written permission.
   *
   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
   * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
   * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
   * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
   * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
   * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
   * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
   * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
   * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   */
  (function() {
  
  const canonize = require('rdf-canonize');
  const util = require('./util');
  const IdentifierIssuer = util.IdentifierIssuer;
  const JsonLdError = require('./JsonLdError');
  const NQuads = require('./NQuads');
  const Rdfa = require('./Rdfa');
  
  const {expand: _expand} = require('./expand');
  const {flatten: _flatten} = require('./flatten');
  const {fromRDF: _fromRDF} = require('./fromRdf');
  const {toRDF: _toRDF} = require('./toRdf');
  
  const {
    frameMergedOrDefault: _frameMergedOrDefault
  } = require('./frame');
  
  const {
    isArray: _isArray,
    isObject: _isObject,
    isString: _isString
  } = require('./types');
  
  const {
    isSubjectReference: _isSubjectReference,
  } = require('./graphTypes');
  
  const {
    getInitialContext: _getInitialContext,
    process: _processContext,
    getAllContexts: _getAllContexts,
    expandIri: _expandIri
  } = require('./context');
  
  const {
    compact: _compact,
    compactIri: _compactIri,
    removePreserve: _removePreserve
  } = require('./compact');
  
  const {
    createNodeMap: _createNodeMap,
    createMergedNodeMap: _createMergedNodeMap,
    mergeNodeMaps: _mergeNodeMaps
  } = require('./nodeMap');
  
  // determine if in-browser or using node.js
  const _nodejs = (
    typeof process !== 'undefined' && process.versions && process.versions.node);
  const _browser = !_nodejs &&
    (typeof window !== 'undefined' || typeof self !== 'undefined');
  
  // attaches jsonld API to the given object
  const wrapper = function(jsonld) {
  
  /* Core API */
  
  /**
   * Performs JSON-LD compaction.
   *
   * @param input the JSON-LD input to compact.
   * @param ctx the context to compact with.
   * @param [options] options to use:
   *          [base] the base IRI to use.
   *          [compactArrays] true to compact arrays to single values when
   *            appropriate, false not to (default: true).
   *          [compactToRelative] true to compact IRIs to be relative to document base,
   *            false to keep absolute (default: true)
   *          [graph] true to always output a top-level graph (default: false).
   *          [expandContext] a context to expand with.
   *          [skipExpansion] true to assume the input is expanded and skip
   *            expansion, false not to, defaults to false.
   *          [documentLoader(url, callback(err, remoteDoc))] the document loader.
   *          [expansionMap(info)] a function that can be used to custom map
   *            unmappable values (or to throw an error when they are detected);
   *            if this function returns `undefined` then the default behavior
   *            will be used.
   *          [framing] true if compaction is occuring during a framing operation.
   *          [compactionMap(info)] a function that can be used to custom map
   *            unmappable values (or to throw an error when they are detected);
   *            if this function returns `undefined` then the default behavior
   *            will be used.
   * @param [callback(err, compacted)] called once the operation completes.
   *
   * @return a Promise that resolves to the compacted output.
   */
  jsonld.compact = util.callbackify(async function(input, ctx, options) {
    if(arguments.length < 2) {
      throw new TypeError('Could not compact, too few arguments.');
    }
  
    if(ctx === null) {
      throw new JsonLdError(
        'The compaction context must not be null.',
        'jsonld.CompactError', {code: 'invalid local context'});
    }
  
    // nothing to compact
    if(input === null) {
      return null;
    }
  
    // set default options
    options = _setDefaults(options, {
      base: _isString(input) ? input : '',
      compactArrays: true,
      compactToRelative: true,
      graph: false,
      skipExpansion: false,
      link: false,
      issuer: new IdentifierIssuer('_:b')
    });
    if(options.link) {
      // force skip expansion when linking, "link" is not part of the public
      // API, it should only be called from framing
      options.skipExpansion = true;
    }
    if(!options.compactToRelative) {
      delete options.base;
    }
  
    // expand input
    let expanded;
    if(options.skipExpansion) {
      expanded = input;
    } else {
      expanded = await jsonld.expand(input, options);
    }
  
    // process context
    const activeCtx = await jsonld.processContext(
      _getInitialContext(options), ctx, options);
  
    // do compaction
    let compacted = _compact({
      activeCtx,
      element: expanded,
      options,
      compactionMap: options.compactionMap
    });
  
    // perform clean up
    if(options.compactArrays && !options.graph && _isArray(compacted)) {
      if(compacted.length === 1) {
        // simplify to a single item
        compacted = compacted[0];
      } else if(compacted.length === 0) {
        // simplify to an empty object
        compacted = {};
      }
    } else if(options.graph && _isObject(compacted)) {
      // always use array if graph option is on
      compacted = [compacted];
    }
  
    // follow @context key
    if(_isObject(ctx) && '@context' in ctx) {
      ctx = ctx['@context'];
    }
  
    // build output context
    ctx = util.clone(ctx);
    if(!_isArray(ctx)) {
      ctx = [ctx];
    }
    // remove empty contexts
    const tmp = ctx;
    ctx = [];
    for(let i = 0; i < tmp.length; ++i) {
      if(!_isObject(tmp[i]) || Object.keys(tmp[i]).length > 0) {
        ctx.push(tmp[i]);
      }
    }
  
    // remove array if only one context
    const hasContext = (ctx.length > 0);
    if(ctx.length === 1) {
      ctx = ctx[0];
    }
  
    // add context and/or @graph
    if(_isArray(compacted)) {
      // use '@graph' keyword
      const graphAlias = _compactIri({activeCtx, iri: '@graph', relativeTo: {vocab: true}});
      const graph = compacted;
      compacted = {};
      if(hasContext) {
        compacted['@context'] = ctx;
      }
      compacted[graphAlias] = graph;
    } else if(_isObject(compacted) && hasContext) {
      // reorder keys so @context is first
      const graph = compacted;
      compacted = {'@context': ctx};
      for(let key in graph) {
        compacted[key] = graph[key];
      }
    }
  
    if(options.framing) {
      // get graph alias
      const graph = _compactIri({activeCtx, iri: '@graph', relativeTo: {vocab: true}});
      // remove @preserve from results
      options.link = {};
      compacted[graph] = _removePreserve(activeCtx, compacted[graph], options);
    }
  
    return compacted;
  });
  
  /**
   * Performs JSON-LD expansion.
   *
   * @param input the JSON-LD input to expand.
   * @param [options] the options to use:
   *          [base] the base IRI to use.
   *          [expandContext] a context to expand with.
   *          [keepFreeFloatingNodes] true to keep free-floating nodes,
   *            false not to, defaults to false.
   *          [documentLoader(url, callback(err, remoteDoc))] the document loader.
   *          [expansionMap(info)] a function that can be used to custom map
   *            unmappable values (or to throw an error when they are detected);
   *            if this function returns `undefined` then the default behavior
   *            will be used.
   * @param [callback(err, expanded)] called once the operation completes.
   *
   * @return a Promise that resolves to the expanded output.
   */
  jsonld.expand = util.callbackify(async function(input, options) {
    if(arguments.length < 1) {
      throw new TypeError('Could not expand, too few arguments.');
    }
  
    // set default options
    options = _setDefaults(options, {
      keepFreeFloatingNodes: false
    });
    if(options.expansionMap === false) {
      options.expansionMap = undefined;
    }
  
    // build set of objects that may have @contexts to resolve
    const toResolve = {};
  
    // build set of contexts to process prior to expansion
    const contextsToProcess = [];
  
    // if an `expandContext` has been given ensure it gets resolved
    if('expandContext' in options) {
      const expandContext = util.clone(options.expandContext);
      if(_isObject(expandContext) && '@context' in expandContext) {
        toResolve.expandContext = expandContext;
      } else {
        toResolve.expandContext = {'@context': expandContext};
      }
      contextsToProcess.push(toResolve.expandContext);
    }
  
    // if input is a string, attempt to dereference remote document
    let defaultBase;
    if(!_isString(input)) {
      // input is not a URL, do not need to retrieve it first
      toResolve.input = util.clone(input);
    } else {
      // load remote doc
      const remoteDoc = await jsonld.get(input, options);
      defaultBase = remoteDoc.documentUrl;
      toResolve.input = remoteDoc.document;
      if(remoteDoc.contextUrl) {
        // context included in HTTP link header and must be resolved
        toResolve.remoteContext = {'@context': remoteDoc.contextUrl};
        contextsToProcess.push(toResolve.remoteContext);
      }
    }
  
    // set default base
    if(!('base' in options)) {
      options.base = defaultBase || '';
    }
  
    // get all contexts in `toResolve`
    await _getAllContexts(toResolve, options);
  
    // process any additional contexts
    let activeCtx = _getInitialContext(options);
    contextsToProcess.forEach(localCtx => {
      activeCtx = _processContext({activeCtx, localCtx, options});
    });
  
    // expand resolved input
    let expanded = _expand({
      activeCtx,
      element: toResolve.input,
      options,
      expansionMap: options.expansionMap
    });
  
    // optimize away @graph with no other properties
    if(_isObject(expanded) && ('@graph' in expanded) &&
      Object.keys(expanded).length === 1) {
      expanded = expanded['@graph'];
    } else if(expanded === null) {
      expanded = [];
    }
  
    // normalize to an array
    if(!_isArray(expanded)) {
      expanded = [expanded];
    }
  
    return expanded;
  });
  
  /**
   * Performs JSON-LD flattening.
   *
   * @param input the JSON-LD to flatten.
   * @param ctx the context to use to compact the flattened output, or null.
   * @param [options] the options to use:
   *          [base] the base IRI to use.
   *          [expandContext] a context to expand with.
   *          [documentLoader(url, callback(err, remoteDoc))] the document loader.
   * @param [callback(err, flattened)] called once the operation completes.
   *
   * @return a Promise that resolves to the flattened output.
   */
  jsonld.flatten = util.callbackify(async function(input, ctx, options) {
    if(arguments.length < 1) {
      return new TypeError('Could not flatten, too few arguments.');
    }
  
    if(typeof ctx === 'function') {
      ctx = null;
    } else {
      ctx = ctx || null;
    }
  
    // set default options
    options = _setDefaults(options, {
      base: _isString(input) ? input : ''
    });
  
    // expand input
    const expanded = await jsonld.expand(input, options);
  
    // do flattening
    const flattened = _flatten(expanded);
  
    if(ctx === null) {
      // no compaction required
      return flattened;
    }
  
    // compact result (force @graph option to true, skip expansion)
    options.graph = true;
    options.skipExpansion = true;
    const compacted = await jsonld.compact(flattened, ctx, options);
  
    return compacted;
  });
  
  /**
   * Performs JSON-LD framing.
   *
   * @param input the JSON-LD input to frame.
   * @param frame the JSON-LD frame to use.
   * @param [options] the framing options.
   *          [base] the base IRI to use.
   *          [expandContext] a context to expand with.
   *          [embed] default @embed flag: '@last', '@always', '@never', '@link'
   *            (default: '@last').
   *          [explicit] default @explicit flag (default: false).
   *          [requireAll] default @requireAll flag (default: true).
   *          [omitDefault] default @omitDefault flag (default: false).
   *          [documentLoader(url, callback(err, remoteDoc))] the document loader.
   * @param [callback(err, framed)] called once the operation completes.
   *
   * @return a Promise that resolves to the framed output.
   */
  jsonld.frame = util.callbackify(async function(input, frame, options) {
    if(arguments.length < 2) {
      throw new TypeError('Could not frame, too few arguments.');
    }
  
    // set default options
    options = _setDefaults(options, {
      base: _isString(input) ? input : '',
      embed: '@last',
      explicit: false,
      requireAll: true,
      omitDefault: false,
      pruneBlankNodeIdentifiers: true,
      bnodesToClear: []
    });
  
    // if frame is a string, attempt to dereference remote document
    if(_isString(frame)) {
      // load remote doc
      const remoteDoc = await jsonld.get(frame, options);
      frame = remoteDoc.document;
  
      if(remoteDoc.contextUrl) {
        // inject link header @context into frame
        let ctx = frame['@context'];
        if(!ctx) {
          ctx = remoteDoc.contextUrl;
        } else if(_isArray(ctx)) {
          ctx.push(remoteDoc.contextUrl);
        } else {
          ctx = [ctx, remoteDoc.contextUrl];
        }
        frame['@context'] = ctx;
      }
    }
  
    let frameContext = frame ? frame['@context'] || {} : {};
  
    // expand input
    const expanded = await jsonld.expand(input, options);
  
    // expand frame
    const opts = util.clone(options);
    opts.isFrame = true;
    opts.keepFreeFloatingNodes = true;
    const expandedFrame = await jsonld.expand(frame, opts);
  
    // if the unexpanded frame includes a key expanding to @graph, frame the default graph, otherwise, the merged graph
    let framed;
    // FIXME should look for aliases of @graph
    opts.merged = !('@graph' in frame);
    // do framing
    framed = _frameMergedOrDefault(expanded, expandedFrame, opts);
  
    // compact result (force @graph option to true, skip expansion,
    // check for linked embeds)
    opts.graph = true;
    opts.skipExpansion = true;
    opts.link = {};
    opts.framing = true;
    const compacted = await jsonld.compact(framed, frameContext, opts);
  
    return compacted;
  });
  
  /**
   * **Experimental**
   *
   * Links a JSON-LD document's nodes in memory.
   *
   * @param input the JSON-LD document to link.
   * @param [ctx] the JSON-LD context to apply.
   * @param [options] the options to use:
   *          [base] the base IRI to use.
   *          [expandContext] a context to expand with.
   *          [documentLoader(url, callback(err, remoteDoc))] the document loader.
   * @param [callback(err, linked)] called once the operation completes.
   *
   * @return a Promise that resolves to the linked output.
   */
  jsonld.link = util.callbackify(async function(input, ctx, options) {
    // API matches running frame with a wildcard frame and embed: '@link'
    // get arguments
    const frame = {};
    if(ctx) {
      frame['@context'] = ctx;
    }
    frame['@embed'] = '@link';
    return jsonld.frame(input, frame, 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.
   *
   * @param input the input to normalize as JSON-LD 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: `URGNA2012`).
   *          [base] the base IRI to use.
   *          [expandContext] a context to expand with.
   *          [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.
   *          [documentLoader(url, callback(err, remoteDoc))] the document loader.
   * @param [callback(err, normalized)] called once the operation completes.
   *
   * @return a Promise that resolves to the normalized output.
   */
  jsonld.normalize = jsonld.canonize = util.callbackify(async function(
    input, options) {
    if(arguments.length < 1) {
      throw new TypeError('Could not canonize, too few arguments.');
    }
  
    // set default options
    options = _setDefaults(options, {
      base: _isString(input) ? input : '',
      algorithm: 'URDNA2015'
    });
    if('inputFormat' in options) {
      if(options.inputFormat !== 'application/n-quads' &&
        options.inputFormat !== 'application/nquads') {
        throw new JsonLdError(
          'Unknown canonicalization input format.',
          'jsonld.CanonizeError');
      }
      // TODO: `await` for async parsers
      const parsedInput = NQuads.parse(input);
  
      // do canonicalization
      return canonize.canonize(parsedInput, options);
    }
  
    // convert to RDF dataset then do normalization
    const opts = util.clone(options);
    delete opts.format;
    opts.produceGeneralizedRdf = false;
    const dataset = await jsonld.toRDF(input, opts);
  
    // do canonicalization
    return canonize.canonize(dataset, options);
  });
  
  /**
   * Converts an RDF dataset to JSON-LD.
   *
   * @param dataset a serialized string of RDF in a format specified by the
   *          format option or an RDF dataset to convert.
   * @param [options] the options to use:
   *          [format] the format if dataset param must first be parsed:
   *            'application/n-quads' for N-Quads (default).
   *          [rdfParser] a custom RDF-parser to use to parse the dataset.
   *          [useRdfType] true to use rdf:type, false to use @type
   *            (default: false).
   *          [useNativeTypes] true to convert XSD types into native types
   *            (boolean, integer, double), false not to (default: false).
   * @param [callback(err, output)] called once the operation completes.
   *
   * @return a Promise that resolves to the JSON-LD document.
   */
  jsonld.fromRDF = util.callbackify(async function(dataset, options) {
    if(arguments.length < 1) {
      throw new TypeError('Could not convert from RDF, too few arguments.');
    }
  
    // set default options
    options = _setDefaults(options, {
      format: _isString(dataset) ? 'application/n-quads' : undefined
    });
  
    let {format, rdfParser} = options;
  
    // handle special format
    if(format) {
      // check supported formats
      rdfParser = rdfParser || _rdfParsers[format];
      if(!rdfParser) {
        throw new JsonLdError(
          'Unknown input format.',
          'jsonld.UnknownFormat', {format});
      }
    } else {
      // no-op parser, assume dataset already parsed
      rdfParser = () => dataset;
    }
  
    // TODO: call `normalizeAsyncFn` on parser fn
  
    // rdfParser can be callback, promise-based, or synchronous
    let parsedDataset;
    if(rdfParser.length > 1) {
      // convert callback-based rdf parser to promise-based
      parsedDataset = new Promise((resolve, reject) => {
        rdfParser(dataset, (err, dataset) => {
          if(err) {
            reject(err);
          } else {
            resolve(dataset);
          }
        });
      });
    } else {
      parsedDataset = Promise.resolve(rdfParser(dataset));
    }
  
    parsedDataset = await parsedDataset;
  
    // back-compat with old parsers that produced legacy dataset format
    if(!Array.isArray(parsedDataset)) {
      parsedDataset = NQuads.legacyDatasetToQuads(parsedDataset);
    }
  
    return _fromRDF(parsedDataset, options);
  });
  
  /**
   * Outputs the RDF dataset found in the given JSON-LD object.
   *
   * @param input the JSON-LD input.
   * @param [options] the options to use:
   *          [base] the base IRI to use.
   *          [expandContext] a context to expand with.
   *          [format] the format to use to output a string:
   *            'application/n-quads' for N-Quads.
   *          [produceGeneralizedRdf] true to output generalized RDF, false
   *            to produce only standard RDF (default: false).
   *          [documentLoader(url, callback(err, remoteDoc))] the document loader.
   * @param [callback(err, dataset)] called once the operation completes.
   *
   * @return a Promise that resolves to the RDF dataset.
   */
  jsonld.toRDF = util.callbackify(async function(input, options) {
    if(arguments.length < 1) {
      throw new TypeError('Could not convert to RDF, too few arguments.');
    }
  
    // set default options
    options = _setDefaults(options, {
      base: _isString(input) ? input : ''
    });
  
    // TODO: support toRDF custom map?
  
    // expand input
    const expanded = await jsonld.expand(input, options);
  
    // output RDF dataset
    const dataset = _toRDF(expanded, options);
    if(options.format) {
      if(options.format === 'application/n-quads' ||
        options.format === 'application/nquads') {
        return await NQuads.serialize(dataset);
      }
      throw new JsonLdError(
        'Unknown output format.',
        'jsonld.UnknownFormat', {format: options.format});
    }
  
    return dataset;
  });
  
  /**
   * **Experimental**
   *
   * Recursively flattens the nodes in the given JSON-LD input into a merged
   * map of node ID => node. All graphs will be merged into the default graph.
   *
   * @param input the JSON-LD input.
   * @param [options] the options to use:
   *          [base] the base IRI to use.
   *          [expandContext] a context to expand with.
   *          [issuer] a jsonld.IdentifierIssuer to use to label blank nodes.
   *          [documentLoader(url, callback(err, remoteDoc))] the document loader.
   * @param [callback(err, nodeMap)] called once the operation completes.
   *
   * @return a Promise that resolves to the merged node map.
   */
  jsonld.createNodeMap = util.callbackify(async function(input, options) {
    if(arguments.length < 1) {
      throw new TypeError('Could not create node map, too few arguments.');
    }
  
    // set default options
    options = _setDefaults(options, {
      base: _isString(input) ? input : ''
    });
  
    // expand input
    const expanded = await jsonld.expand(input, options);
  
    return _createMergedNodeMap(expanded, options);
  });
  
  /**
   * **Experimental**
   *
   * Merges two or more JSON-LD documents into a single flattened document.
   *
   * @param docs the JSON-LD documents to merge together.
   * @param ctx the context to use to compact the merged result, or null.
   * @param [options] the options to use:
   *          [base] the base IRI to use.
   *          [expandContext] a context to expand with.
   *          [issuer] a jsonld.IdentifierIssuer to use to label blank nodes.
   *          [mergeNodes] true to merge properties for nodes with the same ID,
   *            false to ignore new properties for nodes with the same ID once
   *            the ID has been defined; note that this may not prevent merging
   *            new properties where a node is in the `object` position
   *            (default: true).
   *          [documentLoader(url, callback(err, remoteDoc))] the document loader.
   * @param [callback(err, merged)] called once the operation completes.
   *
   * @return a Promise that resolves to the merged output.
   */
  jsonld.merge = util.callbackify(async function(docs, ctx, options) {
    if(arguments.length < 1) {
      throw new TypeError('Could not merge, too few arguments.');
    }
    if(!_isArray(docs)) {
      throw new TypeError('Could not merge, "docs" must be an array.');
    }
  
    if(typeof ctx === 'function') {
      ctx = null;
    } else {
      ctx = ctx || null;
    }
  
    // set default options
    options = _setDefaults(options, {});
  
    // expand all documents
    const expanded = await Promise.all(docs.map(doc => {
      const opts = Object.assign({}, options);
      return jsonld.expand(doc, opts);
    }));
  
    let mergeNodes = true;
    if('mergeNodes' in options) {
      mergeNodes = options.mergeNodes;
    }
  
    const issuer = options.issuer || new IdentifierIssuer('_:b');
    const graphs = {'@default': {}};
  
    for(let i = 0; i < expanded.length; ++i) {
      // uniquely relabel blank nodes
      const doc = util.relabelBlankNodes(expanded[i], {
        issuer: new IdentifierIssuer('_:b' + i + '-')
      });
  
      // add nodes to the shared node map graphs if merging nodes, to a
      // separate graph set if not
      const _graphs = (mergeNodes || i === 0) ? graphs : {'@default': {}};
      _createNodeMap(doc, _graphs, '@default', issuer);
  
      if(_graphs !== graphs) {
        // merge document graphs but don't merge existing nodes
        for(let graphName in _graphs) {
          const _nodeMap = _graphs[graphName];
          if(!(graphName in graphs)) {
            graphs[graphName] = _nodeMap;
            continue;
          }
          const nodeMap = graphs[graphName];
          for(let key in _nodeMap) {
            if(!(key in nodeMap)) {
              nodeMap[key] = _nodeMap[key];
            }
          }
        }
      }
    }
  
    // add all non-default graphs to default graph
    const defaultGraph = _mergeNodeMaps(graphs);
  
    // produce flattened output
    const flattened = [];
    const keys = Object.keys(defaultGraph).sort();
    for(let ki = 0; ki < keys.length; ++ki) {
      const node = defaultGraph[keys[ki]];
      // only add full subjects to top-level
      if(!_isSubjectReference(node)) {
        flattened.push(node);
      }
    }
  
    if(ctx === null) {
      return flattened;
    }
  
    // compact result (force @graph option to true, skip expansion)
    options.graph = true;
    options.skipExpansion = true;
    const compacted = await jsonld.compact(flattened, ctx, options);
  
    return compacted;
  });
  
  /**
   * The default document loader for external documents. If the environment
   * is node.js, a callback-continuation-style document loader is used; otherwise,
   * a promises-style document loader is used.
   *
   * @param url the URL to load.
   * @param callback(err, remoteDoc) called once the operation completes,
   *          if using a non-promises API.
   *
   * @return a promise, if using a promises API.
   */
  Object.defineProperty(jsonld, 'documentLoader', {
    get: () => jsonld._documentLoader,
    set: v => jsonld._documentLoader = util.normalizeDocumentLoader(v)
  });
  // default document loader not implemented
  jsonld.documentLoader = async url => {
    throw new JsonLdError(
      'Could not retrieve a JSON-LD document from the URL. URL ' +
      'dereferencing not implemented.', 'jsonld.LoadDocumentError',
      {code: 'loading document failed', url: url});
  };
  
  /**
   * Deprecated default document loader. Do not use or override.
   */
  jsonld.loadDocument = util.callbackify(async function() {
    return jsonld.documentLoader.apply(null, arguments);
  });
  
  /**
   * Gets a remote JSON-LD document using the default document loader or
   * one given in the passed options.
   *
   * @param url the URL to fetch.
   * @param [options] the options to use:
   *          [documentLoader] the document loader to use.
   * @param [callback(err, remoteDoc)] called once the operation completes.
   *
   * @return a Promise that resolves to the retrieved remote document.
   */
  jsonld.get = util.callbackify(async function(url, options) {
    let load;
    if(typeof options.documentLoader === 'function') {
      load = util.normalizeDocumentLoader(options.documentLoader);
    } else {
      load = jsonld.documentLoader;
    }
  
    const remoteDoc = await load(url);
  
    // TODO: can this be moved into `normalizeDocumentLoader`?
    try {
      if(!remoteDoc.document) {
        throw new JsonLdError(
          'No remote document found at the given URL.',
          'jsonld.NullRemoteDocument');
      }
      if(_isString(remoteDoc.document)) {
        remoteDoc.document = JSON.parse(remoteDoc.document);
      }
    } catch(e) {
      throw new JsonLdError(
        'Could not retrieve a JSON-LD document from the URL.',
        'jsonld.LoadDocumentError', {
          code: 'loading document failed',
          cause: e,
          remoteDoc: remoteDoc
        });
    }
  
    return remoteDoc;
  });
  
  /**
   * Processes a local context, resolving any URLs as necessary, and returns a
   * new active context in its callback.
   *
   * @param activeCtx the current active context.
   * @param localCtx the local context to process.
   * @param [options] the options to use:
   *          [documentLoader(url, callback(err, remoteDoc))] the document loader.
   * @param [callback(err, activeCtx)] called once the operation completes.
   *
   * @return a Promise that resolves to the new active context.
   */
  jsonld.processContext = util.callbackify(async function(
    activeCtx, localCtx, options) {
    // set default options
    options = _setDefaults(options, {
      base: ''
    });
  
    // return initial context early for null context
    if(localCtx === null) {
      return _getInitialContext(options);
    }
  
    // get URLs in localCtx
    localCtx = util.clone(localCtx);
    if(!(_isObject(localCtx) && '@context' in localCtx)) {
      localCtx = {'@context': localCtx};
    }
    let ctx = await _getAllContexts(localCtx, options);
  
    return _processContext({activeCtx, localCtx: ctx, options});
  });
  
  // backwards compatibility
  jsonld.getContextValue = require('./context').getContextValue;
  
  /**
   * Document loaders.
   */
  jsonld.documentLoaders = {};
  jsonld.documentLoaders.node = require('./documentLoaders/node');
  jsonld.documentLoaders.xhr = require('./documentLoaders/xhr');
  
  /**
   * Assigns the default document loader for external document URLs to a built-in
   * default. Supported types currently include: 'xhr' and 'node'.
   *
   * @param type the type to set.
   * @param [params] the parameters required to use the document loader.
   */
  jsonld.useDocumentLoader = function(type) {
    if(!(type in jsonld.documentLoaders)) {
      throw new JsonLdError(
        'Unknown document loader type: "' + type + '"',
        'jsonld.UnknownDocumentLoader',
        {type: type});
    }
  
    // set document loader
    jsonld.documentLoader = jsonld.documentLoaders[type].apply(
      jsonld, Array.prototype.slice.call(arguments, 1));
  };
  
  /** Registered RDF dataset parsers hashed by content-type. */
  const _rdfParsers = {};
  
  /**
   * Registers an RDF dataset parser by content-type, for use with
   * jsonld.fromRDF. An RDF dataset parser will always be given two parameters,
   * a string of input and a callback. An RDF dataset parser can be synchronous
   * or asynchronous.
   *
   * If the parser function returns undefined or null then it will be assumed to
   * be asynchronous w/a continuation-passing style and the callback parameter
   * given to the parser MUST be invoked.
   *
   * If it returns a Promise, then it will be assumed to be asynchronous, but the
   * callback parameter MUST NOT be invoked. It should instead be ignored.
   *
   * If it returns an RDF dataset, it will be assumed to be synchronous and the
   * callback parameter MUST NOT be invoked. It should instead be ignored.
   *
   * @param contentType the content-type for the parser.
   * @param parser(input, callback(err, dataset)) the parser function (takes a
   *          string as a parameter and either returns null/undefined and uses
   *          the given callback, returns a Promise, or returns an RDF dataset).
   */
  jsonld.registerRDFParser = function(contentType, parser) {
    _rdfParsers[contentType] = parser;
  };
  
  /**
   * Unregisters an RDF dataset parser by content-type.
   *
   * @param contentType the content-type for the parser.
   */
  jsonld.unregisterRDFParser = function(contentType) {
    delete _rdfParsers[contentType];
  };
  
  // register the N-Quads RDF parser
  jsonld.registerRDFParser('application/n-quads', NQuads.parse);
  jsonld.registerRDFParser('application/nquads', NQuads.parse);
  
  // register the RDFa API RDF parser
  jsonld.registerRDFParser('rdfa-api', Rdfa.parse);
  
  /* URL API */
  jsonld.url = require('./url');
  
  /* Utility API */
  jsonld.util = util;
  // backwards compatibility
  Object.assign(jsonld, util);
  
  // reexpose API as jsonld.promises for backwards compatability
  jsonld.promises = jsonld;
  
  // backwards compatibility
  jsonld.RequestQueue = require('./RequestQueue');
  
  /* WebIDL API */
  jsonld.JsonLdProcessor = require('./JsonLdProcessor')(jsonld);
  
  // setup browser global JsonLdProcessor
  if(_browser && typeof global.JsonLdProcessor === 'undefined') {
    Object.defineProperty(global, 'JsonLdProcessor', {
      writable: true,
      enumerable: false,
      configurable: true,
      value: jsonld.JsonLdProcessor
    });
  }
  
  // set platform-specific defaults/APIs
  if(_nodejs) {
    // use node document loader by default
    jsonld.useDocumentLoader('node');
  } else if(typeof XMLHttpRequest !== 'undefined') {
    // use xhr document loader by default
    jsonld.useDocumentLoader('xhr');
  }
  
  function _setDefaults(options, {
    documentLoader = jsonld.documentLoader,
    ...defaults
  }) {
    if(typeof options === 'function') {
      options = {};
    }
    options = options || {};
    return Object.assign({}, {documentLoader}, defaults, options);
  }
  
  // end of jsonld API `wrapper` factory
  return jsonld;
  };
  
  // external APIs:
  
  // used to generate a new jsonld API instance
  const factory = function() {
    return wrapper(function() {
      return factory();
    });
  };
  
  if(!_nodejs && (typeof define === 'function' && define.amd)) {
    // export AMD API
    define([], function() {
      // now that module is defined, wrap main jsonld API instance
      wrapper(factory);
      return factory;
    });
  } else {
    // wrap the main jsonld API instance
    wrapper(factory);
  
    if(typeof require === 'function' &&
      typeof module !== 'undefined' && module.exports) {
      // export CommonJS/nodejs API
      module.exports = factory;
    }
  
    if(_browser) {
      // export simple browser API
      if(typeof jsonld === 'undefined') {
        jsonld = jsonldjs = factory;
      } else {
        jsonldjs = factory;
      }
    }
  }
  
  return factory;
  
  })();
  