Update to latest jsonld.js.
authorDave Longley <dlongley@digitalbazaar.com>
Thu, 29 Aug 2013 10:42:18 -0400
changeset 1950 93833b44b434
parent 1949 c1c66daf5460
child 1951 e56627853bef
Update to latest jsonld.js.
playground/jsonld.js
--- a/playground/jsonld.js	Wed Aug 28 17:25:34 2013 -0700
+++ b/playground/jsonld.js	Thu Aug 29 10:42:18 2013 -0400
@@ -93,7 +93,7 @@
     return jsonld.nextTick(function() {
       callback(new JsonLdError(
         'The compaction context must not be null.',
-        'jsonld.CompactError'));
+        'jsonld.CompactError', {code: 'invalid local context'}));
     });
   }
 
@@ -860,7 +860,8 @@
 jsonld.documentLoader = function(url, callback) {
   return callback(new JsonLdError(
     'Could not retrieve a JSON-LD document from the URL. URL derefencing not ' +
-    'implemented.', 'jsonld.LoadDocumentError'),
+    'implemented.', 'jsonld.LoadDocumentError',
+    {code: 'loading document failed'}),
     {contextUrl: null, documentUrl: url, document: null});
 };
 
@@ -1209,7 +1210,8 @@
       return callback(new JsonLdError(
         'URL could not be dereferenced; secure mode is enabled and ' +
         'the URL\'s scheme is not "https".',
-        'jsonld.InvalidUrl', {url: url}), url);
+        'jsonld.InvalidUrl', {code: 'loading document failed', url: url}),
+        {contextUrl: null, documentUrl: url, document: null});
     }
     var doc = cache.get(url);
     if(doc !== null) {
@@ -1231,7 +1233,8 @@
             return callback(new JsonLdError(
               'URL could not be dereferenced, it has more than one ' +
               'associated HTTP Link Header.',
-              'jsonld.InvalidUrl', {url: url}), doc);
+              'jsonld.InvalidUrl',
+              {code: 'multiple context link headers', url: url}), doc);
           }
           doc.contextUrl = linkHeader.target;
         }
@@ -1242,7 +1245,8 @@
       error: function(jqXHR, textStatus, err) {
         callback(new JsonLdError(
           'URL could not be dereferenced, an error occurred.',
-          'jsonld.LoadDocumentError', {url: url, cause: err}),
+          'jsonld.LoadDocumentError',
+          {code: 'loading document failed', url: url, cause: err}),
           {contextUrl: null, documentUrl: url, document: null});
       }
     });
@@ -1270,7 +1274,7 @@
       return callback(new JsonLdError(
         'URL could not be dereferenced; secure mode is enabled and ' +
         'the URL\'s scheme is not "https".',
-        'jsonld.InvalidUrl', {url: url}),
+        'jsonld.InvalidUrl', {code: 'loading document failed', url: url}),
         {contextUrl: null, documentUrl: url, document: null});
     }
     var doc = cache.get(url);
@@ -1288,14 +1292,18 @@
       if(err) {
         return callback(new JsonLdError(
           'URL could not be dereferenced, an error occurred.',
-          'jsonld.LoadDocumentError', {url: url, cause: err}), doc);
+          'jsonld.LoadDocumentError',
+          {code: 'loading document failed', url: url, cause: err}), doc);
       }
       var statusText = http.STATUS_CODES[res.statusCode];
       if(res.statusCode >= 400) {
         return callback(new JsonLdError(
           'URL could not be dereferenced: ' + statusText,
-          'jsonld.InvalidUrl', {url: url, httpStatusCode: res.statusCode}),
-          doc);
+          'jsonld.InvalidUrl', {
+            code: 'loading document failed',
+            url: url,
+            httpStatusCode: res.statusCode
+          }), doc);
       }
 
       // handle Link Header
@@ -1307,7 +1315,8 @@
           return callback(new JsonLdError(
             'URL could not be dereferenced, it has more than one associated ' +
             'HTTP Link Header.',
-            'jsonld.InvalidUrl', {url: url}), doc);
+            'jsonld.InvalidUrl',
+            {code: 'multiple context link headers', url: url}), doc);
         }
         doc.contextUrl = linkHeader.target;
       }
@@ -1318,16 +1327,22 @@
         if(redirects.length === maxRedirects) {
           return callback(new JsonLdError(
             'URL could not be dereferenced; there were too many redirects.',
-            'jsonld.TooManyRedirects',
-            {url: url, httpStatusCode: res.statusCode, redirects: redirects}),
-            doc);
+            'jsonld.TooManyRedirects', {
+              code: 'loading document failed',
+              url: url,
+              httpStatusCode: res.statusCode,
+              redirects: redirects
+            }), doc);
         }
         if(redirects.indexOf(url) !== -1) {
           return callback(new JsonLdError(
             'URL could not be dereferenced; infinite redirection was detected.',
-            'jsonld.InfiniteRedirectDetected',
-            {url: url, httpStatusCode: res.statusCode, redirects: redirects}),
-            doc);
+            'jsonld.InfiniteRedirectDetected', {
+              code: 'recursive context inclusion',
+              url: url,
+              httpStatusCode: res.statusCode,
+              redirects: redirects
+            }), doc);
         }
         redirects.push(url);
         return loadDocument(res.headers.location, redirects, callback);
@@ -1939,7 +1954,7 @@
               'rule but there is more than a single @list that matches ' +
               'the compacted term in the document. Compaction might mix ' +
               'unwanted items into the list.',
-              'jsonld.SyntaxError');
+              'jsonld.SyntaxError', {code: 'compaction to list of lists'});
           }
         }
 
@@ -2010,21 +2025,34 @@
     return null;
   }
 
