Updated jsonld.js, clean up and bug fixes, @context URL support.
authorDave Longley <dlongley@digitalbazaar.com>
Thu, 27 Oct 2011 15:02:49 -0400
changeset 244 5936e137987d
parent 243 12043ab916ff
child 245 8207d5f5a736
Updated jsonld.js, clean up and bug fixes, @context URL support.
playground/index.html
playground/jsonld.js
playground/playground.js
--- a/playground/index.html	Mon Oct 24 15:16:52 2011 +0800
+++ b/playground/index.html	Thu Oct 27 15:02:49 2011 -0400
@@ -46,34 +46,20 @@
 
             <ul id="examples">
               <li class="button-list">Simple Examples:</li>
-              <li class="button">
-                 <span onmousedown="playground.populateWithExample('Person')">Person</span>
-              </li>
-              <li class="button">
-                 <span onmousedown="playground.populateWithExample('Event')">Event</span>
-              </li>
-              <li class="button">
-                 <span onmousedown="playground.populateWithExample('Place')">Place</span>
-              </li>
-              <li class="button">
-                 <span onmousedown="playground.populateWithExample('Product')">Product</span>
-              </li>
-              <li class="button">
-                 <span onmousedown="playground.populateWithExample('Recipe')">Recipe</span>
-              </li>
+              <li class="button"><span>Person</span></li>
+              <li class="button"><span>Event</span></li>
+              <li class="button"><span>Place</span></li>
+              <li class="button"><span>Product</span></li>
+              <li class="button"><span>Recipe</span></li>
               <li class="button-list">Framing Examples:</li>
-              <li class="button">
-                 <span onmousedown="playground.populateWithExample('Library')">Library</span>
-              </li>
+              <li class="button"><span>Library</span></li>
             </ul><br />
 
             <div id="markup-container">
-               <textarea id="markup"
-                  placeholder="Enter your JSON-LD markup here..."
-                  onkeyup="playground.process()"></textarea>
-               <textarea id="frame" class="compressed"
-                  placeholder="Enter your JSON-LD frame here..."
-                  onkeyup="playground.process()"></textarea>
+               <textarea id="markup" class="process"
+                  placeholder="Enter your JSON-LD markup here..."></textarea>
+               <textarea id="frame" class="compressed process"
+                  placeholder="Enter your JSON-LD frame here..."></textarea>
             </div>
 
             <div id="permalink"></div>
@@ -81,6 +67,8 @@
             <div id="markup-errors" class="errors"></div>
 
             <div id="frame-errors" class="errors"></div>
+            
+            <div id="resolve-errors" class="errors"></div>
 
             <div id="tabs">
                 <ul>
--- a/playground/jsonld.js	Mon Oct 24 15:16:52 2011 +0800
+++ b/playground/jsonld.js	Thu Oct 27 15:02:49 2011 -0400
@@ -378,6 +378,12 @@
          tmp = [input];
       }
       
