Support for remote documents, icons and more playground improvements (PR
authorNicholas Bollweg (Nick) <nick.bollweg@gmail.com>
Thu, 27 Mar 2014 08:27:20 +0100
changeset 2143 e73995297ea4
parent 2142 70748090503c
child 2144 a3beaad615e8
child 2145 f126733e13cf
Support for remote documents, icons and more playground improvements (PR

This closes #340 and addresses #325 (I leave this open for the moment till
the RDF icon is either included directly in FontAwesome or we make it b/w
as all the other icons).

Squashed commit of the following:

commit d867cf1fb3b6a310bef70ca9647f1cfaccd04b05
Author: Nicholas Bollweg (Nick) <[email protected]>
Date: Wed Mar 26 22:43:09 2014 -0400

rdf icon

commit d9f2db50f9cdef4657da3a08dccf7629152931c9
Author: Nicholas Bollweg (Nick) <[email protected]>
Date: Wed Mar 26 22:16:14 2014 -0400

typo

commit 241ed5ac3fec52331bbcc4379e114241f6e23e81
Author: Nicholas Bollweg (Nick) <[email protected]>
Date: Wed Mar 26 22:11:46 2014 -0400

styling of context copy

commit 1d0d691f0620917c633a5c6409b555b0932bb817
Author: Nicholas Bollweg (Nick) <[email protected]>
Date: Wed Mar 26 22:01:49 2014 -0400

permalink cleanup

commit d48e0966794d062b8c48e8d1872f8df03ab8b281
Author: Nicholas Bollweg (Nick) <[email protected]>
Date: Wed Mar 26 20:50:45 2014 -0400

cleaning up remote URLs

commit ee28a1e785bff51a0900f50008ff7ac6e8cb7da2
Author: Nicholas Bollweg (Nick) <[email protected]>
Date: Mon Mar 24 23:26:09 2014 -0400

permalink feedback

commit 054fa28197b0cc6082f5bbab6c4e2aee5afe8d4d
Author: Nicholas Bollweg (Nick) <[email protected]>
Date: Mon Mar 24 22:46:46 2014 -0400

more cleanup, UI quirks

commit d65f5dd7eb315acaf9e073b96521b6a52e4b097f
Author: Nicholas Bollweg (Nick) <[email protected]>
Date: Mon Mar 24 21:13:22 2014 -0400

using remoteURL for base, if available

commit 4d2b8539c7b085056678f60c3e056b216272e720
Author: Nicholas Bollweg (Nick) <[email protected]>
Date: Mon Mar 24 20:25:35 2014 -0400

hash permalinks, more icons, doc, return values

commit f804c6144304ab8f662f7c2f7390686b411ae336
Author: Nicholas Bollweg (Nick) <[email protected]>
Date: Sun Mar 23 18:43:22 2014 -0400

adding context copy
playground/index.html
playground/playground.css
playground/playground.js
--- a/playground/index.html	Thu Mar 20 20:44:59 2014 +0100
+++ b/playground/index.html	Thu Mar 27 08:27:20 2014 +0100
@@ -20,15 +20,15 @@
     <link rel="stylesheet" type="text/css" href="../static/css/bootstrap/bootstrap.css">
 
     <!-- CodeMirror -->
-    <link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/g/[email protected](css/bootstrap-responsive.min.css),[email protected](codemirror.css+addon/lint/lint.css+addon/hint/show-hint.css+theme/neat.css)">    
-    <link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/fontawesome/3.0.2/css/font-awesome.min.css">
+    <link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/g/[email protected](css/bootstrap-responsive.min.css),[email protected](codemirror.css+addon/lint/lint.css+addon/hint/show-hint.css+theme/neat.css)">
+    <link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/fontawesome/3.2.1/css/font-awesome.min.css">
     <link rel="stylesheet" type="text/css" href="./playground.css">
 
 
     <link rel="shortcut icon" href="../favicon.ico" />
   </head>
 
-  <body onload="playground.init();">
+  <body id="page-playground" onload="playground.init();">
     <div class="navbar navbar-static-top">
       <div class="navbar-inner">
         <div class="row-fluid">
@@ -84,134 +84,236 @@
         is a work in progress.
       </p>
       <br/>
+    </div>
 
-      <div class="btn-group" data-toggle="buttons-radio">
-        <button class="btn disabled btn-primary">Examples:</button>
-        <button id="btn-person" class="btn button"><span>Person</span></button>
-        <button id="btn-event" class="btn button"><span>Event</span></button>
-        <button id="btn-place" class="btn button"><span>Place</span></button>
-        <button id="btn-product" class="btn button"><span>Product</span></button>
-        <button id="btn-recipe" class="btn button"><span>Recipe</span></button>
-        <button id="btn-library" class="btn button"><span>Library</span></button>
-      </div>
+    <div class="loading hero">
+      <h1><i class="icon-spinner icon-spin icon-large"></i> Loading the Playground...
+    </div>
+    <div class="loaded hide">
+      <div class="container">
+        <div class="btn-group" data-toggle="buttons-radio">
+          <button class="btn disabled btn-primary">Examples:</button>
+          <button id="btn-person" class="btn button">
+            <i class="icon icon-user"></i>
+            <span>Person</span>
+          </button>
+          <button id="btn-event" class="btn button">
+            <i class="icon icon-calendar"></i>
+            <span>Event</span>
+          </button>
+          <button id="btn-place" class="btn button">
+            <i class="icon icon-map-marker"></i>
+            <span>Place</span>
+          </button>
+          <button id="btn-product" class="btn button">
+            <i class="icon icon-barcode"></i>
+            <span>Product</span>
+          </button>
+          <button id="btn-recipe" class="btn button">
+            <i class="icon icon-food"></i>
+            <span>Recipe</span>
+          </button>
+          <button id="btn-library" class="btn button">
+            <i class="icon icon-book"></i>
+            <span>Library</span>
+          </button>
+        </div>
 
-      <div class="pull-right">
-        <button class="btn popover-info">
-          <i class="icon-magic"></i> Shortcuts
-        </button>
-        <div class="popover-info-content hide">
-          <table class="table table-striped">
+        <div class="pull-right">
+          <a class="btn" id="permalink">
+            <i class="icon icon-link"></i>
+            <span>Permalink</span>
+          </a>
+          <button class="btn popover-info" title="Keyboard shortcuts">
+            <i class="icon icon-keyboard"></i> Shortcuts
+          </button>
+          <div class="popover-info-content hide">
+            <table class="table table-striped">
+              <thead>
+                <tr>
+                  <th>Key</th>
+                  <th>Autocomplete</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr>
+                  <td><label class="label">@</label></td>
+                  <td>all of the <b>@</b> keywords</td>
+                </tr>
+                <tr>
+                  <td><label class="label">Ctrl+Space</label></td>
+                  <td>available keys in <b>@context</b></td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        </div>
+
+        <br/><br/>
+        <div class="container" id="markup-container">
+          <div class="row">
+            <div id="markup-div" class="span12">
+              <div class="pull-right editor-options">
+                <div class="input-prepend editor-option" title="Use remote document" data-editor="markup">
+                  <button class="btn" type="button" data-toggle="button">
+                    <span class="icon-stack">
+                      <i class="icon-cloud icon-stack-base icon-muted"></i>
+                      <i class="icon-file"></i>
+                    </span>
+                  </button>
+                  <input class="span2" type="text" placeholder="Document URL"/>
+                </div>
+              </div>
+              <h3>JSON-LD Input</h3>
+              <textarea id="markup" class="compressed process span6 codemirror-input"
+              placeholder="Enter your JSON-LD markup here..." rows="10"></textarea>
+            </div>
+
+            <div id="context-div" class="span6">
+              <div class="pull-right editor-options">
+                <div class="input-prepend editor-option" title="Use remote context" data-editor="context">
+                  <button class="btn" type="button" data-toggle="button">
+                    <span class="icon-stack">
+                      <i class="icon-cloud icon-stack-base icon-muted"></i>
+                      <i class="icon-bullseye"></i>
+                    </span>
+                  </button>
+                  <input class="span2" type="text" placeholder="Context URL"/>
+                </div>
+              </div>
+              <h3>New JSON-LD Context</h3>
+              <button class="btn" data-toggle="button" id="copy-context" title='Copy context from "JSON-LD Input"'>
+                <i class="icon-circle-arrow-right"></i>
+              </button>
+              
+              <textarea id="context" class="compressed process span6 codemirror-input"
+              placeholder="Enter the new JSON-LD context to compact to here..." rows="10">{}</textarea>
+            </div>
+
+            <div id="frame-div" class="span6">
+              <h3>
+                JSON-LD Frame
+                <div class="pull-right editor-options">
+                  <div class="input-prepend editor-option" title="Use remote frame" data-editor="frame">
+                    <button class="btn" type="button" data-toggle="button">
+                      <span class="icon-stack">
+                        <i class="icon-cloud icon-stack-base icon-muted"></i>
+                        <i class="icon-crop"></i>
+                      </span>
+                    </button>
+                    <input class="span2" type="text" placeholder="Frame URL" />
+                  </div>
+                </div>
+              </h3>
+              <textarea id="frame" class="compressed process span6 codemirror-input"
+              placeholder="Enter your JSON-LD frame here..." rows="10">{}</textarea>
+            </div>
+          </div>
+        </div>
+        <div id="markup-errors" class="text-error"></div>
+        <div id="param-errors" class="text-error"></div>
+        <div id="using-context-map" class="hide alert alert-note">
+          <p>NOTE: Schema.org's remote context was detected in your input but, unfortunately,
+            it doesn't resolve to a proper context document yet. The Schema.org team is
+            already working on this issue and it is expected to be resolved in a couple
+            of weeks.</p>
+          <p>In the meantime, if you wish, you can use an alternative context created by
+            the JSON-LD community to process your document.</p>
+          <label class="checkbox">
+            <input type="checkbox" id="use-context-map" value="1"> Use alternative context
+          </label>
+          <table class="table table-condensed">
             <thead>
               <tr>
-                <th>Key</th>
-                <th>Autocomplete</th>
+                <td>Original</td>
+                <td>Alternative</td>
               </tr>
             </thead>
             <tbody>
-              <tr>
-                <td><label class="label">@</label></td>
-                <td>all of the <b>@</b> keywords</td>
-              </tr>
-              <tr>
-                <td><label class="label">Ctrl+Space</label></td>
-                <td>available keys in <b>@context</b></td>
-              </tr>
+              <!-- dynamic rows -->
             </tbody>
           </table>
         </div>
-      </div>
+        <p id="processing-errors" class="text-error"></p>
 
-      <br/><br/>
-      <div class="container" id="markup-container">
-        <div class="row">
-          <div id="markup-div" class="span12">
-            <h3>JSON-LD Input</h3>
-            <textarea id="markup" class="compressed process span6 codemirror-input"
-            placeholder="Enter your JSON-LD markup here..." rows="10"></textarea>
-          </div>
-          <div id="context-div" class="span6">
-            <h3>New JSON-LD Context</h3>
-            <textarea id="context" class="compressed process span6 codemirror-input"
-            placeholder="Enter the new JSON-LD context to compact to here..." rows="10">{}</textarea>
-          </div>
-          <div id="frame-div" class="span6">
-            <h3>JSON-LD Frame</h3>
-            <textarea id="frame" class="compressed process span6 codemirror-input"
-            placeholder="Enter your JSON-LD frame here..." rows="10">{}</textarea>
-          </div>
+        <div id="output-container">
+          <ul id="tabs" class="nav nav-tabs">
+            <li class="active">
+              <a id="tab-expanded" href="#pane1" data-toggle="tab" name="tab-expanded">
+                <i class="icon-resize-full"></i>
+                <span>Expanded</span>
+              </a>
+            </li>
+            <li>
+              <a id="tab-compacted" href="#pane2" data-toggle="tab" name="tab-compacted">
+                <i class="icon-resize-small"></i>
+                <span>Compacted</span>
+              </a>
+            </li>
+            <li>
+              <a id="tab-flattened" href="#pane3" data-toggle="tab" name="tab-flattened">
+                <i class="icon-reorder"></i>
+                <span>Flattened</span>
+              </a>
+            </li>
+            <li>
+              <a id="tab-framed" href="#pane4" data-toggle="tab" name="tab-framed">
+                <i class="icon-crop"></i>
+                <span>Framed</span>
+              </a>
+            </li>
+            <li>
+              <a id="tab-nquads" href="#pane5" data-toggle="tab" name="tab-nquads">
+                <img src="../images/rdf_flyer.svg" width="13"/>
+                <span>N-Quads</span>
+              </a>
+            </li>
+            <li>
+              <a id="tab-normalized" href="#pane6" data-toggle="tab" name="tab-normalized">
+                <i class="icon-archive"></i>
+                <span>Normalized</span>
+              </a>
+            </li>
+          </ul>
+
+          <div class="tab-content">
+            <div id="pane1" class="tab-pane active">
+              <textarea id="expanded" class="codemirror-output"></textarea>
+            </div>
+            <div id="pane2" class="tab-pane">
+              <textarea id="compacted" class="codemirror-output"></textarea>
+            </div>
+            <div id="pane3" class="tab-pane">
+              <textarea id="flattened" class="codemirror-output"></textarea>
+            </div>
+            <div id="pane4" class="tab-pane">
+              <textarea id="framed" class="codemirror-output"></textarea>
+            </div>
+            <div id="pane5" class="tab-pane">
+              <textarea id="nquads" class="codemirror-output"></textarea>
+            </div>
+            <div id="pane6" class="tab-pane">
+              <textarea id="normalized" class="codemirror-output"></textarea>
+            </div>
+          </div><!-- /.tab-content -->
         </div>
-      </div>
-      <div id="permalink"></div>
-      <div id="markup-errors" class="text-error"></div>
-      <div id="param-errors" class="text-error"></div>
-      <div id="using-context-map" class="hide alert alert-note">
-        <p>NOTE: Schema.org's remote context was detected in your input but, unfortunately,
-          it doesn't resolve to a proper context document yet. The Schema.org team is
-          already working on this issue and it is expected to be resolved in a couple
-          of weeks.</p>
-        <p>In the meantime, if you wish, you can use an alternative context created by
-          the JSON-LD community to process your document.</p>
-        <label class="checkbox">
-          <input type="checkbox" id="use-context-map" value="1"> Use alternative context
-        </label>
-        <table class="table table-condensed">
-          <thead>
-            <tr>
-              <td>Original</td>
-              <td>Alternative</td>
-            </tr>
-          </thead>
-          <tbody>
-            <!-- dynamic rows -->
-          </tbody>
-        </table>
-      </div>
-      <p id="processing-errors" class="text-error"></p>
 
-      <div id="output-container">
-        <ul id="tabs" class="nav nav-tabs">
-          <li class="active"><a id="tab-expanded" href="#pane1" data-toggle="tab"><span>Expanded</span></a></li>
-          <li><a id="tab-compacted" href="#pane2" data-toggle="tab"><span>Compacted</span></a></li>
-          <li><a id="tab-flattened" href="#pane3" data-toggle="tab"><span>Flattened</span></a></li>
-          <li><a id="tab-framed" href="#pane4" data-toggle="tab"><span>Framed</span></a></li>
-          <li><a id="tab-nquads" href="#pane5" data-toggle="tab"><span>N-Quads</span></a></li>
-          <li><a id="tab-normalized" href="#pane6" data-toggle="tab"><span>Normalized</span></a></li>
-        </ul>
-        <div class="tab-content">
-          <div id="pane1" class="tab-pane active">
-            <textarea id="expanded" class="codemirror-output"></textarea>
-          </div>
-          <div id="pane2" class="tab-pane">
-            <textarea id="compacted" class="codemirror-output"></textarea>
-          </div>
-          <div id="pane3" class="tab-pane">
-            <textarea id="flattened" class="codemirror-output"></textarea>
-          </div>
-          <div id="pane4" class="tab-pane">
-            <textarea id="framed" class="codemirror-output"></textarea>
-          </div>
-          <div id="pane5" class="tab-pane">
-            <textarea id="nquads" class="codemirror-output"></textarea>
-          </div>
-          <div id="pane6" class="tab-pane">
-            <textarea id="normalized" class="codemirror-output"></textarea>
-          </div>
-        </div><!-- /.tab-content -->
-      </div>
+        <hr/>
+      </div> <!-- /.container -->
+    </div> <!-- /.loading -->
 
-      <hr>
+    <div class="container">
       <div id="footer">
         <p id="copyright">
          Website content released under a <a href="http://creativecommons.org/about/cc0">Creative Commons CC0 Public Domain Dedication</a> except where an alternate is specified.
          Part of the <a href="http://payswarm.com/">PaySwarm</a> standardization initiative.
         </p>
       </div>
-    </div> <!-- /container -->
-
+    </div>
 
     <!-- sccdn scripts -->
     <script src="//cdn.jsdelivr.net/g/[email protected],[email protected],[email protected],[email protected](codemirror.min.js+addon/lint/lint.js+addon/edit/matchbrackets.js+addon/edit/closebrackets.js+addon/display/placeholder.js+addon/hint/show-hint.js+mode/ntriples/ntriples.js+mode/javascript/javascript.js)"></script>
-    
+
     <!-- local scripts -->
     <script src="./jsonld.js"></script>
     <script src="./jsonlint.js"></script>
--- a/playground/playground.css	Thu Mar 20 20:44:59 2014 +0100
+++ b/playground/playground.css	Thu Mar 27 08:27:20 2014 +0100
@@ -1,5 +1,13 @@
+.jumbotron { text-align: center; }
+
 #frame-div{ display: none; }
 #tabs{ margin-bottom: 0; }
+.nav-tabs > li.active > a,
+.nav-tabs > li.active > a:hover{
+  background-color: #fff;
+}
+
+
 .btn.resizer{
   cursor: row-resize;
   padding: .5px;
@@ -13,8 +21,6 @@
   padding: 5px;
 }
 
-
-
 /*
   auto-resize bootstrap popovers from
   http://stackoverflow.com/questions/15776487/bootstrap-popover-width-for-popover-inner
@@ -41,4 +47,25 @@
   -webkit-background-clip: padding-box;
      -moz-background-clip: padding;
           background-clip: padding-box;
-}
\ No newline at end of file
+}
+
+.editor-options > * {
+  margin-top: 10px;
+}
+
+#page-playground .editor-option {
+  margin-bottom: 0;
+}
+
+#page-playground .editor-option .btn {
+  padding: 1px 5px 0px 5px;
+}
+
+#page-playground .read-only {
+  opacity: .6;
+}
+
+#copy-context {
+  padding: 0px 4px;
+  float: left;
+}
--- a/playground/playground.js	Thu Mar 20 20:44:59 2014 +0100
+++ b/playground/playground.js	Thu Mar 27 08:27:20 2014 +0100
@@ -7,13 +7,28 @@
  * @author Nicholas Bollweg
  * @author Markus Lanthaler
  */
-(function($, CodeMirror) {
-  // create the playground instance if it doesn't already exist
-  window.playground = window.playground || {};
-  var playground = window.playground;
+;(function($, CodeMirror, jsonld, Promise){
+  "use strict";
+  // assume nothing
+  var window = this,
+    console = window.console,
+    setTimeout = window.setTimeout,
+    document = window.document,
+
+    // create the playground instance if it doesn't already exist
+    playground = window.playground = {},
+
+    // given this is needed, we probably need a `Document` class...
+    docs = function(){
+      return {
+        markup: null,
+        frame: null,
+        context: null
+      };
+    };
 
   // the codemirror editors
-  playground.editors = {};
+  playground.editors = docs();
 
   // ... and outputs
   playground.outputs = {};
@@ -22,17 +37,7 @@
   playground.theme = "neat";
 
   // the last parsed version of same
-  playground.lastParsed = {
-    markup: null,
-    frame: null,
-    context: null
-  };
-  
-  playground.lineIndex = {
-    markup: null,
-    frame: null,
-    context: null
-  };
+  playground.lastParsed = docs();
 
   // set the active tab to the expanded view
   playground.activeTab = 'tab-expanded';
@@ -50,6 +55,16 @@
   // JSON schema for JSON-LD documents
   playground.schema = null;
 
+  // copy context from the input
+  playground.copyContext = false;
+
+  // currently-active urls
+  playground.remoteUrl = docs();
+
+  // whether a remote document should be used
+  playground.useRemote = docs();
+
+
   /**
    * Get a query parameter by name.
    *
@@ -61,10 +76,11 @@
    * @return the value of the parameter or null if it does not exist
    */
   function getParameterByName(name) {
-    var match = RegExp('[?&]' + name + '=([^&]*)')
-      .exec(window.location.search);
+    var match = new RegExp('[#?&]' + name + '=([^&]*)')
+      .exec(window.location.hash || window.location.search);
     return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
-  };
+  }
+
 
   /**
    * Consistent human-readable JSON formatting.
@@ -74,16 +90,17 @@
    * @return a string containing the humanized string.
    */
   playground.humanize = function(value) {
-    return ($.type(value) === 'string')
-      ? value
-      : JSON.stringify(value, null, 2);
+    return ($.type(value) === 'string') ?
+      value :
+      JSON.stringify(value, null, 2);
   };
 
+
   /**
    * Handle URL query parameters.
    *
-   * Checks 'json-ld', 'context', and 'frame' parameters.  If they look like
-   * JSON then interpret as JSON strings else interpret as URLs of remote
+   * Checks 'json-ld', 'context', 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.
    *
@@ -91,11 +108,7 @@
    */
   playground.processQueryParameters = function() {
     // data from the query
-    var queryData = {
-      markup: null,
-      frame: null,
-      context: null
-    };
+    var queryData = docs();
 
     /**
      * Read a parameter as JSON or create an jQuery AJAX Deferred call
@@ -107,7 +120,7 @@
      *
      * @return jQuery Deferred or null.
      */
-    function handleParameter(param, fieldName, msgName) {
+    function handleParameter(param, fieldName) {
       // the ajax deferred or null
       var rval = null;
 
@@ -117,31 +130,25 @@
           // param looks like JSON, try to parse it
           try {
             queryData[fieldName] = JSON.parse(param);
+            playground.setRemoteUrl(fieldName, null);
           }
           catch(e) {
             queryData[fieldName] = param;
           }
         }
         else {
-          // treat param as a URL
-          rval = $.ajax({
-            url: param,
-            dataType: 'text',
-            crossDomain: true,
-            success: function(data, textStatus, jqXHR) {
-               queryData[fieldName] = data;
-            },
-            error: function(jqXHR, textStatus, errorThrown) {
-               // FIXME: better error handling
-               $('#processing-errors')
-                  .text('Error loading ' + msgName + ' URL: ' + param);
-            }
-          });
+          playground.toggleRemote(fieldName, true);
+          rval = playground.setRemoteUrl(fieldName, param);
+          if(rval){
+            rval.then(function(data){
+              queryData[fieldName] = data;
+            });
+          }
         }
-      };
+      }
 
       return rval;
-    };
+    }
 
     // build deferreds
     var jsonLdDeferred = handleParameter(
@@ -161,20 +168,22 @@
        $('#' + startTab).tab('show');
     }
 
+    playground.copyContext = getParameterByName('copyContext') === "true";
+
     // wait for ajax if needed
     // failures handled in AJAX calls
     $.when(jsonLdDeferred, frameDeferred, contextDeferred, paramDeferred)
       .done(function() {
         // Maintain backwards permalink compatability
-        if(queryData['param'] &&
-          !(queryData['frame'] || queryData['context'])) {
-          queryData['frame'] = queryData['context'] = queryData['param'];
+        if(queryData.param && !(queryData.frame || queryData.context)) {
+          queryData.frame = queryData.context = queryData.param;
         }
         // populate UI with data
         playground.populateWithJSON(queryData);
       });
   };
 
+
   /**
    * Used to initialize the UI, call once on document load.
    */
@@ -195,7 +204,6 @@
     CodeMirror.commands.autocomplete = function(cm) {
       CodeMirror.showHint(cm, CodeMirror.hint.jsonld, {
         lastParsed: playground.lastParsed[cm.options._playground_key],
-        lineIndex: playground.lineIndex[cm.options._playground_key],
         completeSingle: false,
         schemata: function(){
           return playground.schema ? [playground.schema] : [];
@@ -208,18 +216,36 @@
         isAt: true,
         completeSingle: false,
         lastParsed: playground.lastParsed[cm.options._playground_key],
-        lineIndex: playground.lineIndex[cm.options._playground_key],
         schemata: function(){
           return playground.schema ? [playground.schema] : [];
         }
       });
     };
 
-    $(".codemirror-input").each(playground.init.editor);
-    $(".codemirror-output").each(playground.init.output);
+    $(".codemirror-input").each(function(){ playground.init.editor(this); });
+    $(".codemirror-output").each(function(){ playground.init.output(this); });
+
     playground.makeResizer($("#markup-container"), playground.editors);
     playground.makeResizer($("#output-container"), playground.outputs);
 
+    $("#copy-context").click(function(){
+      playground.toggleCopyContext();
+    });
+
+    $(".editor-option").each(function(){
+      var option = $(this),
+        key = option.data("editor");
+      option.find("input").bind("input", function(){
+        playground.setRemoteUrl(key, this.value);
+      });
+      option.find("button").bind("click", function(){
+        playground.toggleRemote(key);
+      });
+    });
+
+    $("[title]").tooltip();
+
+    $(window).bind("hashchange", playground.processQueryParameters);
 
     // load the schema
     $.ajax({
@@ -229,18 +255,32 @@
       .done(function(schema){
         playground.schema = schema;
       })
-      .fail(function(xhr){
+      .fail(function(){
         console.warn("Schema could not be loaded. Schema validation disabled.");
       });
 
-    if(window.location.search) {
+    if(window.location.search || window.location.hash) {
       playground.processQueryParameters();
     }
+    $(".loading").fadeOut(function(){
+      $(this).remove();
+      $(".loaded").fadeIn();
+      playground.editor.refresh();
+      playground.editor.refresh(playground.outputs);
+    });
   };
 
-  playground.init.editor = function(){
-    var key = this.id,
-      editor = playground.editors[key] = CodeMirror.fromTextArea(this, {
+
+  /**
+   * Initialize a CodeMirror editor
+   *
+   * @param a `<textarea>`
+   *
+   * @return the CodeMirror editor
+   */
+  playground.init.editor = function(node){
+    var key = node.id,
+      editor = playground.editors[key] = CodeMirror.fromTextArea(node, {
         matchBrackets: true,
         autoCloseBrackets: true,
         lineWrapping: true,
@@ -261,7 +301,14 @@
       });
 
     // set up 'process' areas to process JSON-LD after typing
-    editor.on("change", playground.process);
+    editor.on("change", function(){
+      if(playground.copyContext && key === "markup"){
+        if(playground.toggleCopyContext.copy()){
+          return;
+        }
+      }
+      playground.process();
+    });
 
     // check on every keyup for `@`: doesn't get caught by (extra|custom)Keys
     editor.on("keyup", function(editor, evt) {
@@ -284,28 +331,168 @@
         CodeMirror.commands.at_autocomplete(editor, evt);
       }
     });
+
+    return editor;
   };
 
-  playground.init.output = function() {
-    var key = this.id,
-      output = playground.outputs[key] = CodeMirror.fromTextArea(this, {
+
+  /**
+   * Initialize a read-only CodeMirror viewer
+   *
+   * @param a `<textarea>`
+   *
+   * @return the CodeMirror editor
+   */
+  playground.init.output = function(node) {
+    var key = node.id,
+      output = playground.outputs[key] = CodeMirror.fromTextArea(node, {
         readOnly: true,
         lineWrapping: true,
-        mode: ["normalized", "nquads"].indexOf(key) > -1
-          ? "text/n-triples"
-          : "application/ld+json",
+        mode: ["normalized", "nquads"].indexOf(key) > -1 ?
+          "text/n-triples" :
+          "application/ld+json",
         theme: playground.theme
       });
+    return output;
   };
 
+
   /**
-   * Make one or more CodeMirror editor resizeable.
+   * Toggle whether the output context will be updated from the input
+   *
+   * @param the new value of the setting. If not provided, invert current
+   *
+   * @return the JSON that was actually set, or `undefined` if nothing was set
+   */
+  playground.toggleCopyContext = function(val){
+    var editor = playground.editors.context;
+
+    playground.copyContext =  val = arguments.length ?
+      Boolean(val) :
+      !playground.copyContext;
+
+    playground.editor.setReadOnly(editor, val);
+
+    setTimeout(function(){
+      $("#copy-context").toggleClass("toggle", val);
+    }, 1);
+
+    if(val){
+      return playground.toggleCopyContext.copy();
+    }
+  };
+
+
+  /**
+   * Copy the context right now.
+   *
+   * @return the JSON that was set, or `undefined` if nothing was set
+   */
+  playground.toggleCopyContext.copy = function(){
+    var editor = playground.editors.context,
+      json = playground.humanize({
+        "@context": playground.lastParsed.markup ?
+          playground.lastParsed.markup["@context"] :
+          {}});
+    if(json !== editor.getValue()){
+      playground.editors.context.setValue(json);
+      return json;
+    }
+  };
+
+
+  /**
+   * Set the remote URL for an editor, then fetch (if enabled).
+   *
+   * @param the key for the editor
+   * @param the value
+   *
+   * @return jQuery deferred, or `undefined`
+   */
+  playground.setRemoteUrl = function(key, val){
+    var opt = $("[data-editor=" + key + "]"),
+      btn = opt.find("button"),
+      inp = opt.find("input");
+
+    playground.remoteUrl[key] = val ? val : null;
+
+    if(inp.val() != val){
+      inp.val(val);
+    }
+
+    // the button state is no longer valid
+    btn.removeClass("btn-danger btn-info");
+
+    return playground.fetchRemote(key);
+  };
+
+
+  /**
+   * Toggle (or set) whether a remote document will be used for an editor.
+   *
+   * @param the key for the editor
+   * @param the value: omit to toggle
+   *
+   * @return jQuery deferred, or `undefined`
+   */
+  playground.toggleRemote = function(key, val){
+    var btn = $("[data-editor=" + key + "] button");
+
+    playground.useRemote[key] = val = arguments.length === 2 ?
+      Boolean(val) :
+      !playground.useRemote[key];
+
+    playground.editor.setReadOnly(key, val);
+
+    // the button state is no longer valid
+    setTimeout(function(){
+      btn.removeClass("btn-danger btn-info" + (!val ? " active" : ""));
+    }, 1);
+    return playground.fetchRemote(key);
+  };
+
+
+  /**
+   * Fetch a remote document and populate an editor.
+   *
+   * @param the key for the editor
+   *
+   * @return jQuery deferred, or `undefined`
+   */
+  playground.fetchRemote = function(key){
+    if(!playground.useRemote[key]){ return; }
+
+    var btn = $("[data-editor=" + key + "] button");
+
+    return $.ajax({
+      url: playground.remoteUrl[key],
+      dataType: 'json',
+      crossDomain: true,
+      success: function(data) {
+        btn.addClass("btn-info active");
+        // setValue always triggers a .process()
+        playground.editors[key].setValue(playground.humanize(data));
+        return data;
+      },
+      error: function() {
+        btn.addClass("btn-danger active");
+        $('#processing-errors')
+           .text('Error loading ' + key + ' URL: ' + playground.remoteUrl[key]);
+      }
+    });
+  };
+
+
+  /**
+   * Make one or more editor resizeable together.
    *
    * @param parent the dom element to which the button should be attached
-   * @param target the CodeMirror instances to be resized
+   * @param an object or list of CodeMirror instances to be resized together
+   *
+   * @return the resizer button DOM
    */
   playground.makeResizer = function(parent, targets){
-    targets = $.map(targets, function(val, key){ return val; });
+    targets = $.map(targets, function(val){ return val; });
     var start_y,
       start_height,
       handlers = {},
@@ -325,15 +512,76 @@
             });
         })
         .appendTo(parent);
+    return btn[0];
   };
 
+
+  /**
+   * Namespace for editor functions, and utility for doing things against them
+   *
+   * @param a keyed object of editors, the name of an editor, an editor
+   *        or a list of editors. or nothing, which assumes all of them.
+   * @param the action to peform, of the form `function(editor, key)`
+   *
+   * @return the result of the action
+   */
+  playground.editor = function(editors, action){
+    var key,
+      editor;
+    if($.type(editors) === "string"){
+      key = editors;
+      editors = {};
+      editors[key] = playground.editors[key];
+    }else if(editors instanceof CodeMirror){
+      key = editors.getTextArea().id;
+      editor = editors;
+      editors = {};
+      editors[key] = editor;
+    }else if(!editors){
+      editors = playground.editors;
+    }
+    return $.map(editors, action);
+  };
+
+
+  /**
+   * Make a CodeMirror editor (temporarily) read-only.
+   *
+   * @param see `playground.editor`
+   * @param whether the CodeMirror editor should be editable
+   *
+   * @return the new value of the read-only setting
+   */
+  playground.editor.setReadOnly = function(editors, value){
+    value = Boolean(value);
+    playground.editor(editors, function(editor){
+      editor.setOption("readOnly", value);
+      $(editor.getWrapperElement()).toggleClass("read-only", value);
+    });
+    return value;
+  };
+
+
+  /**
+  * Refresh one or more CodeMirror editors, such as after being revealed.
+  *
+  * @param see `playground.editor`
+  */
+  playground.editor.refresh = function(editor){
+    return playground.editor(editor, function(editor){
+      editor.refresh();
+    });
+  };
+
+
   /**
    * Callback for when tabs are selected in the UI.
    *
    * @param event the event that fired when the tab was selected.
+   *
+   * @return the process() promise
    */
   playground.tabSelected = function(evt) {
-
     var id = playground.activeTab = evt.target.id;
 
     if(['tab-compacted', 'tab-flattened', 'tab-framed'].indexOf(id) > -1) {
@@ -358,24 +606,32 @@
       $('#param-type').html('');
     }
 
-    $.each(playground.editors, function(id, editor){ editor.refresh(); });
+    // refresh all the editors
+    playground.editor.refresh();
+    playground.editor.refresh(playground.outputs);
 
     // perform processing on the data provided in the input boxes
-    playground.process();
+    return playground.process();
   };
 
+
   /**
    * Returns a Promise to performs the JSON-LD API action based on the active
    * tab.
    *
    * @param input the JSON-LD object input or null no error.
    * @param param the JSON-LD param to use.
+   *
+   * @return a promise to perform the action
    */
   playground.performAction = function(input, param) {
     var processor = new jsonld.JsonLdProcessor();
 
     // set base IRI
-    var options = {base: (document.baseURI || document.URL)};
+    var options = {
+      base: (playground.useRemote.markup && playground.remoteUrl.markup) ||
+        document.baseURI || document.URL
+    };
 
     var promise;
     if(playground.activeTab === 'tab-compacted') {
@@ -409,11 +665,14 @@
     });
   };
 
+
   /**
    * Process the JSON-LD markup that has been input and display the output
    * in the active tab.
+   *
+   * @return a promise to process
    */
-  playground.process = function() {
+  playground.process = function(){
     $('#markup-errors').text('');
     $('#param-errors').text('');
     $('#processing-errors').text('');
@@ -422,6 +681,7 @@
     playground.activeContextMap = {};
     var errors = false;
     var markup = playground.editors.markup.getValue();
+    var input;
 
     // nothing to process
     if(markup === '') {
@@ -430,10 +690,7 @@
 
     // check to see if the JSON-LD markup is valid JSON
     try {
-      var input = jsonlint.parse(markup);
-      playground.lastParsed.markup = input.parsedObject;
-      playground.lineIndex.markup = input.lineIndex;
-      input = input.parsedObject;
+      input = playground.lastParsed.markup = JSON.parse(markup);
     }
     catch(e) {
       $('#markup-errors').text('JSON markup - ' + e);
@@ -460,10 +717,7 @@
 
     if(needParam) {
       try {
-        param = jsonlint.parse(jsonParam);
-        playground.lastParsed[paramType] = param.parsedObject;
-        playground.lineIndex[paramType] = param.lineIndex;
-        param = param.parsedObject;
+        playground.lastParsed[paramType] = param = JSON.parse(jsonParam);
       }
       catch(e) {
         $('#param-errors').text($('#param-type').text() + ' - ' + e);
@@ -471,92 +725,141 @@
       }
     }
 
-    // errors detected
-    if(errors) {
-      $('#permalink').hide();
-      return;
-    }
-
     // no errors, perform the action and display the output
-    playground.performAction(input, param).then(function() {
-      // generate a link for current data
-      var link = '?json-ld=' + encodeURIComponent(JSON.stringify(input));
-      if(playground.editors.frame.getValue().length > 0) {
-        link += '&frame=' + encodeURIComponent(playground.editors.frame.getValue());
-      }
-      if(playground.editors.context.getValue().length > 0) {
-        link += '&context=' + encodeURIComponent(playground.editors.context.getValue());
-      }
+    return playground.performAction(input, param)
+      .then(
+        function(){
+          playground.permalink();
+        },
+        function(err){
+          // FIXME: add better error handling output
+          $('#processing-errors').text(playground.humanize(err));
+          playground.permalink(err);
+        }
+      );
+  };
 
-      // Start at the currently active tab
-      link += '&startTab=' + encodeURIComponent(playground.activeTab);
 
-      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+)';
+  /**
+   * Update the permalink button with a `#` link to the current playground
+   *
+   * @param the error object, string or object
+   *
+   * @return the current permalink URL
+   */
+  playground.permalink = function(errors) {
+    // generate a hash link for current data, starting with the tab
+    var loc = window.location.href.replace(/[#\?].*$/, ""),
+      hash = "",
+      params = {
+        startTab: playground.activeTab,
+        copyContext: playground.copyContext
+      },
+      val,
+      messages;
+
+    // check the editors for inputs/remotes
+    $.each(playground.editors, function(key){
+      if(key === "context" && params.copyContext){ return; }
+      val = playground.useRemote[key] ? playground.remoteUrl[key] : null;
+      val = val ? val : JSON.stringify(playground.lastParsed[key]);
+      if(val && val !== "null"){
+        params[key] = val;
       }
-      $('#permalink')
-        .html(permalink)
-        .show();
-    }, function(err) {
-      // FIXME: add better error handling output
-      $('#processing-errors').text(JSON.stringify(err));
     });
+
+    // encode and concat the hash components
+    $.each(params, function(key, val){
+      if(!val){ return; }
+      hash += (hash ? "&" : "#") +
+        (key === "markup" ? "json-ld" : key) + "=" + encodeURIComponent(val);
+    });
+
+    messages = {
+      danger: errors === void 0 ? "" :
+        "This link will show the current errors.",
+      warning: (loc + hash).length < 2048 ? "" :
+        "This link is longer than 2kb, and may not work."
+    };
+
+    playground.permalink.title =  messages.danger + " " + messages.warning;
+
+    $("#permalink").tooltip({
+      title: function(){
+        var tip = $("<p/>"),
+          inp = $("<input/>", {
+            "class": "span2",
+            value: loc + hash,
+            autofocus: true
+          });
+        tip.append(
+          $("<span/>")
+            .text(playground.permalink.title.trim() + " Press Ctrl+C to copy."),
+          inp
+        );
+
+        setTimeout(function(){
+          inp[0].select();
+        });
+
+        return tip[0];
+      },
+      html: true
+    });
+
+    $("#permalink")
+      .attr({
+        href: loc + hash
+      })
+      .toggleClass("btn-danger", messages.danger.length !== 0)
+      .toggleClass("btn-warning", messages.warning.length !== 0)
+    .find("span")
+      .text("Permalink");
+
+    return loc + hash;
   };
 
+
   /**
    * Populate the UI with markup, frame, and context JSON. The data parameter
    * should have a 'markup' field and optional 'frame' and 'context' fields.
    *
    * @param data object with optional 'markup', 'frame' and 'context' fields.
+   *
+   * @return the process promise, or `undefined` if no data was found
    */
   playground.populateWithJSON = function(data) {
     var hasData = false;
 
-    if('markup' in data && data.markup !== null) {
-      hasData = true;
-      // fill the markup box with the example
-      playground.editors.markup.setValue(playground.humanize(data.markup));
-    }
+    $.each(playground.editors, function(key, editor){
+      if(key in data && data[key] !== null){
+        hasData = true;
+        editor.setValue(playground.humanize(data[key]));
+      }else{
+        editor.setValue("{}");
+      }
+    });
 
-    if('frame' in data && data.frame !== null) {
-      hasData = true;
-      // fill the frame input box with the given frame
-      playground.editors.frame.setValue(playground.humanize(data.frame));
-    }
-    else {
-      playground.editors.frame.setValue('{}');
-    }
-
-    if('context' in data && data.context !== null) {
-      hasData = true;
-      // fill the context input box with the given context
-      playground.editors.context.setValue(playground.humanize(data.context));
-    }
-    else {
-      playground.editors.context.setValue('{}');
+    if(playground.copyContext){
+      playground.toggleCopyContext(true);
     }
 
     if(hasData) {
       // perform processing on the data provided in the input boxes
-      playground.process();
+      return playground.process();
     }
   };
 
+
   /**
    * Populate the UI with a named example.
    *
    * @param name the name of the example to pre-populate the input boxes.
+   *
+   * @return a promise to process the data, or `undefined`
    */
   playground.populateWithExample = function(name) {
-    var data = {
-      markup: null,
-      context: null,
-      frame: null
-    };
+    var data = docs();
 
     if(name in playground.examples) {
       // fill the markup with the example
@@ -577,10 +880,18 @@
       }
     }
 
+    // clean up any remote URLs
+    $.each(playground.editors, function(key){
+      playground.toggleRemote(key, false);
+      playground.setRemoteUrl(key, null);
+    });
+    playground.toggleCopyContext(false);
+
     // populate with the example
-    playground.populateWithJSON(data);
+    return playground.populateWithJSON(data);
   };
 
+
   // event handlers
   $(document).ready(function() {
     // Add custom document loader that uses a context URL map.
@@ -606,7 +917,7 @@
     };
 
     // set up buttons to load examples
-    $('.button').each(function(idx) {
+    $('.button').each(function() {
       var button = $(this);
       button.click(function() {
         playground.populateWithExample(button.find('span').text());
@@ -616,26 +927,6 @@
     $('#use-context-map').change(function() {
       playground.process();
     });
-
-    $('#theme-select a').click(function(evt) {
-      var theme = evt.currentTarget.text,
-        file = evt.currentTarget.title ? evt.currentTarget.title : theme,
-        key;
-
-      $("#theme-name").text(theme);
-
-      $('#theme-stylesheet').prop("href",
-        "//cdnjs.cloudflare.com/ajax/libs/codemirror/3.16.0/theme/" +
-        file + ".css"
-      );
-
-      for(key in playground.editors) {
-        playground.editors[key].setOption("theme", theme);
-      }
-
-      for(key in playground.outputs) {
-        playground.outputs[key].setOption("theme", theme);
-      }
-    });
   });
-})(jQuery, CodeMirror);
+  return playground;
+}).call(this, this.jQuery, this.CodeMirror, this.jsonld, this.Promise);