+  if(!_isArray(element) && !_isObject(element)) {
+    // drop free-floating scalars that are not in lists
+    if(!insideList && (activeProperty === null ||
+      _expandIri(activeCtx, activeProperty, {vocab: true}) === '@graph')) {
+      return null;
+    }
+
+    // expand element according to value expansion rules
+    return _expandValue(activeCtx, activeProperty, element);
+  }
+
   // recursively expand array
   if(_isArray(element)) {
     var rval = [];
+    var container = jsonld.getContextValue(
+      activeCtx, activeProperty, '@container');
+    insideList = insideList || container === '@list';
     for(var i = 0; i < element.length; ++i) {
       // expand element
-      var e = self.expand(
-        activeCtx, activeProperty, element[i], options, insideList);
+      var e = self.expand(activeCtx, activeProperty, element[i], options);
       if(insideList && (_isArray(e) || _isList(e))) {
         // lists of lists are illegal
         throw new JsonLdError(
           'Invalid JSON-LD syntax; lists of lists are not permitted.',
-          'jsonld.SyntaxError');
+          'jsonld.SyntaxError', {code: 'list of lists'});
       }
       // drop null values
-      else if(e !== null) {
+      if(e !== null) {
         if(_isArray(e)) {
           rval = rval.concat(e);
         }
@@ -2036,344 +2064,340 @@
     return rval;
   }
 
-  // recursively expand object
-  if(_isObject(element)) {
-    // if element has a context, process it
-    if('@context' in element) {
-      activeCtx = self.processContext(activeCtx, element['@context'], options);
-    }
-
-    // expand the active property
-    var expandedActiveProperty = _expandIri(
-      activeCtx, activeProperty, {vocab: true});
-
-    var rval = {};
-    var keys = Object.keys(element).sort();
-    for(var ki = 0; ki < keys.length; ++ki) {
-      var key = keys[ki];
-      var value = element[key];
-      var expandedValue;
-
-      // skip @context
-      if(key === '@context') {
-        continue;
-      }
-
-      // expand key to IRI
-      var expandedProperty = _expandIri(activeCtx, key, {vocab: true});
-
-      // drop non-absolute IRI keys that aren't keywords
-      if(expandedProperty === null ||
-        !(_isAbsoluteIri(expandedProperty) || _isKeyword(expandedProperty))) {
-        continue;
-      }
-
-      if(_isKeyword(expandedProperty) &&
-        expandedActiveProperty === '@reverse') {
+  // recursively expand object:
+
+  // if element has a context, process it
+  if('@context' in element) {
+    activeCtx = self.processContext(activeCtx, element['@context'], options);
+  }
+
+  // expand the active property
+  var expandedActiveProperty = _expandIri(
+    activeCtx, activeProperty, {vocab: true});
+
+  var rval = {};
+  var keys = Object.keys(element).sort();
+  for(var ki = 0; ki < keys.length; ++ki) {
+    var key = keys[ki];
+    var value = element[key];
+    var expandedValue;
+
+    // skip @context
+    if(key === '@context') {
+      continue;
+    }
+
+    // expand property
+    var expandedProperty = _expandIri(activeCtx, key, {vocab: true});
+
+    // drop non-absolute IRI keys that aren't keywords
+    if(expandedProperty === null ||
+      !(_isAbsoluteIri(expandedProperty) || _isKeyword(expandedProperty))) {
+      continue;
+    }
+
+    if(_isKeyword(expandedProperty)) {
+      if(expandedActiveProperty === '@reverse') {
         throw new JsonLdError(
           'Invalid JSON-LD syntax; a keyword cannot be used as a @reverse ' +
-          'property.',
-          'jsonld.SyntaxError', {value: value});
-      }
-
-      // syntax error if @id is not a string
-      if(expandedProperty === '@id' && !_isString(value)) {
-        if(!options.isFrame) {
-          throw new JsonLdError(
-            'Invalid JSON-LD syntax; "@id" value must a string.',
-            'jsonld.SyntaxError', {value: value});
-        }
-        if(!_isObject(value)) {
-          throw new JsonLdError(
-            'Invalid JSON-LD syntax; "@id" value must be a string or an ' +
-            'object.', 'jsonld.SyntaxError', {value: value});
-        }
-      }
-
-      // validate @type value
-      if(expandedProperty === '@type') {
-        _validateTypeValue(value);
-      }
-
-      // @graph must be an array or an object
-      if(expandedProperty === '@graph' &&
-        !(_isObject(value) || _isArray(value))) {
-        throw new JsonLdError(
-          'Invalid JSON-LD syntax; "@value" value must not be an ' +
-          'object or an array.',
-          'jsonld.SyntaxError', {value: value});
-      }
-
-      // @value must not be an object or an array
-      if(expandedProperty === '@value' &&
-        (_isObject(value) || _isArray(value))) {
+          'property.', 'jsonld.SyntaxError',
+          {code: 'invalid reverse property map', value: value});
+      }
+      if(expandedProperty in rval) {
         throw new JsonLdError(
-          'Invalid JSON-LD syntax; "@value" value must not be an ' +
-          'object or an array.',
-          'jsonld.SyntaxError', {value: value});
-      }
-
-      // @language must be a string
-      if(expandedProperty === '@language') {
-        if(!_isString(value)) {
-          throw new JsonLdError(
-            'Invalid JSON-LD syntax; "@language" value must be a string.',
-            'jsonld.SyntaxError', {value: value});
-        }
-        // ensure language value is lowercase
-        value = value.toLowerCase();
-      }
-
-      // @index must be a string
-      if(expandedProperty === '@index') {
-        if(!_isString(value)) {
-          throw new JsonLdError(
-            'Invalid JSON-LD syntax; "@index" value must be a string.',
-            'jsonld.SyntaxError', {value: value});
-        }
-      }
-
-      // @reverse must be an object
-      if(expandedProperty === '@reverse') {
-        if(!_isObject(value)) {
-          throw new JsonLdError(
-            'Invalid JSON-LD syntax; "@reverse" value must be an object.',
-            'jsonld.SyntaxError', {value: value});
-        }
-
-        expandedValue = self.expand(
-          activeCtx, '@reverse', value, options, insideList);
-
-        // properties double-reversed
-        if('@reverse' in expandedValue) {
-          for(var property in expandedValue['@reverse']) {
-            jsonld.addValue(
-              rval, property, expandedValue['@reverse'][property],
-              {propertyIsArray: true});
-          }
+          'Invalid JSON-LD syntax; colliding keywords detected.',
+          'jsonld.SyntaxError',
+          {code: 'colliding keywords', keyword: expandedProperty});
+      }
+    }
+
+    // syntax error if @id is not a string
+    if(expandedProperty === '@id' && !_isString(value)) {
+      if(!options.isFrame) {
+        throw new JsonLdError(
+          'Invalid JSON-LD syntax; "@id" value must a string.',
+          'jsonld.SyntaxError', {code: 'invalid @id value', value: value});
+      }
+      if(!_isObject(value)) {
+        throw new JsonLdError(
+          'Invalid JSON-LD syntax; "@id" value must be a string or an ' +
+          'object.', 'jsonld.SyntaxError',
+          {code: 'invalid @id value', value: value});
+      }
+    }
+
+    if(expandedProperty === '@type') {
+      _validateTypeValue(value);
+    }
+
+    // @graph must be an array or an object
+    if(expandedProperty === '@graph' &&
+      !(_isObject(value) || _isArray(value))) {
+      throw new JsonLdError(
+        'Invalid JSON-LD syntax; "@graph" value must not be an ' +
+        'object or an array.',
+        'jsonld.SyntaxError', {code: 'invalid @graph value', value: value});
+    }
+
+    // @value must not be an object or an array
+    if(expandedProperty === '@value' &&
+      (_isObject(value) || _isArray(value))) {
+      throw new JsonLdError(
+        'Invalid JSON-LD syntax; "@value" value must not be an ' +
+        'object or an array.',
+        'jsonld.SyntaxError',
+        {code: 'invalid value object value', value: value});
+    }
+
+    // @language must be a string
+    if(expandedProperty === '@language') {
+      if(!_isString(value)) {
+        throw new JsonLdError(
+          'Invalid JSON-LD syntax; "@language" value must be a string.',
+          'jsonld.SyntaxError',
+          {code: 'invalid language-tagged string', value: value});
+      }
+      // ensure language value is lowercase
+      value = value.toLowerCase();
+    }
+
+    // @index must be a string
+    if(expandedProperty === '@index') {
+      if(!_isString(value)) {
+        throw new JsonLdError(
+          'Invalid JSON-LD syntax; "@index" value must be a string.',
+          'jsonld.SyntaxError',
+          {code: 'invalid @index value', value: value});
+      }
+    }
+
+    // @reverse must be an object
+    if(expandedProperty === '@reverse') {
+      if(!_isObject(value)) {
+        throw new JsonLdError(
+          'Invalid JSON-LD syntax; "@reverse" value must be an object.',
+          'jsonld.SyntaxError', {code: 'invalid @reverse value', value: value});
+      }
+
+      expandedValue = self.expand(activeCtx, '@reverse', value, options);
+
+      // properties double-reversed
+      if('@reverse' in expandedValue) {
+        for(var property in expandedValue['@reverse']) {
+          jsonld.addValue(
+            rval, property, expandedValue['@reverse'][property],
+            {propertyIsArray: true});
         }
-
-        // FIXME: can this be merged with code below to simplify?
-        // merge in all reversed properties
-        var reverseMap = rval['@reverse'] || null;
-        for(var property in expandedValue) {
-          if(property === '@reverse') {
-            continue;
-          }
-          if(reverseMap === null) {
-            reverseMap = rval['@reverse'] = {};
-          }
-          jsonld.addValue(reverseMap, property, [], {propertyIsArray: true});
-          var items = expandedValue[property];
-          for(var ii = 0; ii < items.length; ++ii) {
-            var item = items[ii];
-            if(_isValue(item) || _isList(item)) {
-              throw new JsonLdError(
-                'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
-                '@value or an @list.',
-                'jsonld.SyntaxError', {value: expandedValue});
-            }
-            jsonld.addValue(
-              reverseMap, property, item, {propertyIsArray: true});
-          }
+      }
+
+      // FIXME: can this be merged with code below to simplify?
+      // merge in all reversed properties
+      var reverseMap = rval['@reverse'] || null;
+      for(var property in expandedValue) {
+        if(property === '@reverse') {
+          continue;
         }
-
-        continue;
-      }
-
-      var container = jsonld.getContextValue(activeCtx, key, '@container');
-
-      // handle language map container (skip if value is not an object)
-      if(container === '@language' && _isObject(value)) {
-        expandedValue = _expandLanguageMap(value);
-      }
-      // handle index container (skip if value is not an object)
-      else if(container === '@index' && _isObject(value)) {
-        expandedValue = (function _expandIndexMap(activeProperty) {
-          var rval = [];
-          var keys = Object.keys(value).sort();
-          for(var ki = 0; ki < keys.length; ++ki) {
-            var key = keys[ki];
-            var val = value[key];
-            if(!_isArray(val)) {
-              val = [val];
-            }
-            val = self.expand(activeCtx, activeProperty, val, options, false);
-            for(var vi = 0; vi < val.length; ++vi) {
-              var item = val[vi];
-              if(!('@index' in item)) {
-                item['@index'] = key;
-              }
-              rval.push(item);
-            }
-          }
-          return rval;
-        })(key);
-      }
-      else {
-        // recurse into @list or @set
-        var isList = (expandedProperty === '@list');
-        if(isList || expandedProperty === '@set') {
-          var nextActiveProperty = activeProperty;
-          if(isList && expandedActiveProperty === '@graph') {
-            nextActiveProperty = null;
-          }
-          expandedValue = self.expand(
-            activeCtx, nextActiveProperty, value, options, isList);
-          if(isList && _isList(expandedValue)) {
-            throw new JsonLdError(
-              'Invalid JSON-LD syntax; lists of lists are not permitted.',
-              'jsonld.SyntaxError');
-          }
+        if(reverseMap === null) {
+          reverseMap = rval['@reverse'] = {};
         }
-        else {
-          // recursively expand value with key as new active property
-          expandedValue = self.expand(activeCtx, key, value, options, false);
-        }
-      }
-
-      // drop null values if property is not @value
-      if(expandedValue === null && expandedProperty !== '@value') {
-        continue;
-      }
-
-      // convert expanded value to @list if container specifies it
-      if(expandedProperty !== '@list' && !_isList(expandedValue) &&
-        container === '@list') {
-        // ensure expanded value is an array
-        expandedValue = (_isArray(expandedValue) ?
-          expandedValue : [expandedValue]);
-        expandedValue = {'@list': expandedValue};
-      }
-
-      // FIXME: can this be merged with code above to simplify?
-      // merge in reverse properties
-      if(activeCtx.mappings[key] && activeCtx.mappings[key].reverse) {
-        var reverseMap = rval['@reverse'] = {};
-        if(!_isArray(expandedValue)) {
-          expandedValue = [expandedValue];
-        }
-        for(var ii = 0; ii < expandedValue.length; ++ii) {
-          var item = expandedValue[ii];
+        jsonld.addValue(reverseMap, property, [], {propertyIsArray: true});
+        var items = expandedValue[property];
+        for(var ii = 0; ii < items.length; ++ii) {
+          var item = items[ii];
           if(_isValue(item) || _isList(item)) {
             throw new JsonLdError(
               'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
-              '@value or an @list.',
-              'jsonld.SyntaxError', {value: expandedValue});
+              '@value or an @list.', 'jsonld.SyntaxError',
+              {code: 'invalid reverse property value', value: expandedValue});
           }
           jsonld.addValue(
-            reverseMap, expandedProperty, item, {propertyIsArray: true});
+            reverseMap, property, item, {propertyIsArray: true});
         }
-        continue;
-      }
-
-      // add value for property
-      // use an array except for certain keywords
-      var useArray =
-        ['@index', '@id', '@type', '@value', '@language'].indexOf(
-          expandedProperty) === -1;
-      jsonld.addValue(
-        rval, expandedProperty, expandedValue, {propertyIsArray: useArray});
-    }
-
-    // get property count on expanded output
-    keys = Object.keys(rval);
-    var count = keys.length;
-
-    if('@value' in rval) {
-      // @value must only have @language or @type
-      if('@type' in rval && '@language' in rval) {
-        throw new JsonLdError(
-          'Invalid JSON-LD syntax; an element containing "@value" may not ' +
-          'contain both "@type" and "@language".',
-          'jsonld.SyntaxError', {element: rval});
-      }
-      var validCount = count - 1;
-      if('@type' in rval) {
-        validCount -= 1;
-      }
-      if('@index' in rval) {
-        validCount -= 1;
-      }
-      if('@language' in rval) {
-        validCount -= 1;
-      }
-      if(validCount !== 0) {
-        throw new JsonLdError(
-          'Invalid JSON-LD syntax; an element containing "@value" may only ' +
-          'have an "@index" property and at most one other property ' +
-          'which can be "@type" or "@language".',
-          'jsonld.SyntaxError', {element: rval});
-      }
-      // drop null @values
-      if(rval['@value'] === null) {
-        rval = null;
-      }
-      // drop @language if @value isn't a string
-      else if('@language' in rval && !_isString(rval['@value'])) {
-        delete rval['@language'];
-      }
-    }
-    // convert @type to an array
-    else if('@type' in rval && !_isArray(rval['@type'])) {
-      rval['@type'] = [rval['@type']];
-    }
-    // handle @set and @list
-    else if('@set' in rval || '@list' in rval) {
-      if(count > 1 && (count !== 2 && '@index' in rval)) {
-        throw new JsonLdError(
-          'Invalid JSON-LD syntax; if an element has the property "@set" ' +
-          'or "@list", then it can have at most one other property that is ' +
-          '"@index".',
-          'jsonld.SyntaxError', {element: rval});
-      }
-      // optimize away @set
-      if('@set' in rval) {
-        rval = rval['@set'];
-        keys = Object.keys(rval);
-        count = keys.length;
-      }
-    }
-    // drop objects with only @language
-    else if(count === 1 && '@language' in rval) {
-      rval = null;
-    }
-
-    // drop certain top-level objects that do not occur in lists
-    if(_isObject(rval) &&
-      !options.keepFreeFloatingNodes && !insideList &&
-      (activeProperty === null || expandedActiveProperty === '@graph')) {
-      // drop empty object or top-level @value
-      if(count === 0 || ('@value' in rval)) {
-        rval = null;
+      }
+
+      continue;
+    }
+
+    var container = jsonld.getContextValue(activeCtx, key, '@container');
+
+    // handle language map container (skip if value is not an object)
+    if(container === '@language' && _isObject(value)) {
+      expandedValue = _expandLanguageMap(value);
+    }
+    // handle index container (skip if value is not an object)
+    else if(container === '@index' && _isObject(value)) {
+      expandedValue = (function _expandIndexMap(activeProperty) {
+        var rval = [];
+        var keys = Object.keys(value).sort();
+        for(var ki = 0; ki < keys.length; ++ki) {
+          var key = keys[ki];
+          var val = value[key];
+          if(!_isArray(val)) {
+            val = [val];
+          }
+          val = self.expand(activeCtx, activeProperty, val, options, false);
+          for(var vi = 0; vi < val.length; ++vi) {
+            var item = val[vi];
+            if(!('@index' in item)) {
+              item['@index'] = key;
+            }
+            rval.push(item);
+          }
+        }
+        return rval;
+      })(key);
+    }
+    else {
+      // recurse into @list or @set
+      var isList = (expandedProperty === '@list');
+      if(isList || expandedProperty === '@set') {
+        var nextActiveProperty = activeProperty;
+        if(isList && expandedActiveProperty === '@graph') {
+          nextActiveProperty = null;
+        }
+        expandedValue = self.expand(
+          activeCtx, nextActiveProperty, value, options, isList);
+        if(isList && _isList(expandedValue)) {
+          throw new JsonLdError(
+            'Invalid JSON-LD syntax; lists of lists are not permitted.',
+            'jsonld.SyntaxError', {code: 'list of lists'});
+        }
       }
       else {
-        // drop nodes that generate no triples
-        var hasTriples = false;
-        var ignore = ['@graph', '@type'];
-        for(var ki = 0; !hasTriples && ki < keys.length; ++ki) {
-          if(!_isKeyword(keys[ki]) || ignore.indexOf(keys[ki]) !== -1) {
-            hasTriples = true;
-          }
-        }
-        if(!hasTriples) {
-          rval = null;
+        // recursively expand value with key as new active property
+        expandedValue = self.expand(activeCtx, key, value, options, false);
+      }
+    }
+
+    // drop null values if property is not @value
+    if(expandedValue === null && expandedProperty !== '@value') {
+      continue;
+    }
+
+    // convert expanded value to @list if container specifies it
+    if(expandedProperty !== '@list' && !_isList(expandedValue) &&
+      container === '@list') {
+      // ensure expanded value is an array
+      expandedValue = (_isArray(expandedValue) ?
+        expandedValue : [expandedValue]);
+      expandedValue = {'@list': expandedValue};
+    }
+
+    // FIXME: can this be merged with code above to simplify?
+    // merge in reverse properties
+    if(activeCtx.mappings[key] && activeCtx.mappings[key].reverse) {
+      var reverseMap = rval['@reverse'] = {};
+      if(!_isArray(expandedValue)) {
+        expandedValue = [expandedValue];
+      }
+      for(var ii = 0; ii < expandedValue.length; ++ii) {
+        var item = expandedValue[ii];
+        if(_isValue(item) || _isList(item)) {
+          throw new JsonLdError(
+            'Invalid JSON-LD syntax; "@reverse" value must not be a ' +
+            '@value or an @list.', 'jsonld.SyntaxError',
+            {code: 'invalid reverse property value', value: expandedValue});
         }
-      }
-    }
-
-    return rval;
-  }
-
-  // drop top-level scalars that are not in lists
-  if(!insideList &&
-    (activeProperty === null ||
-    _expandIri(activeCtx, activeProperty, {vocab: true}) === '@graph')) {
-    return null;
-  }
-
-  // expand element according to value expansion rules
-  return _expandValue(activeCtx, activeProperty, element);
+        jsonld.addValue(
+          reverseMap, expandedProperty, item, {propertyIsArray: true});
+      }
+      continue;
+    }
+
+    // add value for property
+    // use an array except for certain keywords
+    var useArray =
+      ['@index', '@id', '@type', '@value', '@language'].indexOf(
+        expandedProperty) === -1;
+    jsonld.addValue(
+      rval, expandedProperty, expandedValue, {propertyIsArray: useArray});
+  }
+
+  // get property count on expanded output
+  keys = Object.keys(rval);
+  var count = keys.length;
+
+  if('@value' in rval) {
+    // @value must only have @language or @type
+    if('@type' in rval && '@language' in rval) {
+      throw new JsonLdError(
+        'Invalid JSON-LD syntax; an element containing "@value" may not ' +
+        'contain both "@type" and "@language".',
+        'jsonld.SyntaxError', {code: 'invalid value object', element: rval});
+    }
+    var validCount = count - 1;
+    if('@type' in rval) {
+      validCount -= 1;
+    }
+    if('@index' in rval) {
+      validCount -= 1;
+    }
+    if('@language' in rval) {
+      validCount -= 1;
+    }
+    if(validCount !== 0) {
+      throw new JsonLdError(
+        'Invalid JSON-LD syntax; an element containing "@value" may only ' +
+        'have an "@index" property and at most one other property ' +
+        'which can be "@type" or "@language".',
+        'jsonld.SyntaxError', {code: 'invalid value object', element: rval});
+    }
+    // drop null @values
+    if(rval['@value'] === null) {
+      rval = null;
+    }
+    // if @language is present, @value must be a string
+    else if('@language' in rval && !_isString(rval['@value'])) {
+      throw new JsonLdError(
+        'Invalid JSON-LD syntax; only strings may be language-tagged.',
+        'jsonld.SyntaxError',
+        {code: 'invalid language-tagged value', element: rval});
+    }
+    else if('@type' in rval && (!_isAbsoluteIri(rval['@type']) ||
+      rval['@type'].indexOf('_:') === 0)) {
+      throw new JsonLdError(
+        'Invalid JSON-LD syntax; an element containing "@value" and "@type" ' +
+        'must have an absolute IRI for the value of "@type".',
+        'jsonld.SyntaxError', {code: 'invalid typed value', element: rval});
+    }
+  }
+  // convert @type to an array
+  else if('@type' in rval && !_isArray(rval['@type'])) {
+    rval['@type'] = [rval['@type']];
+  }
+  // handle @set and @list
+  else if('@set' in rval || '@list' in rval) {
+    if(count > 1 && !(count === 2 && '@index' in rval)) {
+      throw new JsonLdError(
+        'Invalid JSON-LD syntax; if an element has the property "@set" ' +
+        'or "@list", then it can have at most one other property that is ' +
+        '"@index".', 'jsonld.SyntaxError',
+        {code: 'invalid set or list object', element: rval});
+    }
+    // optimize away @set
+    if('@set' in rval) {
+      rval = rval['@set'];
+      keys = Object.keys(rval);
+      count = keys.length;
+    }
+  }
+  // drop objects with only @language
+  else if(count === 1 && '@language' in rval) {
+    rval = null;
+  }
+
+  // drop certain top-level objects that do not occur in lists
+  if(_isObject(rval) &&
+    !options.keepFreeFloatingNodes && !insideList &&
+    (activeProperty === null || expandedActiveProperty === '@graph')) {
+    // drop empty object, top-level @value/@list, or object with only @id
+    if(count === 0 || '@value' in rval || '@list' in rval ||
+      (count === 1 && '@id' in rval)) {
+      rval = null;
+    }
+  }
+
+  return rval;
 };
 
 /**
@@ -2749,9 +2773,8 @@
       // 1. Be used only once in a list.
       // 2. Have an array for rdf:first that has 1 item.
       // 3. Have an array for rdf:rest that has 1 item.
-      // 4. Have no keys other than: @id, usages, rdf:first, rdf:rest and,
+      // 4. Have no keys other than: @id, usages, rdf:first, rdf:rest, and,
       //   optionally, @type where the value is rdf:List.
-      // 5. Not already be in a list (it is in the eliminated nodes map).
       var nodeKeyCount = Object.keys(node).length;
       while(property === RDF_REST && node.usages.length === 1 &&
         _isArray(node[RDF_FIRST]) && node[RDF_FIRST].length === 1 &&
@@ -2774,7 +2797,7 @@
         }
       }
 
-      // the list is nested another list
+      // the list is nested in another list
       if(property === RDF_FIRST) {
         // empty list
         if(node['@id'] === RDF_NIL) {
@@ -2844,7 +2867,10 @@
   var graphNames = Object.keys(nodeMap).sort();
   for(var i = 0; i < graphNames.length; ++i) {
     var graphName = graphNames[i];
-    dataset[graphName] = _graphToRDF(nodeMap[graphName], namer, options);
+    // skip relative IRIs
+    if(graphName === '@default' || _isAbsoluteIri(graphName)) {
+      dataset[graphName] = _graphToRDF(nodeMap[graphName], namer, options);
+    }
   }
   return dataset;
 };
@@ -2893,7 +2919,7 @@
     if(!_isObject(ctx)) {
       throw new JsonLdError(
         'Invalid JSON-LD syntax; @context must be an object.',
-        'jsonld.SyntaxError', {context: ctx});
+        'jsonld.SyntaxError', {code: 'invalid local context', context: ctx});
     }
 
     // get context from cache if available
@@ -2927,13 +2953,13 @@
         throw new JsonLdError(
           'Invalid JSON-LD syntax; the value of "@base" in a ' +
           '@context must be a string or null.',
-          'jsonld.SyntaxError', {context: ctx});
+          'jsonld.SyntaxError', {code: 'invalid base IRI', context: ctx});
       }
       else if(base !== '' && !_isAbsoluteIri(base)) {
         throw new JsonLdError(
           'Invalid JSON-LD syntax; the value of "@base" in a ' +
           '@context must be an absolute IRI or the empty string.',
-          'jsonld.SyntaxError', {context: ctx});
+          'jsonld.SyntaxError', {code: 'invalid base IRI', context: ctx});
       }
 
       base = jsonld.url.parse(base || '');
@@ -2951,13 +2977,13 @@
         throw new JsonLdError(
           'Invalid JSON-LD syntax; the value of "@vocab" in a ' +
           '@context must be a string or null.',
-          'jsonld.SyntaxError', {context: ctx});
+          'jsonld.SyntaxError', {code: 'invalid vocab mapping', context: ctx});
       }
       else if(!_isAbsoluteIri(value)) {
         throw new JsonLdError(
           'Invalid JSON-LD syntax; the value of "@vocab" in a ' +
           '@context must be an absolute IRI.',
-          'jsonld.SyntaxError', {context: ctx});
+          'jsonld.SyntaxError', {code: 'invalid vocab mapping', context: ctx});
       }
       else {
         rval['@vocab'] = value;
@@ -2975,7 +3001,8 @@
         throw new JsonLdError(
           'Invalid JSON-LD syntax; the value of "@language" in a ' +
           '@context must be a string or null.',
-          'jsonld.SyntaxError', {context: ctx});
+          'jsonld.SyntaxError',
+          {code: 'invalid default language', context: ctx});
       }
       else {
         rval['@language'] = value.toLowerCase();
@@ -3018,7 +3045,8 @@
       if(!_isString(item)) {
         throw new JsonLdError(
           'Invalid JSON-LD syntax; language map values must be strings.',
-          'jsonld.SyntaxError', {languageMap: languageMap});
+          'jsonld.SyntaxError',
+          {code: 'invalid language map value', languageMap: languageMap});
       }
       rval.push({
         '@value': item,
@@ -3161,11 +3189,21 @@
         subject.type = (id.indexOf('_:') === 0) ? 'blank node' : 'IRI';
         subject.value = id;
 
+        // skip relative IRI subjects
+        if(!_isAbsoluteIri(id)) {
+          continue;
+        }
+
         // RDF predicate
         var predicate = {};
         predicate.type = (property.indexOf('_:') === 0) ? 'blank node' : 'IRI';
         predicate.value = property;
 
+        // skip relative IRI predicates
+        if(!_isAbsoluteIri(property)) {
+          continue;
+        }
+
         // skip blank node predicates unless producing generalized RDF
         if(predicate.type === 'blank node' && !options.produceGeneralizedRdf) {
           continue;
@@ -3178,7 +3216,11 @@
         // convert value or node object to triple
         else {
           var object = _objectToRDF(item);
-          rval.push({subject: subject, predicate: predicate, object: object});
+
+          // skip null objects (they are relative IRIs)
+          if(object) {
+            rval.push({subject: subject, predicate: predicate, object: object});
+          }
         }
       }
     }
@@ -3211,7 +3253,11 @@
     subject = blankNode;
     predicate = first;
     var object = _objectToRDF(item);
-    triples.push({subject: subject, predicate: predicate, object: object});
+
+    // skip null objects (they are relative IRIs)
+    if(object) {
+      triples.push({subject: subject, predicate: predicate, object: object});
+    }
 
     predicate = rest;
   }
@@ -3267,6 +3313,11 @@
     object.value = id;
   }
 
+  // skip relative IRIs
+  if(object.type === 'IRI' && !_isAbsoluteIri(object.value)) {
+    return null;
+  }
+
   return object;
 }
 
@@ -3683,7 +3734,8 @@
       if(property === '@index' && '@index' in subject) {
         throw new JsonLdError(
           'Invalid JSON-LD syntax; conflicting @index property detected.',
-          'jsonld.SyntaxError', {subject: subject});
+          'jsonld.SyntaxError',
+          {code: 'conflicting indexes', subject: subject});
       }
       subject[property] = input[property];
       continue;
@@ -4558,7 +4610,8 @@
     // cycle detected
     throw new JsonLdError(
       'Cyclical context definition detected.',
-      'jsonld.CyclicalContext', {context: localCtx, term: term});
+      'jsonld.CyclicalContext',
+      {code: 'cyclic IRI mapping', context: localCtx, term: term});
   }
 
   // now defining term
@@ -4567,7 +4620,7 @@
   if(_isKeyword(term)) {
     throw new JsonLdError(
       'Invalid JSON-LD syntax; keywords cannot be overridden.',
-      'jsonld.SyntaxError', {context: localCtx});
+      'jsonld.SyntaxError', {code: 'keyword redefinition', context: localCtx});
   }
 
   // remove old mapping
@@ -4594,7 +4647,8 @@
     throw new JsonLdError(
       'Invalid JSON-LD syntax; @context property values must be ' +
       'strings or objects.',
-      'jsonld.SyntaxError', {context: localCtx});
+      'jsonld.SyntaxError',
+      {code: 'invalid term definition', context: localCtx});
   }
 
   // create new mapping
@@ -4605,19 +4659,26 @@
     if('@id' in value) {
       throw new JsonLdError(
         'Invalid JSON-LD syntax; a @reverse term definition must not ' +
-        'contain @id.',
-        'jsonld.SyntaxError', {context: localCtx});
+        'contain @id.', 'jsonld.SyntaxError',
+        {code: 'invalid reverse property', context: localCtx});
     }
     var reverse = value['@reverse'];
     if(!_isString(reverse)) {
       throw new JsonLdError(
         'Invalid JSON-LD syntax; a @context @reverse value must be a string.',
-        'jsonld.SyntaxError', {context: localCtx});
+        'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx});
     }
 
     // expand and add @id mapping
-    mapping['@id'] = _expandIri(
+    var id = _expandIri(
       activeCtx, reverse, {vocab: true, base: false}, localCtx, defined);
+    if(!_isAbsoluteIri(id)) {
+      throw new JsonLdError(
+        'Invalid JSON-LD syntax; a @context @reverse value must be an IRI ' +
+        'or a blank node identifier.',
+        'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx});
+    }
+    mapping['@id'] = id;
     mapping.reverse = true;
   }
   else if('@id' in value) {
@@ -4626,7 +4687,7 @@
       throw new JsonLdError(
         'Invalid JSON-LD syntax; a @context @id value must be an array ' +
         'of strings or a string.',
-        'jsonld.SyntaxError', {context: localCtx});
+        'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx});
     }
     if(id !== term) {
       // expand and add @id mapping
@@ -4660,7 +4721,8 @@
       if(!('@vocab' in activeCtx)) {
         throw new JsonLdError(
           'Invalid JSON-LD syntax; @context terms must define an @id.',
-          'jsonld.SyntaxError', {context: localCtx, term: term});
+          'jsonld.SyntaxError',
+          {code: 'invalid IRI mapping', context: localCtx, term: term});
       }
       // prepend vocab to term
       mapping['@id'] = activeCtx['@vocab'] + term;
@@ -4674,14 +4736,29 @@
     var type = value['@type'];
     if(!_isString(type)) {
       throw new JsonLdError(
-        'Invalid JSON-LD syntax; @context @type values must be strings.',
-        'jsonld.SyntaxError', {context: localCtx});
-    }
-
-    if(type !== '@id') {
+        'Invalid JSON-LD syntax; an @context @type values must be a string.',
+        'jsonld.SyntaxError',
+        {code: 'invalid type mapping', context: localCtx});
+    }
+
+    if(type !== '@id' && type !== '@vocab') {
       // expand @type to full IRI
       type = _expandIri(
-        activeCtx, type, {vocab: true, base: true}, localCtx, defined);
+        activeCtx, type, {vocab: true, base: false}, localCtx, defined);
+      if(!_isAbsoluteIri(type)) {
+        throw new JsonLdError(
+          'Invalid JSON-LD syntax; an @context @type value must be an ' +
+          'absolute IRI.',
+          'jsonld.SyntaxError',
+          {code: 'invalid type mapping', context: localCtx});
+      }
+      if(type.indexOf('_:') === 0) {
+        throw new JsonLdError(
+          'Invalid JSON-LD syntax; an @context @type values must be an IRI, ' +
+          'not a blank node identifier.',
+          'jsonld.SyntaxError',
+          {code: 'invalid type mapping', context: localCtx});
+      }
     }
 
     // add @type to mapping
@@ -4695,14 +4772,15 @@
       throw new JsonLdError(
         'Invalid JSON-LD syntax; @context @container value must be ' +
         'one of the following: @list, @set, @index, or @language.',
-        'jsonld.SyntaxError', {context: localCtx});
+        'jsonld.SyntaxError',
+        {code: 'invalid container mapping', context: localCtx});
     }
     if(mapping.reverse && container !== '@index' && container !== '@set' &&
       container !== null) {
       throw new JsonLdError(
         'Invalid JSON-LD syntax; @context @container value for a @reverse ' +
-        'type definition must be @index or @set.',
-        'jsonld.SyntaxError', {context: localCtx});
+        'type definition must be @index or @set.', 'jsonld.SyntaxError',
+        {code: 'invalid reverse property', context: localCtx});
     }
 
     // add @container to mapping
@@ -4714,8 +4792,8 @@
     if(language !== null && !_isString(language)) {
       throw new JsonLdError(
         'Invalid JSON-LD syntax; @context @language value must be ' +
-        'a string or null.',
-        'jsonld.SyntaxError', {context: localCtx});
+        'a string or null.', 'jsonld.SyntaxError',
+        {code: 'invalid language mapping', context: localCtx});
     }
 
     // add @language to mapping
@@ -4730,7 +4808,7 @@
   if(id === '@context' || id === '@preserve') {
     throw new JsonLdError(
       'Invalid JSON-LD syntax; @context and @preserve cannot be aliased.',
-      'jsonld.SyntaxError');
+      'jsonld.SyntaxError', {code: 'invalid keyword alias', context: localCtx});
   }
 }
 
@@ -4820,8 +4898,8 @@
     if(!_isAbsoluteIri(rval)) {
       throw new JsonLdError(
         'Invalid JSON-LD syntax; a @context value does not expand to ' +
-        'an absolute IRI.',
-        'jsonld.SyntaxError', {context: localCtx, value: value});
+        'an absolute IRI.', 'jsonld.SyntaxError',
+        {code: 'invalid IRI mapping', context: localCtx, value: value});
     }
   }
 
@@ -5205,7 +5283,8 @@
   if(!isValid) {
     throw new JsonLdError(
       'Invalid JSON-LD syntax; "@type" value must a string, an array of ' +
-      'strings, or an empty object.', 'jsonld.SyntaxError', {value: v});
+      'strings, or an empty object.', 'jsonld.SyntaxError',
+      {code: 'invalid type value', value: v});
   }
 }
 
@@ -5499,7 +5578,8 @@
     if(Object.keys(cycles).length > MAX_CONTEXT_URLS) {
       error = new JsonLdError(
         'Maximum number of @context URLs exceeded.',
-        'jsonld.ContextUrlError', {max: MAX_CONTEXT_URLS});
+        'jsonld.ContextUrlError',
+        {code: 'loading remote context failed', max: MAX_CONTEXT_URLS});
       return callback(error);
     }
 
@@ -5526,7 +5606,8 @@
         // validate URL
         if(!regex.test(url)) {
           error = new JsonLdError(
-            'Malformed URL.', 'jsonld.InvalidUrl', {url: url});
+            'Malformed URL.', 'jsonld.InvalidUrl',
+            {code: 'loading remote context failed', url: url});
           return callback(error);
         }
         queue.push(url);
@@ -5541,7 +5622,8 @@
         if(url in cycles) {
           error = new JsonLdError(
             'Cyclical @context URLs detected.',
-            'jsonld.ContextUrlError', {url: url});
+            'jsonld.ContextUrlError',
+            {code: 'recursive context inclusion', url: url});
           return callback(error);
         }
         var _cycles = _clone(cycles);
@@ -5574,13 +5656,15 @@
               'using client-side JavaScript), too many redirects, a ' +
               'non-JSON response, or more than one HTTP Link Header was ' +
               'provided for a remote context.',
-              'jsonld.InvalidUrl', {url: url, cause: err});
+              'jsonld.InvalidUrl',
+              {code: 'loading remote context failed', url: url, cause: err});
           }
           else if(!_isObject(ctx)) {
             err = new JsonLdError(
               'Derefencing a URL did not result in a JSON object. The ' +
               'response was valid JSON, but it was not a JSON object.',
-              'jsonld.InvalidUrl', {url: url, cause: err});
+              'jsonld.InvalidUrl',
+              {code: 'invalid remote context', url: url, cause: err});
           }
           if(err) {
             error = err;
@@ -6662,7 +6746,7 @@
 }
 // export AMD API
 else if(typeof define === 'function' && define.amd) {
-  define('jsonld', [], function() {
+  define([], function() {
     return factory;
   });
 }