+      // merge context if it is an array
+      if(ctx.constructor === Array)
+      {
+         ctx = jsonld.mergeContexts({}, ctx);
+      }
+      
       for(var i in tmp)
       {
          // setup output context
@@ -416,50 +422,67 @@
  */
 jsonld.mergeContexts = function(ctx1, ctx2)
 {
+   // merge first context if it is an array
+   if(ctx1.constructor === Array)
+   {
+      ctx1 = jsonld.mergeContexts({}, ctx1);
+   }
+   
    // copy context to merged output
    var merged = _clone(ctx1);
-
-   // if the new context contains any IRIs that are in the merged context,
-   // remove them from the merged context, they will be overwritten
-   for(var key in ctx2)
+   
+   if(ctx2.constructor === Array)
    {
-      // ignore special keys starting with '@'
-      if(key.indexOf('@') !== 0)
+      // merge array of contexts in order
+      for(var i in ctx2)
       {
-         for(var mkey in merged)
+         merged = jsonld.mergeContexts(merged, ctx2[i]);
+      }
+   }
+   else
+   {
+      // if the new context contains any IRIs that are in the merged context,
+      // remove them from the merged context, they will be overwritten
+      for(var key in ctx2)
+      {
+         // ignore special keys starting with '@'
+         if(key.indexOf('@') !== 0)
          {
-            if(merged[mkey] === ctx2[key])
+            for(var mkey in merged)
             {
-               // FIXME: update related @coerce rules
-               delete merged[mkey];
-               break;
+               if(merged[mkey] === ctx2[key])
+               {
+                  // FIXME: update related @coerce rules
+                  delete merged[mkey];
+                  break;
+               }
             }
          }
       }
-   }
-
-   // merge contexts
-   for(var key in ctx2)
-   {
-      // skip @coerce, to be merged below
-      if(key !== '@coerce')
-      {
-         merged[key] = _clone(ctx2[key]);
-      }
-   }
    
-   // merge @coerce
-   if('@coerce' in ctx2)
-   {
-      if(!('@coerce' in merged))
+      // merge contexts
+      for(var key in ctx2)
       {
-         merged['@coerce'] = _clone(ctx2['@coerce']);
+         // skip @coerce, to be merged below
+         if(key !== '@coerce')
+         {
+            merged[key] = _clone(ctx2[key]);
+         }
       }
-      else
+      
+      // merge @coerce
+      if('@coerce' in ctx2)
       {
-         for(var key in ctx2['@coerce'])
+         if(!('@coerce' in merged))
          {
-            merged['@coerce'][key] = ctx2['@coerce'][key];
+            merged['@coerce'] = _clone(ctx2['@coerce']);
+         }
+         else
+         {
+            for(var key in ctx2['@coerce'])
+            {
+               merged['@coerce'][key] = ctx2['@coerce'][key];
+            }
          }
       }
    }
@@ -569,7 +592,7 @@
                break;
             }
          }
-      }
+      }m
       if(quit)
       {
          break;
@@ -579,6 +602,130 @@
    return rval;
 };
 
