text: /*
   * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved.
   */
  'use strict';
  
  const JsonLdError = require('./JsonLdError');
  
  const {
    isArray: _isArray,
    isObject: _isObject,
    isString: _isString,
    isUndefined: _isUndefined
  } = require('./types');
  
  const {
    isList: _isList,
    isValue: _isValue,
    isGraph: _isGraph,
    isSimpleGraph: _isSimpleGraph,
    isSubjectReference: _isSubjectReference
  } = require('./graphTypes');
  
  const {
    expandIri: _expandIri,
    getContextValue: _getContextValue,
    isKeyword: _isKeyword,
    process: _processContext
  } = require('./context');
  
  const {
    removeBase: _removeBase
  } = require('./url');
  
  const {
    addValue: _addValue,
    asArray: _asArray,
    compareShortestLeast: _compareShortestLeast
  } = require('./util');
  
  const api = {};
  module.exports = api;
  /**
   * Recursively compacts an element using the given active context. All values
   * must be in expanded form before this method is called.
   *
   * @param activeCtx the active context to use.
   * @param activeProperty the compacted property associated with the element
   *          to compact, null for none.
   * @param element the element to compact.
   * @param options the compaction options.
   * @param compactionMap the compaction map to use.
   *
   * @return the compacted value.
   */
  
  api.compact = ({
    activeCtx,
    activeProperty = null,
    element,
    options = {},
    compactionMap = () => undefined
  }) => {
    // recursively compact array
    if (_isArray(element)) {
      let rval = [];
  
      for (let i = 0; i < element.length; ++i) {
        // compact, dropping any null values unless custom mapped
        let compacted = api.compact({
          activeCtx,
          activeProperty,
          element: element[i],
          options,
          compactionMap
        });
  
        if (compacted === null) {
          // TODO: use `await` to support async
          compacted = compactionMap({
            unmappedValue: element[i],
            activeCtx,
            activeProperty,
            parent: element,
            index: i,
            options
          });
  
          if (compacted === undefined) {
            continue;
          }
        }
  
        rval.push(compacted);
      }
  
      if (options.compactArrays && rval.length === 1) {
        // use single element if no container is specified
        const container = _getContextValue(activeCtx, activeProperty, '@container') || [];
  
        if (container.length === 0) {
          rval = rval[0];
        }
      }
  
      return rval;
    } // use any scoped context on activeProperty
  
  
    const ctx = _getContextValue(activeCtx, activeProperty, '@context');
  
    if (!_isUndefined(ctx)) {
      // Note: spec's `from term` var is named `isPropertyTermScopedContext`
      activeCtx = _processContext({
        activeCtx,
        localCtx: ctx,
        isPropertyTermScopedContext: true,
        options
      });
    } // recursively compact object
  
  
    if (_isObject(element)) {
      if (options.link && '@id' in element && options.link.hasOwnProperty(element['@id'])) {
        // check for a linked element to reuse
        const linked = options.link[element['@id']];
  
        for (let i = 0; i < linked.length; ++i) {
          if (linked[i].expanded === element) {
            return linked[i].compacted;
          }
        }
      } // do value compaction on @values and subject references
  
  
      if (_isValue(element) || _isSubjectReference(element)) {
        const rval = api.compactValue({
          activeCtx,
          activeProperty,
          value: element,
          options
        });
  
        if (options.link && _isSubjectReference(element)) {
          // store linked element
          if (!options.link.hasOwnProperty(element['@id'])) {
            options.link[element['@id']] = [];
          }
  
          options.link[element['@id']].push({
            expanded: element,
            compacted: rval
          });
        }
  
        return rval;
      } // if expanded property is @list and we're contained within a list
      // container, recursively compact this item to an array
  
  
      if (_isList(element)) {
        const container = _getContextValue(activeCtx, activeProperty, '@container') || [];
  
        if (container.includes('@list')) {
          return api.compact({
            activeCtx,
            activeProperty,
            element: element['@list'],
            options,
            compactionMap
          });
        }
      } // FIXME: avoid misuse of active property as an expanded property?
  
  
      const insideReverse = activeProperty === '@reverse';
      const rval = {}; // revert type scoped context
  
      activeCtx = activeCtx.revertTypeScopedContext();
  
      if (options.link && '@id' in element) {
        // store linked element
        if (!options.link.hasOwnProperty(element['@id'])) {
          options.link[element['@id']] = [];
        }
  
        options.link[element['@id']].push({
          expanded: element,
          compacted: rval
        });
      } // apply any context defined on an alias of @type
      // if key is @type and any compacted value is a term having a local
      // context, overlay that context
  
  
      let types = element['@type'] || [];
  
      if (types.length > 1) {
        types = Array.from(types).sort();
      } // find all type-scoped contexts based on current context, prior to
      // updating it
  
  
      const typeContext = activeCtx;
  
      for (const type of types) {
        const compactedType = api.compactIri({
          activeCtx: typeContext,
          iri: type,
          relativeTo: {
            vocab: true
          }
        }); // Use any type-scoped context defined on this value
  
        const ctx = _getContextValue(typeContext, compactedType, '@context');
  
        if (!_isUndefined(ctx)) {
          activeCtx = _processContext({
            activeCtx,
            localCtx: ctx,
            options,
            isTypeScopedContext: true
          });
        }
      } // process element keys in order
  
  
      const keys = Object.keys(element).sort();
  
      for (const expandedProperty of keys) {
        const expandedValue = element[expandedProperty]; // compact @id and @type(s)
  
        if (expandedProperty === '@id' || expandedProperty === '@type') {
          // if using a type-scoped context, resolve type values against previous
          // context
          const isType = expandedProperty === '@type';
          const valueContext = isType ? activeCtx.previousContext || activeCtx : activeCtx;
  
          let compactedValue = _asArray(expandedValue).map(expandedIri => api.compactIri({
            activeCtx: valueContext,
            iri: expandedIri,
            relativeTo: {
              vocab: isType
            }
          }));
  
          if (compactedValue.length === 1) {
            compactedValue = compactedValue[0];
          } // use keyword alias and add value
  
  
          const alias = api.compactIri({
            activeCtx,
            iri: expandedProperty,
            relativeTo: {
              vocab: true
            }
          });
          const isArray = _isArray(compactedValue) && expandedValue.length === 0;
  
          _addValue(rval, alias, compactedValue, {
            propertyIsArray: isArray
          });
  
          continue;
        } // handle @reverse
  
  
        if (expandedProperty === '@reverse') {
          // recursively compact expanded value
          const compactedValue = api.compact({
            activeCtx,
            activeProperty: '@reverse',
            element: expandedValue,
            options,
            compactionMap
          }); // handle double-reversed properties
  
          for (const compactedProperty in compactedValue) {
            if (activeCtx.mappings.has(compactedProperty) && activeCtx.mappings.get(compactedProperty).reverse) {
              const value = compactedValue[compactedProperty];
              const container = _getContextValue(activeCtx, compactedProperty, '@container') || [];
              const useArray = container.includes('@set') || !options.compactArrays;
  
              _addValue(rval, compactedProperty, value, {
                propertyIsArray: useArray
              });
  
              delete compactedValue[compactedProperty];
            }
          }
  
          if (Object.keys(compactedValue).length > 0) {
            // use keyword alias and add value
            const alias = api.compactIri({
              activeCtx,
              iri: expandedProperty,
              relativeTo: {
                vocab: true
              }
            });
  
            _addValue(rval, alias, compactedValue);
          }
  
          continue;
        }
  
        if (expandedProperty === '@preserve') {
          // compact using activeProperty
          const compactedValue = api.compact({
            activeCtx,
            activeProperty,
            element: expandedValue,
            options,
            compactionMap
          });
  
          if (!(_isArray(compactedValue) && compactedValue.length === 0)) {
            _addValue(rval, expandedProperty, compactedValue);
          }
  
          continue;
        } // handle @index property
  
  
        if (expandedProperty === '@index') {
          // drop @index if inside an @index container
          const container = _getContextValue(activeCtx, activeProperty, '@container') || [];
  
          if (container.includes('@index')) {
            continue;
          } // use keyword alias and add value
  
  
          const alias = api.compactIri({
            activeCtx,
            iri: expandedProperty,
            relativeTo: {
              vocab: true
            }
          });
  
          _addValue(rval, alias, expandedValue);
  
          continue;
        } // skip array processing for keywords that aren't @graph or @list
  
  
        if (expandedProperty !== '@graph' && expandedProperty !== '@list' && _isKeyword(expandedProperty)) {
          // use keyword alias and add value as is
          const alias = api.compactIri({
            activeCtx,
            iri: expandedProperty,
            relativeTo: {
              vocab: true
            }
          });
  
          _addValue(rval, alias, expandedValue);
  
          continue;
        } // Note: expanded value must be an array due to expansion algorithm.
  
  
        if (!_isArray(expandedValue)) {
          throw new JsonLdError('JSON-LD expansion error; expanded value must be an array.', 'jsonld.SyntaxError');
        } // preserve empty arrays
  
  
        if (expandedValue.length === 0) {
          const itemActiveProperty = api.compactIri({
            activeCtx,
            iri: expandedProperty,
            value: expandedValue,
            relativeTo: {
              vocab: true
            },
            reverse: insideReverse
          });
          const nestProperty = activeCtx.mappings.has(itemActiveProperty) ? activeCtx.mappings.get(itemActiveProperty)['@nest'] : null;
          let nestResult = rval;
  
          if (nestProperty) {
            _checkNestProperty(activeCtx, nestProperty, options);
  
            if (!_isObject(rval[nestProperty])) {
              rval[nestProperty] = {};
            }
  
            nestResult = rval[nestProperty];
          }
  
          _addValue(nestResult, itemActiveProperty, expandedValue, {
            propertyIsArray: true
          });
        } // recusively process array values
  
  
        for (const expandedItem of expandedValue) {
          // compact property and get container type
          const itemActiveProperty = api.compactIri({
            activeCtx,
            iri: expandedProperty,
            value: expandedItem,
            relativeTo: {
              vocab: true
            },
            reverse: insideReverse
          }); // if itemActiveProperty is a @nest property, add values to nestResult,
          // otherwise rval
  
          const nestProperty = activeCtx.mappings.has(itemActiveProperty) ? activeCtx.mappings.get(itemActiveProperty)['@nest'] : null;
          let nestResult = rval;
  
          if (nestProperty) {
            _checkNestProperty(activeCtx, nestProperty, options);
  
            if (!_isObject(rval[nestProperty])) {
              rval[nestProperty] = {};
            }
  
            nestResult = rval[nestProperty];
          }
  
          const container = _getContextValue(activeCtx, itemActiveProperty, '@container') || []; // get simple @graph or @list value if appropriate
  
          const isGraph = _isGraph(expandedItem);
  
          const isList = _isList(expandedItem);
  
          let inner;
  
          if (isList) {
            inner = expandedItem['@list'];
          } else if (isGraph) {
            inner = expandedItem['@graph'];
          } // recursively compact expanded item
  
  
          let compactedItem = api.compact({
            activeCtx,
            activeProperty: itemActiveProperty,
            element: isList || isGraph ? inner : expandedItem,
            options,
            compactionMap
          }); // handle @list
  
          if (isList) {
            // ensure @list value is an array
            if (!_isArray(compactedItem)) {
              compactedItem = [compactedItem];
            }
  
            if (!container.includes('@list')) {
              // wrap using @list alias
              compactedItem = {
                [api.compactIri({
                  activeCtx,
                  iri: '@list',
                  relativeTo: {
                    vocab: true
                  }
                })]: compactedItem
              }; // include @index from expanded @list, if any
  
              if ('@index' in expandedItem) {
                compactedItem[api.compactIri({
                  activeCtx,
                  iri: '@index',
                  relativeTo: {
                    vocab: true
                  }
                })] = expandedItem['@index'];
              }
            } else {
              _addValue(nestResult, itemActiveProperty, compactedItem, {
                valueIsArray: true,
                allowDuplicate: true
              });
  
              continue;
            }
          } // Graph object compaction cases
  
  
          if (isGraph) {
            if (container.includes('@graph') && (container.includes('@id') || container.includes('@index') && _isSimpleGraph(expandedItem))) {
              // get or create the map object
              let mapObject;
  
              if (nestResult.hasOwnProperty(itemActiveProperty)) {
                mapObject = nestResult[itemActiveProperty];
              } else {
                nestResult[itemActiveProperty] = mapObject = {};
              } // index on @id or @index or alias of @none
  
  
              const key = (container.includes('@id') ? expandedItem['@id'] : expandedItem['@index']) || api.compactIri({
                activeCtx,
                iri: '@none',
                vocab: true
              }); // add compactedItem to map, using value of `@id` or a new blank
              // node identifier
  
              _addValue(mapObject, key, compactedItem, {
                propertyIsArray: !options.compactArrays || container.includes('@set')
              });
            } else if (container.includes('@graph') && _isSimpleGraph(expandedItem)) {
              // container includes @graph but not @id or @index and value is a
              // simple graph object add compact value
              _addValue(nestResult, itemActiveProperty, compactedItem, {
                propertyIsArray: !options.compactArrays || container.includes('@set')
              });
            } else {
              // wrap using @graph alias, remove array if only one item and
              // compactArrays not set
              if (_isArray(compactedItem) && compactedItem.length === 1 && options.compactArrays) {
                compactedItem = compactedItem[0];
              }
  
              compactedItem = {
                [api.compactIri({
                  activeCtx,
                  iri: '@graph',
                  relativeTo: {
                    vocab: true
                  }
                })]: compactedItem
              }; // include @id from expanded graph, if any
  
              if ('@id' in expandedItem) {
                compactedItem[api.compactIri({
                  activeCtx,
                  iri: '@id',
                  relativeTo: {
                    vocab: true
                  }
                })] = expandedItem['@id'];
              } // include @index from expanded graph, if any
  
  
              if ('@index' in expandedItem) {
                compactedItem[api.compactIri({
                  activeCtx,
                  iri: '@index',
                  relativeTo: {
                    vocab: true
                  }
                })] = expandedItem['@index'];
              }
  
              _addValue(nestResult, itemActiveProperty, compactedItem, {
                propertyIsArray: !options.compactArrays || container.includes('@set')
              });
            }
          } else if (container.includes('@language') || container.includes('@index') || container.includes('@id') || container.includes('@type')) {
            // handle language and index maps
            // get or create the map object
            let mapObject;
  
            if (nestResult.hasOwnProperty(itemActiveProperty)) {
              mapObject = nestResult[itemActiveProperty];
            } else {
              nestResult[itemActiveProperty] = mapObject = {};
            }
  
            let key;
  
            if (container.includes('@language')) {
              // if container is a language map, simplify compacted value to
              // a simple string
              if (_isValue(compactedItem)) {
                compactedItem = compactedItem['@value'];
              }
  
              key = expandedItem['@language'];
            } else if (container.includes('@index')) {
              key = expandedItem['@index'];
            } else if (container.includes('@id')) {
              const idKey = api.compactIri({
                activeCtx,
                iri: '@id',
                vocab: true
              });
              key = compactedItem[idKey];
              delete compactedItem[idKey];
            } else if (container.includes('@type')) {
              const typeKey = api.compactIri({
                activeCtx,
                iri: '@type',
                vocab: true
              });
              let types;
              [key, ...types] = _asArray(compactedItem[typeKey] || []);
  
              switch (types.length) {
                case 0:
                  delete compactedItem[typeKey];
                  break;
  
                case 1:
                  compactedItem[typeKey] = types[0];
                  break;
  
                default:
                  compactedItem[typeKey] = types;
                  break;
              }
            } // if compacting this value which has no key, index on @none
  
  
            if (!key) {
              key = api.compactIri({
                activeCtx,
                iri: '@none',
                vocab: true
              });
            } // add compact value to map object using key from expanded value
            // based on the container type
  
  
            _addValue(mapObject, key, compactedItem, {
              propertyIsArray: container.includes('@set')
            });
          } else {
            // use an array if: compactArrays flag is false,
            // @container is @set or @list , value is an empty
            // array, or key is @graph
            const isArray = !options.compactArrays || container.includes('@set') || container.includes('@list') || _isArray(compactedItem) && compactedItem.length === 0 || expandedProperty === '@list' || expandedProperty === '@graph'; // add compact value
  
            _addValue(nestResult, itemActiveProperty, compactedItem, {
              propertyIsArray: isArray
            });
          }
        }
      }
  
      return rval;
    } // only primitives remain which are already compact
  
  
    return element;
  };
  /**
   * Compacts an IRI or keyword into a term or prefix if it can be. If the
   * IRI has an associated value it may be passed.
   *
   * @param activeCtx the active context to use.
   * @param iri the IRI to compact.
   * @param value the value to check or null.
   * @param relativeTo options for how to compact IRIs:
   *          vocab: true to split after @vocab, false not to.
   * @param reverse true if a reverse property is being compacted, false if not.
   *
   * @return the compacted term, prefix, keyword alias, or the original IRI.
   */
  
  
  api.compactIri = ({
    activeCtx,
    iri,
    value = null,
    relativeTo = {
      vocab: false
    },
    reverse = false
  }) => {
    // can't compact null
    if (iri === null) {
      return iri;
    } // if context is from a property term scoped context composed with a
    // type-scoped context, then use the previous context instead
  
  
    if (activeCtx.isPropertyTermScoped && activeCtx.previousContext) {
      activeCtx = activeCtx.previousContext;
    }
  
    const inverseCtx = activeCtx.getInverse(); // if term is a keyword, it may be compacted to a simple alias
  
    if (_isKeyword(iri) && iri in inverseCtx && '@none' in inverseCtx[iri] && '@type' in inverseCtx[iri]['@none'] && '@none' in inverseCtx[iri]['@none']['@type']) {
      return inverseCtx[iri]['@none']['@type']['@none'];
    } // use inverse context to pick a term if iri is relative to vocab
  
  
    if (relativeTo.vocab && iri in inverseCtx) {
      const defaultLanguage = activeCtx['@language'] || '@none'; // prefer @index if available in value
  
      const containers = [];
  
      if (_isObject(value) && '@index' in value && !('@graph' in value)) {
        containers.push('@index', '@index@set');
      } // if value is a preserve object, use its value
  
  
      if (_isObject(value) && '@preserve' in value) {
        value = value['@preserve'][0];
      } // prefer most specific container including @graph, prefering @set
      // variations
  
  
      if (_isGraph(value)) {
        // favor indexmap if the graph is indexed
        if ('@index' in value) {
          containers.push('@graph@index', '@graph@index@set', '@index', '@index@set');
        } // favor idmap if the graph is has an @id
  
  
        if ('@id' in value) {
          containers.push('@graph@id', '@graph@id@set');
        }
  
        containers.push('@graph', '@graph@set', '@set'); // allow indexmap if the graph is not indexed
  
        if (!('@index' in value)) {
          containers.push('@graph@index', '@graph@index@set', '@index', '@index@set');
        } // allow idmap if the graph does not have an @id
  
  
        if (!('@id' in value)) {
          containers.push('@graph@id', '@graph@id@set');
        }
      } else if (_isObject(value) && !_isValue(value)) {
        containers.push('@id', '@id@set', '@type', '@set@type');
      } // defaults for term selection based on type/language
  
  
      let typeOrLanguage = '@language';
      let typeOrLanguageValue = '@null';
  
      if (reverse) {
        typeOrLanguage = '@type';
        typeOrLanguageValue = '@reverse';
        containers.push('@set');
      } else if (_isList(value)) {
        // choose the most specific term that works for all elements in @list
        // only select @list containers if @index is NOT in value
        if (!('@index' in value)) {
          containers.push('@list');
        }
  
        const list = value['@list'];
  
        if (list.length === 0) {
          // any empty list can be matched against any term that uses the
          // @list container regardless of @type or @language
          typeOrLanguage = '@any';
          typeOrLanguageValue = '@none';
        } else {
          let commonLanguage = list.length === 0 ? defaultLanguage : null;
          let commonType = null;
  
          for (let i = 0; i < list.length; ++i) {
            const item = list[i];
            let itemLanguage = '@none';
            let itemType = '@none';
  
            if (_isValue(item)) {
              if ('@language' in item) {
                itemLanguage = item['@language'];
              } else if ('@type' in item) {
                itemType = item['@type'];
              } else {
                // plain literal
                itemLanguage = '@null';
              }
            } else {
              itemType = '@id';
            }
  
            if (commonLanguage === null) {
              commonLanguage = itemLanguage;
            } else if (itemLanguage !== commonLanguage && _isValue(item)) {
              commonLanguage = '@none';
            }
  
            if (commonType === null) {
              commonType = itemType;
            } else if (itemType !== commonType) {
              commonType = '@none';
            } // there are different languages and types in the list, so choose
            // the most generic term, no need to keep iterating the list
  
  
            if (commonLanguage === '@none' && commonType === '@none') {
              break;
            }
          }
  
          commonLanguage = commonLanguage || '@none';
          commonType = commonType || '@none';
  
          if (commonType !== '@none') {
            typeOrLanguage = '@type';
            typeOrLanguageValue = commonType;
          } else {
            typeOrLanguageValue = commonLanguage;
          }
        }
      } else {
        if (_isValue(value)) {
          if ('@language' in value && !('@index' in value)) {
            containers.push('@language', '@language@set');
            typeOrLanguageValue = value['@language'];
          } else if ('@type' in value) {
            typeOrLanguage = '@type';
            typeOrLanguageValue = value['@type'];
          }
        } else {
          typeOrLanguage = '@type';
          typeOrLanguageValue = '@id';
        }
  
        containers.push('@set');
      } // do term selection
  
  
      containers.push('@none'); // an index map can be used to index values using @none, so add as a low
      // priority
  
      if (_isObject(value) && !('@index' in value)) {
        // allow indexing even if no @index present
        containers.push('@index', '@index@set');
      } // values without type or language can use @language map
  
  
      if (_isValue(value) && Object.keys(value).length === 1) {
        // allow indexing even if no @index present
        containers.push('@language', '@language@set');
      }
  
      const term = _selectTerm(activeCtx, iri, value, containers, typeOrLanguage, typeOrLanguageValue);
  
      if (term !== null) {
        return term;
      }
    } // no term match, use @vocab if available
  
  
    if (relativeTo.vocab) {
      if ('@vocab' in activeCtx) {
        // determine if vocab is a prefix of the iri
        const vocab = activeCtx['@vocab'];
  
        if (iri.indexOf(vocab) === 0 && iri !== vocab) {
          // use suffix as relative iri if it is not a term in the active context
          const suffix = iri.substr(vocab.length);
  
          if (!activeCtx.mappings.has(suffix)) {
            return suffix;
          }
        }
      }
    } // no term or @vocab match, check for possible CURIEs
  
  
    let choice = null; // TODO: make FastCurieMap a class with a method to do this lookup
  
    const partialMatches = [];
    let iriMap = activeCtx.fastCurieMap; // check for partial matches of against `iri`, which means look until
    // iri.length - 1, not full length
  
    const maxPartialLength = iri.length - 1;
  
    for (let i = 0; i < maxPartialLength && iri[i] in iriMap; ++i) {
      iriMap = iriMap[iri[i]];
  
      if ('' in iriMap) {
        partialMatches.push(iriMap[''][0]);
      }
    } // check partial matches in reverse order to prefer longest ones first
  
  
    for (let i = partialMatches.length - 1; i >= 0; --i) {
      const entry = partialMatches[i];
      const terms = entry.terms;
  
      for (const term of terms) {
        // a CURIE is usable if:
        // 1. it has no mapping, OR
        // 2. value is null, which means we're not compacting an @value, AND
        //   the mapping matches the IRI
        const curie = term + ':' + iri.substr(entry.iri.length);
        const isUsableCurie = activeCtx.mappings.get(term)._prefix && (!activeCtx.mappings.has(curie) || value === null && activeCtx.mappings.get(curie)['@id'] === iri); // select curie if it is shorter or the same length but lexicographically
        // less than the current choice
  
        if (isUsableCurie && (choice === null || _compareShortestLeast(curie, choice) < 0)) {
          choice = curie;
        }
      }
    } // return chosen curie
  
  
    if (choice !== null) {
      return choice;
    } // compact IRI relative to base
  
  
    if (!relativeTo.vocab) {
      return _removeBase(activeCtx['@base'], iri);
    } // return IRI as is
  
  
    return iri;
  };
  /**
   * Performs value compaction on an object with '@value' or '@id' as the only
   * property.
   *
   * @param activeCtx the active context.
   * @param activeProperty the active property that points to the value.
   * @param value the value to compact.
   * @param {Object} [options] - processing options.
   *
   * @return the compaction result.
   */
  
  
  api.compactValue = ({
    activeCtx,
    activeProperty,
    value,
    options
  }) => {
    // value is a @value
    if (_isValue(value)) {
      // get context rules
      const type = _getContextValue(activeCtx, activeProperty, '@type');
  
      const language = _getContextValue(activeCtx, activeProperty, '@language');
  
      const container = _getContextValue(activeCtx, activeProperty, '@container') || []; // whether or not the value has an @index that must be preserved
  
      const preserveIndex = '@index' in value && !container.includes('@index'); // if there's no @index to preserve ...
  
      if (!preserveIndex) {
        // matching @type or @language specified in context, compact value
        if (value['@type'] === type || value['@language'] === language) {
          return value['@value'];
        }
      } // return just the value of @value if all are true:
      // 1. @value is the only key or @index isn't being preserved
      // 2. there is no default language or @value is not a string or
      //   the key has a mapping with a null @language
  
  
      const keyCount = Object.keys(value).length;
      const isValueOnlyKey = keyCount === 1 || keyCount === 2 && '@index' in value && !preserveIndex;
      const hasDefaultLanguage = '@language' in activeCtx;
  
      const isValueString = _isString(value['@value']);
  
      const hasNullMapping = activeCtx.mappings.has(activeProperty) && activeCtx.mappings.get(activeProperty)['@language'] === null;
  
      if (isValueOnlyKey && (!hasDefaultLanguage || !isValueString || hasNullMapping)) {
        return value['@value'];
      }
  
      const rval = {}; // preserve @index
  
      if (preserveIndex) {
        rval[api.compactIri({
          activeCtx,
          iri: '@index',
          relativeTo: {
            vocab: true
          }
        })] = value['@index'];
      }
  
      if ('@type' in value) {
        // compact @type IRI
        rval[api.compactIri({
          activeCtx,
          iri: '@type',
          relativeTo: {
            vocab: true
          }
        })] = api.compactIri({
          activeCtx,
          iri: value['@type'],
          relativeTo: {
            vocab: true
          }
        });
      } else if ('@language' in value) {
        // alias @language
        rval[api.compactIri({
          activeCtx,
          iri: '@language',
          relativeTo: {
            vocab: true
          }
        })] = value['@language'];
      } // alias @value
  
  
      rval[api.compactIri({
        activeCtx,
        iri: '@value',
        relativeTo: {
          vocab: true
        }
      })] = value['@value'];
      return rval;
    } // value is a subject reference
  
  
    const expandedProperty = _expandIri(activeCtx, activeProperty, {
      vocab: true
    }, options);
  
    const type = _getContextValue(activeCtx, activeProperty, '@type');
  
    const compacted = api.compactIri({
      activeCtx,
      iri: value['@id'],
      relativeTo: {
        vocab: type === '@vocab'
      }
    }); // compact to scalar
  
    if (type === '@id' || type === '@vocab' || expandedProperty === '@graph') {
      return compacted;
    }
  
    return {
      [api.compactIri({
        activeCtx,
        iri: '@id',
        relativeTo: {
          vocab: true
        }
      })]: compacted
    };
  };
  /**
   * Removes the @preserve keywords as the last step of the compaction
   * algorithm when it is running on framed output.
   *
   * @param ctx the active context used to compact the input.
   * @param input the framed, compacted output.
   * @param options the compaction options used.
   *
   * @return the resulting output.
   */
  
  
  api.removePreserve = (ctx, input, options) => {
    // recurse through arrays
    if (_isArray(input)) {
      const output = [];
  
      for (let i = 0; i < input.length; ++i) {
        const result = api.removePreserve(ctx, input[i], options); // drop nulls from arrays
  
        if (result !== null) {
          output.push(result);
        }
      }
  
      input = output;
    } else if (_isObject(input)) {
      // remove @preserve
      if ('@preserve' in input) {
        if (input['@preserve'] === '@null') {
          return null;
        }
  
        return input['@preserve'];
      } // skip @values
  
  
      if (_isValue(input)) {
        return input;
      } // recurse through @lists
  
  
      if (_isList(input)) {
        input['@list'] = api.removePreserve(ctx, input['@list'], options);
        return input;
      } // handle in-memory linked nodes
  
  
      const idAlias = api.compactIri({
        activeCtx: ctx,
        iri: '@id',
        relativeTo: {
          vocab: true
        }
      });
  
      if (input.hasOwnProperty(idAlias)) {
        const id = input[idAlias];
  
        if (options.link.hasOwnProperty(id)) {
          const idx = options.link[id].indexOf(input);
  
          if (idx !== -1) {
            // already visited
            return options.link[id][idx];
          } // prevent circular visitation
  
  
          options.link[id].push(input);
        } else {
          // prevent circular visitation
          options.link[id] = [input];
        }
      } // recurse through properties
  
  
      const graphAlias = api.compactIri({
        activeCtx: ctx,
        iri: '@graph',
        relativeTo: {
          vocab: true
        }
      });
  
      for (const prop in input) {
        // potentially remove the id, if it is an unreference bnode
        if (prop === idAlias && options.bnodesToClear.includes(input[prop])) {
          delete input[idAlias];
          continue;
        }
  
        let result = api.removePreserve(ctx, input[prop], options);
        const container = _getContextValue(ctx, prop, '@container') || [];
  
        if (options.compactArrays && _isArray(result) && result.length === 1 && container.length === 0 && prop !== graphAlias) {
          result = result[0];
        }
  
        input[prop] = result;
      }
    }
  
    return input;
  };
  /**
   * Picks the preferred compaction term from the given inverse context entry.
   *
   * @param activeCtx the active context.
   * @param iri the IRI to pick the term for.
   * @param value the value to pick the term for.
   * @param containers the preferred containers.
   * @param typeOrLanguage either '@type' or '@language'.
   * @param typeOrLanguageValue the preferred value for '@type' or '@language'.
   *
   * @return the preferred term.
   */
  
  
  function _selectTerm(activeCtx, iri, value, containers, typeOrLanguage, typeOrLanguageValue) {
    if (typeOrLanguageValue === null) {
      typeOrLanguageValue = '@null';
    } // preferences for the value of @type or @language
  
  
    const prefs = []; // determine prefs for @id based on whether or not value compacts to a term
  
    if ((typeOrLanguageValue === '@id' || typeOrLanguageValue === '@reverse') && _isSubjectReference(value)) {
      // prefer @reverse first
      if (typeOrLanguageValue === '@reverse') {
        prefs.push('@reverse');
      } // try to compact value to a term
  
  
      const term = api.compactIri({
        activeCtx,
        iri: value['@id'],
        relativeTo: {
          vocab: true
        }
      });
  
      if (activeCtx.mappings.has(term) && activeCtx.mappings.get(term) && activeCtx.mappings.get(term)['@id'] === value['@id']) {
        // prefer @vocab
        prefs.push.apply(prefs, ['@vocab', '@id']);
      } else {
        // prefer @id
        prefs.push.apply(prefs, ['@id', '@vocab']);
      }
    } else {
      prefs.push(typeOrLanguageValue);
    }
  
    prefs.push('@none');
    const containerMap = activeCtx.inverse[iri];
  
    for (let ci = 0; ci < containers.length; ++ci) {
      // if container not available in the map, continue
      const container = containers[ci];
  
      if (!(container in containerMap)) {
        continue;
      }
  
      const typeOrLanguageValueMap = containerMap[container][typeOrLanguage];
  
      for (let pi = 0; pi < prefs.length; ++pi) {
        // if type/language option not available in the map, continue
        const pref = prefs[pi];
  
        if (!(pref in typeOrLanguageValueMap)) {
          continue;
        } // select term
  
  
        return typeOrLanguageValueMap[pref];
      }
    }
  
    return null;
  }
  /**
   * The value of `@nest` in the term definition must either be `@nest`, or a term
   * which resolves to `@nest`.
   *
   * @param activeCtx the active context.
   * @param nestProperty a term in the active context or `@nest`.
   * @param {Object} [options] - processing options.
   */
  
  
  function _checkNestProperty(activeCtx, nestProperty, options) {
    if (_expandIri(activeCtx, nestProperty, {
      vocab: true
    }, options) !== '@nest') {
      throw new JsonLdError('JSON-LD compact error; nested property must have an @nest value ' + 'resolving to @nest.', 'jsonld.SyntaxError', {
        code: 'invalid @nest value'
      });
    }
  }