Update to latest jsonld.js.
authorDave Longley <dlongley@digitalbazaar.com>
Sun, 08 Sep 2013 02:19:56 -0400
changeset 1988 f5a4cac779dc
parent 1987 22bb3509f8c5
child 1989 967c329d53d2
Update to latest jsonld.js.
playground/jsonld.js
--- a/playground/jsonld.js	Sun Sep 08 02:19:32 2013 -0400
+++ b/playground/jsonld.js	Sun Sep 08 02:19:56 2013 -0400
@@ -257,9 +257,6 @@
   options = options || {};
 
   // set default options
-  if(!('base' in options)) {
-    options.base = (typeof input === 'string') ? input : '';
-  }
   if(!('documentLoader' in options)) {
     options.documentLoader = jsonld.loadDocument;
   }
@@ -270,18 +267,46 @@
   jsonld.nextTick(function() {
     // if input is a string, attempt to dereference remote document
     if(typeof input === 'string') {
-      return options.documentLoader(input, function(err, remoteDoc) {
+      var done = function(err, remoteDoc) {
         if(err) {
           return callback(err);
         }
+        try {
+          if(!remoteDoc.document) {
+            throw new JsonLdError(
+              'No remote document found at the given URL.',
+              'jsonld.NullRemoteDocument');
+          }
+          if(typeof remoteDoc.document === 'string') {
+            remoteDoc.document = JSON.parse(remoteDoc.document);
+          }
+        }
+        catch(ex) {
+          return callback(new JsonLdError(
+            'Could not retrieve a JSON-LD document from the URL. URL ' +
+            'derefencing not implemented.', 'jsonld.LoadDocumentError', {
+              code: 'loading document failed',
+              cause: ex,
+              remoteDoc: remoteDoc
+          }));
+        }
         expand(remoteDoc);
-      });
+      };
+      var promise = options.documentLoader(input, done);
+      if(promise && 'then' in promise) {
+        promise.then(done.bind(null, null), done);
+      }
+      return;
     }
     // nothing to load
     expand({contextUrl: null, documentUrl: null, document: input});
   });
 
   function expand(remoteDoc) {
+    // set default base
+    if(!('base' in options)) {
+      options.base = remoteDoc.documentUrl || '';
+    }
     // build meta-object and retrieve all @context URLs
     var input = {
       document: _clone(remoteDoc.document),
@@ -456,12 +481,36 @@
   jsonld.nextTick(function() {
     // if frame is a string, attempt to dereference remote document
     if(typeof frame === 'string') {
-      return options.documentLoader(frame, function(err, remoteDoc) {
+      var done = function(err, remoteDoc) {
         if(err) {
           return callback(err);
         }
+        try {
+          if(!remoteDoc.document) {
+            throw new JsonLdError(
+              'No remote document found at the given URL.',
+              'jsonld.NullRemoteDocument');
+          }
+          if(typeof remoteDoc.document === 'string') {
+            remoteDoc.document = JSON.parse(remoteDoc.document);
+          }
+        }
+        catch(ex) {
+          return callback(new JsonLdError(
+            'Could not retrieve a JSON-LD document from the URL. URL ' +
+            'derefencing not implemented.', 'jsonld.LoadDocumentError', {
+              code: 'loading document failed',
+              cause: ex,
+              remoteDoc: remoteDoc
+          }));
+        }
         doFrame(remoteDoc);
-      });
+      };
+      var promise = options.documentLoader(frame, done);
+      if(promise && 'then' in promise) {
+        promise.then(done.bind(null, null), done);
+      }
+      return;
     }
     // nothing to load
     doFrame({contextUrl: null, documentUrl: null, document: frame});
@@ -852,17 +901,27 @@
 };
 
 /**
- * The default document loader for external documents.
+ * 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.
+ * @param callback(err, remoteDoc) called once the operation completes,
+ *          if using a non-promises API.
+ *
+ * @return a promise, if using a promises API.
  */
 jsonld.documentLoader = function(url, callback) {
-  return callback(new JsonLdError(
+  var err = new JsonLdError(
     'Could not retrieve a JSON-LD document from the URL. URL derefencing not ' +
     'implemented.', 'jsonld.LoadDocumentError',
-    {code: 'loading document failed'}),
-    {contextUrl: null, documentUrl: url, document: null});
+    {code: 'loading document failed'});
+  if(_nodejs) {
+    return callback(err, {contextUrl: null, documentUrl: url, document: null});
+  }
+  return jsonld.promisify(function(callback) {
+    callback(err);
+  });
 };
 
 /**
@@ -870,63 +929,29 @@
  * instead.
  */
 jsonld.loadDocument = function(url, callback) {
-  jsonld.documentLoader(url, callback);
+  var promise = jsonld.documentLoader(url, callback);
+  if(promise && 'then' in promise) {
+    promise.then(callback.bind(null, null), callback);
+  }
 };
 
 /* Promises API */
 
 jsonld.promises = function() {
-  var Promise = _nodejs ? require('./Promise').Promise : global.Promise;
   var slice = Array.prototype.slice;
-
-  // converts a node.js async op into a promise w/boxed resolved value(s)
-  function promisify(op) {
-    var args = slice.call(arguments, 1);
-    return new Promise(function(resolver) {
-      op.apply(null, args.concat(function(err, value) {
-        if(err) {
-          resolver.reject(err);
-        }
-        else {
-          resolver.resolve(value);
-        }
-      }));
-    });
-  }
-
-  // converts a load document promise callback to a node-style callback
-  function createDocumentLoader(promise) {
-    return function(url, callback) {
-      promise(url).then(
-        // success
-        function(remoteDocument) {
-          callback(null, remoteDocument);
-        },
-        // failure
-        callback
-      );
-    };
-  }
+  var promisify = jsonld.promisify;
 
   var api = {};
   api.expand = function(input) {
     if(arguments.length < 1) {
       throw new TypeError('Could not expand, too few arguments.');
     }
-    var options = (arguments.length > 1) ? arguments[1] : {};
-    if('documentLoader' in options) {
-      options.documentLoader = createDocumentLoader(options.documentLoader);
-    }
     return promisify.apply(null, [jsonld.expand].concat(slice.call(arguments)));
   };
   api.compact = function(input, ctx) {
     if(arguments.length < 2) {
       throw new TypeError('Could not compact, too few arguments.');
     }
-    var options = (arguments.length > 2) ? arguments[2] : {};
-    if('documentLoader' in options) {
-      options.documentLoader = createDocumentLoader(options.documentLoader);
-    }
     var compact = function(input, ctx, options, callback) {
       // ensure only one value is returned in callback
       jsonld.compact(input, ctx, options, function(err, compacted) {
@@ -939,10 +964,6 @@
     if(arguments.length < 1) {
       throw new TypeError('Could not flatten, too few arguments.');
     }
-    var options = (arguments.length > 2) ? arguments[2] : {};
-    if('documentLoader' in options) {
-      options.documentLoader = createDocumentLoader(options.documentLoader);
-    }
     return promisify.apply(
       null, [jsonld.flatten].concat(slice.call(arguments)));
   };
@@ -950,10 +971,6 @@
     if(arguments.length < 2) {
       throw new TypeError('Could not frame, too few arguments.');
     }
-    var options = (arguments.length > 2) ? arguments[2] : {};
-    if('documentLoader' in options) {
-      options.documentLoader = createDocumentLoader(options.documentLoader);
-    }
     return promisify.apply(null, [jsonld.frame].concat(slice.call(arguments)));
   };
   api.fromRDF = function(dataset) {
@@ -967,26 +984,40 @@
     if(arguments.length < 1) {
       throw new TypeError('Could not convert to RDF, too few arguments.');
     }
-    var options = (arguments.length > 1) ? arguments[1] : {};
-    if('documentLoader' in options) {
-      options.documentLoader = createDocumentLoader(options.documentLoader);
-    }
     return promisify.apply(null, [jsonld.toRDF].concat(slice.call(arguments)));
   };
   api.normalize = function(input) {
     if(arguments.length < 1) {
       throw new TypeError('Could not normalize, too few arguments.');
     }
-    var options = (arguments.length > 1) ? arguments[1] : {};
-    if('documentLoader' in options) {
-      options.documentLoader = createDocumentLoader(options.documentLoader);
-    }
     return promisify.apply(
       null, [jsonld.normalize].concat(slice.call(arguments)));
   };
   return api;
 };
 
+/**
+ * Converts a node.js async op into a promise w/boxed resolved value(s).
+ *
+ * @param op the operation to convert.
+ *
+ * @return the promise.
+ */
+jsonld.promisify = function(op) {
+  var Promise = _nodejs ? require('./Promise').Promise : global.Promise;
+  var args = Array.prototype.slice.call(arguments, 1);
+  return new Promise(function(resolver) {
+    op.apply(null, args.concat(function(err, value) {
+      if(err) {
+        resolver.reject(err);
+      }
+      else {
+        resolver.resolve(value);
+      }
+    }));
+  });
+};
+
 /* WebIDL API */
 
 function JsonLdProcessor() {}
@@ -1100,7 +1131,7 @@
     while(match = rParams.exec(params)) {
       result[match[1]] = (match[2] === undefined) ? match[3] : match[2];
     }
-    var rel = result['rel'];
+    var rel = result['rel'] || '';
     if(_isArray(rval[rel])) {
       rval[rel].push(result);
     }
@@ -1194,18 +1225,20 @@
 jsonld.documentLoaders = {};
 
 /**
- * The built-in jquery document loader.
+ * Creates a built-in jquery document loader.
  *
  * @param $ the jquery instance to use.
  * @param options the options to use:
  *          secure: require all URLs to use HTTPS.
+ *          usePromise: true to use a promises API, false for a
+ *            callback-continuation-style API; true by default.
  *
  * @return the jquery document loader.
  */
-jsonld.documentLoaders['jquery'] = function($, options) {
+jsonld.documentLoaders.jquery = function($, options) {
   options = options || {};
   var cache = new jsonld.DocumentCache();
-  return function(url, callback) {
+  var loader = function(url, callback) {
     if(options.secure && url.indexOf('https') !== 0) {
       return callback(new JsonLdError(
         'URL could not be dereferenced; secure mode is enabled and ' +
@@ -1225,8 +1258,9 @@
         var doc = {contextUrl: null, documentUrl: url, document: data};
 
         // handle Link Header
+        var contentType = jqXHR.getResponseHeader('Content-Type');
         var linkHeader = jqXHR.getResponseHeader('Link');
-        if(linkHeader) {
+        if(linkHeader && contentType !== 'application/ld+json') {
           // only 1 related link header permitted
           linkHeader = jsonld.parseLinkHeader(linkHeader)[LINK_HEADER_REL];
           if(_isArray(linkHeader)) {
@@ -1236,7 +1270,9 @@
               'jsonld.InvalidUrl',
               {code: 'multiple context link headers', url: url}), doc);
           }
-          doc.contextUrl = linkHeader.target;
+          if(linkHeader) {
+            doc.contextUrl = linkHeader.target;
+          }
         }
 
         cache.set(url, doc);
@@ -1251,19 +1287,28 @@
       }
     });
   };
+
+  if(!('usePromise' in options) || options.usePromise) {
+    return function(url) {
+      return jsonld.promisify(loader, url);
+    };
+  }
+  return loader;
 };
 
 /**
- * The built-in node document loader.
+ * Creates a built-in node document loader.
  *
  * @param options the options to use:
  *          secure: require all URLs to use HTTPS.
  *          maxRedirects: the maximum number of redirects to permit, none by
  *            default.
+ *          usePromise: true to use a promises API, false for a
+ *            callback-continuation-style API; false by default.
  *
  * @return the node document loader.
  */
-jsonld.documentLoaders['node'] = function(options) {
+jsonld.documentLoaders.node = function(options) {
   options = options || {};
   var maxRedirects = ('maxRedirects' in options) ? options.maxRedirects : -1;
   var request = require('request');
@@ -1307,7 +1352,8 @@
       }
 
       // handle Link Header
-      if(res.headers.link) {
+      if(res.headers.link &&
+        res.headers['content-type'] !== 'application/ld+json') {
         // only 1 related link header permitted
         var linkHeader = jsonld.parseLinkHeader(
           res.headers.link)[LINK_HEADER_REL];
@@ -1318,7 +1364,9 @@
             'jsonld.InvalidUrl',
             {code: 'multiple context link headers', url: url}), doc);
         }
-        doc.contextUrl = linkHeader.target;
+        if(linkHeader) {
+          doc.contextUrl = linkHeader.target;
+        }
       }
 
       // handle redirect
@@ -1358,16 +1406,22 @@
     });
   }
 
-  return function(url, callback) {
+  var loader = function(url, callback) {
     loadDocument(url, [], callback);
   };
+  if(options.usePromise) {
+    return function(url) {
+      return jsonld.promisify(loader, url);
+    };
+  }
+  return loader;
 };
 
 /**
  * Assigns the default document loader for external document URLs to a built-in
  * default. Supported types currently include: 'jquery' and 'node'.
  *
- * To use the jquery document loader, the 'data' parameter must be a reference
+ * To use the jquery document loader, the first parameter must be a reference
  * to the main jquery object.
  *
  * @param type the type to set.
@@ -4674,8 +4728,8 @@
       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.',
+        'Invalid JSON-LD syntax; a @context @reverse value must be an ' +
+        'absolute IRI or a blank node identifier.',
         'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx});
     }
     mapping['@id'] = id;
@@ -4691,8 +4745,16 @@
     }
     if(id !== term) {
       // expand and add @id mapping
-      mapping['@id'] = _expandIri(
+      id = _expandIri(
         activeCtx, id, {vocab: true, base: false}, localCtx, defined);
+      if(!_isAbsoluteIri(id) && !_isKeyword(id)) {
+        throw new JsonLdError(
+          'Invalid JSON-LD syntax; a @context @id value must be an ' +
+          'absolute IRI, a blank node identifier, or a keyword.',
+          'jsonld.SyntaxError',
+          {code: 'invalid IRI mapping', context: localCtx});
+      }
+      mapping['@id'] = id;
     }
   }
 
@@ -4893,16 +4955,6 @@
     rval = _prependBase(activeCtx['@base'], rval);
   }
 
-  if(localCtx) {
-    // value must now be an absolute IRI
-    if(!_isAbsoluteIri(rval)) {
-      throw new JsonLdError(
-        'Invalid JSON-LD syntax; a @context value does not expand to ' +
-        'an absolute IRI.', 'jsonld.SyntaxError',
-        {code: 'invalid IRI mapping', context: localCtx, value: value});
-    }
-  }
-
   return rval;
 }
 
@@ -5628,8 +5680,7 @@
         }
         var _cycles = _clone(cycles);
         _cycles[url] = true;
-
-        documentLoader(url, function(err, remoteDoc) {
+        var done = function(err, remoteDoc) {
           // short-circuit if there was an error with another URL
           if(error) {
             return;
@@ -5698,7 +5749,11 @@
               finished();
             }
           });
-        });
+        };
+        var promise = documentLoader(url, done);
+        if(promise && 'then' in promise) {
+          promise.then(done.bind(null, null), done);
+        }
       }(queue[i]));
     }
   };