+/**
+ * Resolves external @context URLs. Every @context URL in the given JSON-LD
+ * object is resolved using the given URL-resolver function. Once all of
+ * the @contexts have been resolved, the given result callback is invoked.
+ * 
+ * @param input the JSON-LD input object (or array).
+ * @param resolver the resolver method that takes a URL and a callback that
+ *           receives a JSON-LD serialized @context or null on error (with
+ *           optional an error object as the second parameter).
+ * @param callback the callback to be invoked with the fully-resolved
+ *           JSON-LD output (object or array) or null on error (with an
+ *           optional error array as the second parameter).
+ */
+jsonld.resolve = function(input, resolver, callback)
+{
+   // find all @context URLs
+   var urls = {};
+   var findUrls = function(input, replace)
+   {
+      if(input.constructor === Array)
+      {
+         for(var i in input)
+         {
+            findUrls(input[i]);
+         }
+      }
+      else if(input.constructor === Object)
+      {
+         for(var key in input)
+         {
+            if(key === '@context')
+            {
+               // @context is an array that might contain URLs
+               if(input[key].constructor === Array)
+               {
+                  var list = input[key];
+                  for(var i in list)
+                  {
+                     if(list[i].constructor === String)
+                     {
+                        // replace w/resolved @context if appropriate
+                        if(replace)
+                        {
+                           list[i] = urls[list[i]];
+                        }
+                        // unresolved @context found
+                        else
+                        {
+                           urls[list[i]] = {};
+                        }
+                     }
+                  }
+               }
+               else if(input[key].constructor === String)
+               {
+                  // replace w/resolved @context if appropriate
+                  if(replace)
+                  {
+                     input[key] = urls[input[key]];
+                  }
+                  // unresolved @context found
+                  else
+                  {
+                     urls[input[key]] = {};
+                  }
+               }
+            }
+         }
+      }
+   };
+   findUrls(input, false);
+   
+   // state for resolving URLs
+   var count = Object.keys(urls).length;
+   var errors = null;
+   
+   if(count === 0)
+   {
+      callback(input, errors);
+   }
+   else
+   {
+      // resolve all URLs
+      for(var url in urls)
+      {
+         resolver(url, function(result, error)
+         {
+            --count;
+            
+            if(result === null)
+            {
+               errors = errors || [];
+               errors.push({ url: url, error: error });
+            }
+            else if(result.constructor === String)
+            {
+               try
+               {
+                  urls[url] = JSON.parse(result);
+               }
+               catch(ex)
+               {
+                  errors = errors || [];
+                  errors.push({ url: url, error: ex });
+               }
+            }
+            else
+            {
+               urls[url] = result;
+            }
+            
+            if(count === 0)
+            {
+               if(errors === null)
+               {
+                  findUrls(input, true);
+               }
+               callback(input, errors);
+            }
+         });
+      }
+   }
+};
+
 // TODO: organizational rewrite
 
 /**
--- a/playground/playground.js	Mon Oct 24 15:16:52 2011 +0800
+++ b/playground/playground.js	Thu Oct 27 15:02:49 2011 -0400
@@ -3,6 +3,7 @@
  * for Linked Data.
  *
  * @author Manu Sporny <[email protected]>
+ * @author Dave Longley
  */
 (function($)
 {
@@ -11,7 +12,7 @@
    var playground = window.playground;
    
    // set the active tab to the compacted view
-   playground.activeTab = "tab-compacted";
+   playground.activeTab = 'tab-compacted';
    
    // the counter is used to throttle colorization requests in milliseconds
    playground.colorizeDelay = 500;
@@ -29,15 +30,14 @@
    {
       // replace each special HTML character in the string
       return text.replace(/([&<>])/g, function (c) {
-         return "&" + {
-             "&": "amp",
-             "<": "lt",
-             ">": "gt"
-         }[c] + ";";
+         return '&' + {
+             '&': 'amp',
+             '<': 'lt',
+             '>': 'gt'
+         }[c] + ';';
       });
    };
 
-
    /**
     * Get a query parameter by name.
     *
@@ -57,7 +57,7 @@
    /**
     * Handle URL query parameters.
     *
-    * Checks "json-ld" and "frame" parameters.  If they look like JSON then
+    * Checks 'json-ld' and 'frame' parameters.  If they look like JSON then
     * interpret as JSON strings else interpret as URLs of remote resources.
     * Note: URLs must be CORS enabled to load due to browser same origin policy
     * issues.
@@ -71,7 +71,7 @@
       };
 
       /**
-       * Read a parameter as JSON or created an jQuery AJAX Deferred call
+       * Read a parameter as JSON or create an jQuery AJAX Deferred call
        * to read the data.
        *
        * @param param a query parameter value.
@@ -85,11 +85,11 @@
          // the ajax deferred or null
          var rval = null;
 
-         // check "json-ld" parameter
+         // check 'json-ld' parameter
          if(param !== null)
          {
             hasQueryData = true;
-            if(param.length == 0 || param[0] == "{" || param[0] == "[")
+            if(param.length == 0 || param[0] == '{' || param[0] == '[')
             {
                // param looks like JSON
                queryData[fieldName] = param;
@@ -105,8 +105,8 @@
                   },
                   error: function(jqXHR, textStatus, errorThrown) {
                      // FIXME: better error handling
-                     $("#markup-errors")
-                        .text("Error loading " + msgName + " URL: " + param);
+                     $('#resolve-errors')
+                        .text('Error loading ' + msgName + ' URL: ' + param);
                   }
                });
             }
@@ -117,9 +117,9 @@
 
       // build deferreds
       var jsonLdDeferred = handleParameter(
-         getParameterByName("json-ld"), "markup", "JSON-LD");
+         getParameterByName('json-ld'), 'markup', 'JSON-LD');
       var frameDeferred = handleParameter(
-         getParameterByName("frame"), "frame", "frame");
+         getParameterByName('frame'), 'frame', 'frame');
 
       // wait for ajax if needed
       // failures handled in AJAX calls
@@ -135,9 +135,9 @@
     */
    playground.init = function()
    {
-      $("#tabs").tabs();
-      $("#frame").hide();
-      $("#tabs").bind("tabsselect", playground.tabSelected);
+      $('#tabs').tabs();
+      $('#frame').hide();
+      $('#tabs').bind('tabsselect', playground.tabSelected);
       playground.processQueryParameters();
    };
 
@@ -150,17 +150,17 @@
    playground.tabSelected = function(event, ui)
    {
       playground.activeTab = ui.tab.id;
-      if(ui.tab.id == "tab-framed")
+      if(ui.tab.id == 'tab-framed')
       {
          // if the 'frame' tab is selected, display the frame input textarea
-         $("#markup").addClass("compressed");
-         $("#frame").show();
+         $('#markup').addClass('compressed');
+         $('#frame').show();
       }
       else
       {
          // if the 'frame' tab is not selected, hide the frame input area
-         $("#frame").hide();
-         $("#markup").removeClass("compressed");
+         $('#frame').hide();
+         $('#markup').removeClass('compressed');
       }
       
       // perform processing on the data provided in the input boxes
@@ -169,6 +169,98 @@
       // apply the syntax colorization
       prettyPrint();
    };
+   
+   /**
+    * Resolves a JSON-LD @context url.
+    *
+    * @param url the url to resolve.
+    * @param callback the callback to call once the url has been resolved.
+    */
+   playground.resolveContext = function(url, callback)
+   {
+      var regex = /(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
+      if(!regex.test(url))
+      {
+         callback(null, 'Invalid URL');
+      }
+      else
+      {
+         // treat param as a URL
+         $.ajax({
+            url: url,
+            dataType: 'application/ld+json',
+            success: function(data, textStatus, jqXHR)
+            {
+               callback(data);
+            },
+            error: function(jqXHR, textStatus, errorThrown)
+            {
+               callback(null, errorThrown);
+            }
+         });
+      }
+   };
+   
+   /**
+    * Performs the JSON-LD API action based on the active tab.
+    *
+    * @param input the JSON-LD object input or null no error.
+    * @param frame the JSON-LD frame to use.
+    */
+   playground.performAction = function(input, frame)
+   {
+      if(playground.activeTab == 'tab-normalized')
+      {
+         var normalized = jsonld.normalize(input);
+         $('#normalized').html(js_beautify(JSON.stringify(normalized)),
+            { 'indent_size': 3, 'brace_style': 'expand' });
+      }
+      else if(playground.activeTab == 'tab-expanded')
+      {
+         var expanded = jsonld.expand(input);
+         $('#expanded').html(js_beautify(JSON.stringify(expanded)),
+            { 'indent_size': 3, 'brace_style': 'expand' });
+      }
+      else if(playground.activeTab == 'tab-compacted')
+      {
+         var compacted = jsonld.compact(
+            input['@context'] || {}, input);
+         $('#compacted').html(js_beautify(JSON.stringify(compacted)),
+            { 'indent_size': 3, 'brace_style': 'expand' });
+      }
+      else if(playground.activeTab == 'tab-framed')
+      {
+         var framed = jsonld.frame(input, frame);
+         $('#framed').html(js_beautify(JSON.stringify(framed)),
+            { 'indent_size': 3, 'brace_style': 'expand' });
+      }
+      else if(playground.activeTab == 'tab-turtle')
+      {
+         var turtle = jsonld.turtle(input);
+         $('#turtle').html(playground.htmlEscape(turtle));
+      }
+
+      // generate a link for current data
+      var link = '?json-ld=' + encodeURIComponent(JSON.stringify(input));
+      if($('#frame').val().length > 0)
+      {
+         link += '&frame=' + encodeURIComponent(JSON.stringify(frame));
+      }
+      var permalink = '<a href="' + link + '">permalink</a>';
+      // size warning for huge links
+      if((window.location.protocol.length + 2 +
+         window.location.host.length + window.location.pathname.length +
+         link.length) > 2048)
+      {
+         permalink += ' (2KB+)';
+      }
+      $('#permalink')
+         .html(permalink)
+         .show();
+      
+      // start the colorization delay
+      playground.checkColorizeDelay(true);
+   };
 
    /**
     * Process the JSON-LD markup that has been input and display the output
@@ -176,94 +268,64 @@
     */
    playground.process = function()
    {
-      var input = null;
-      var frame = null;
+      $('#markup-errors').text('');
+      $('#frame-errors').text('');
+      $('#resolve-errors').text('');
       var errors = false;
 
       // check to see if the JSON-LD markup is valid JSON
       try
       {
-         $("#markup-errors").text("");
-         input = JSON.parse($("#markup").val());
+         var input = JSON.parse($('#markup').val());
       }
       catch(e)
       {
-         $("#markup-errors").text("JSON markup - " + e);
+         $('#markup-errors').text('JSON markup - ' + e);
          errors = true;
       }
 
       // check to see if the JSON-LD frame is valid JSON
       try
       {
-         $("#frame-errors").text("");
-         frame = JSON.parse($("#frame").val());
+         var frame = JSON.parse($('#frame').val());
       }
       catch(e)
       {
-         $("#frame-errors").text("JSON-LD frame - " + e);
+         $('#frame-errors').text('JSON-LD frame - ' + e);
          errors = true;
       }
 
-      // if there are no errors, perform the action and display the output
-      if(!errors)
+      // errors detected
+      if(errors)
       {
-         if(playground.activeTab == "tab-normalized")
-         {
-            var normalized = jsonld.normalize(input);
-            $("#normalized").html(js_beautify(JSON.stringify(normalized)),
-               { "indent_size": 3, "brace_style": "expand" });
-         }
-         else if(playground.activeTab == "tab-expanded")
-         {
-            var expanded = jsonld.expand(input);
-            $("#expanded").html(js_beautify(JSON.stringify(expanded)),
-               { "indent_size": 3, "brace_style": "expand" });
-         }
-         else if(playground.activeTab == "tab-compacted")
-         {
-            var compacted = jsonld.compact(
-               input["@context"] || {}, input);
-            $("#compacted").html(js_beautify(JSON.stringify(compacted)),
-               { "indent_size": 3, "brace_style": "expand" });
-         }
-         else if(playground.activeTab == "tab-framed")
-         {
-            var framed = jsonld.frame(input, frame);
-            $("#framed").html(js_beautify(JSON.stringify(framed)),
-               { "indent_size": 3, "brace_style": "expand" });
-         }
-         else if(playground.activeTab == "tab-turtle")
-         {
-            var turtle = jsonld.turtle(input);
-            $("#turtle").html(playground.htmlEscape(turtle));
-         }
-
-         // generate a link for current data
-         var link = "?json-ld=" + encodeURIComponent(JSON.stringify(input));
-         if($("#frame").val().length > 0)
-         {
-            link += "&frame=" + encodeURIComponent(JSON.stringify(frame));
-         }
-         var permalink = '<a href="' + link + '">permalink</a>';
-         // size warning for huge links
-         if((window.location.protocol.length + 2 +
-            window.location.host.length + window.location.pathname.length +
-            link.length) > 2048)
-         {
-            permalink += " (2KB+)"
-         }
-         $("#permalink")
-            .html(permalink)
-            .show();
+         $('#permalink').hide();
+         
+         // start the colorization delay
+         playground.checkColorizeDelay(true);
       }
+      // no errors, perform the action and display the output
       else
       {
-         $("#permalink").hide();
+         // resolve external @context URLs and perform action
+         jsonld.resolve(
+            input,
+            playground.resolveContext,
+            function(input, errors)
+            {
+               if(errors)
+               {
+                  // FIXME: better error handling
+                  $('#resolve-errors').text(
+                     'Could not load @context URL: "' +
+                     errors[0].url + '", ' + errors[0].error);
+               }
+               else
+               {
+                  playground.performAction(input, frame);
+               }
+            });
       }
-      
-      // Start the colorization delay
-      playground.checkColorizeDelay(true);
-   }
+   };
 
    /**
     * Performs a check on the colorize delay. If the delay hits 0, the
@@ -315,22 +377,22 @@
       {
          hasData = true;
          // fill the markup box with the example
-         $("#markup").val(js_beautify(
+         $('#markup').val(js_beautify(
             data.markup,
-            { "indent_size": 3, "brace_style": "expand" }));
+            { 'indent_size': 3, 'brace_style': 'expand' }));
       }
 
       if('frame' in data && data.frame !== null)
       {
          hasData = true;
          // fill the frame input box with the example frame
-         $("#frame").val(js_beautify(
+         $('#frame').val(js_beautify(
             data.frame,
-            { "indent_size": 3, "brace_style": "expand" }));
+            { 'indent_size': 3, 'brace_style': 'expand' }));
       }
       else
       {
-         $("#frame").val("{}");
+         $('#frame').val('{}');
       }
 
       if(hasData)
@@ -370,6 +432,28 @@
       // populate with the example
       playground.populateWithJSON(data);
    };
-
+   
+   // event handlers
+   $(document).ready(function()
+   {
+      // set up buttons to load examples
+      $('.button').each(function(idx)
+      {
+         var button = $(this);
+         button.click(function()
+         {
+            playground.populateWithExample(button.find('span').text());
+         });
+      });
+      
+      // set up 'process' areas to process JSON-LD after typing
+      var processTimer = null;
+      $('.process').keyup(function()
+      {
+         clearTimeout(processTimer);
+         processTimer = setTimeout(playground.process, 500);
+      });
+   });
+   
 })(jQuery);