PageSourceSearch

https://json-ld.org/playground/playground.js

js json-ld.org collected 2026-10-01 06:57:40 UTC 50,149 bytes, 1,676 lines download raw bytes

1/**
2 * The JSON-LD playground is used to test out JavaScript Object Notation
3 * for Linked Data.
4 *
5 * @author Manu Sporny <[email protected]>
6 * @author Dave Longley <[email protected]>
7 * @author Nicholas Bollweg
8 * @author Markus Lanthaler
9 */
10;(function($, CodeMirror, jsonld, Promise){
11  "use strict";
12  // assume nothing
13  var window = this,
14  console = window.console,
15  setTimeout = window.setTimeout,
16  document = window.document,
17
18  // create the playground instance if it doesn't already exist
19  playground = window.playground = {},
20
21  // given this is needed, we probably need a `Document` class...
22  docs = function(){
23    return {
24      markup: null,
25      frame: null,
26      context: null
27    };
28  };
29
30  // the codemirror editors
31  playground.editors = docs();
32
33  // ... and outputs
34  playground.outputs = {};
35
36  // default theme
37  playground.theme = "neat";
38
39  // the last parsed version of same
40  playground.lastParsed = docs();
41
42  // set the active tab to the expanded view
43  playground.activeTab = 'tab-expanded';
44
45  // map of original to modifed contexts
46  playground.contextMap = {
47    // be careful when working with redirectors as as Chrome (not firefox)
48    // will drop Accept: application/ld+json after redirecting
49  };
50
51  // map of currently active mapped contexts for user feedback use
52  playground.activeContextMap = {};
53
54  // JSON schema for JSON-LD documents
55  playground.schema = null;
56
57  // copy context from the input
58  playground.copyContext = false;
59
60  // currently-active urls
61  playground.remoteUrl = docs();
62
63  // whether a remote document should be used
64  playground.useRemote = docs();
65
66
67  /**
68   * Get a query parameter by name.
69   *
70   * Code from:
71   * http://stackoverflow.com/questions/901115/get-query-string-values-in-javascript/5158301#5158301
72   *
73   * @param name a query parameter name.
74   *
75   * @return the value of the parameter or null if it does not exist
76   */
77  function getParameterByName(name) {
78    var match = new RegExp('[#?&]' + name + '=([^&]*)')
79      .exec(window.location.hash || window.location.search);
80    return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
81  }
82
83
84  /**
85   * Consistent human-readable JSON formatting.
86   *
87   * @param the object or string to humanize.
88   *
89   * @return a string containing the humanized string.
90   */
91  playground.humanize = function(value) {
92    switch($.type(value)) {
93      case 'string':
94        return value;
95      case 'error':
96        // TODO: limit output size
97        var str = value.toString();
98        if('details' in value) {
99          // TODO: improve error handling
100          // special cases as of jsonld.js 8.0.0
101          if('event' in value.details) {
102            str += '\n' + JSON.stringify(value.details.event, null, 2);
103          }
104          // some error and event details can be verbose and need good handling
105          // all errors and events should have type codes that can be used
106        }
107        return str;
108      default:
109        return JSON.stringify(value, null, 2);
110    }
111  };
112
113
114  /**
115   * Handle URL query parameters.
116   *
117   * Checks 'json-ld', 'context', and 'frame' parameters.  If they look
118   * like JSON then interpret as JSON strings else interpret as URLs of remote
119   * resources.  Note: URLs must be CORS enabled to load due to browser same
120   * origin policy issues.
121   *
122   * If 'startTab' is present, select that tab automatically.
123   */
124  playground.processQueryParameters = function() {
125    // data from the query
126    var queryData = docs();
127
128    /**
129     * Read a parameter as JSON or create an jQuery AJAX Deferred call
130     * to read the data.
131     *
132     * @param param a query parameter value.
133     * @param fieldName the field name to populate in queryData object.
134     * @param msgName the param name to use in UI messages.
135     *
136     * @return jQuery Deferred or null.
137     */
138    function handleParameter(param, fieldName) {
139      // the ajax deferred or null
140      var rval = null;
141
142      // check 'json-ld' parameter
143      if(param !== null) {
144        if(param.length === 0 || param[0] === '{' || param[0] === '[') {
145          // param looks like JSON, try to parse it
146          try {
147            queryData[fieldName] = JSON.parse(param);
148            playground.setRemoteUrl(fieldName, null);
149          }
150          catch(e) {
151            queryData[fieldName] = param;
152          }
153        }
154        else {
155          rval = playground.setRemoteUrl(fieldName, param);
156          playground.toggleRemote(fieldName, true);
157          if(rval){
158            rval.then(function(data){
159              queryData[fieldName] = data;
160            });
161          }
162        }
163      }
164
165      return rval;
166    }
167
168    function handleGist(gist){
169      /**
170       * Turn the contents of a gist into the equivalent of the legacy URL
171       * scheme
172       * @param gist: a gist id
173      */
174      var deferreds = [],
175        manifest = JSON.parse(gist.files["playground.jsonld"].content),
176        done = function(){
177          $('#' + manifest["startTab"]).tab('show');
178          playground.populateWithJSON(queryData);
179        };
180
181      playground.copyContext = manifest["copyContext"];
182
183      $.each(queryData, function(key, val){
184        val = manifest[key];
185        if(val){
186          // is within this gist?
187          if(val[0] === "."){
188            var file = gist.files[val.slice(2)];
189            if(!file.truncated){
190              queryData[key] = JSON.parse(file.content);
191              playground.setRemoteUrl(key, null);
192            }else{
193              deferreds.push($.get(file.raw_url).done(function(data){
194                queryData[fieldName] = JSON.parse(data);
195              }));
196            }
197          }else{
198            playground.toggleRemote(key, true);
199            var rval = playground.setRemoteUrl(key, val);
200            if(rval){
201              rval.then(function(data){
202                queryData[fieldName] = data;
203              });
204              deferreds.push(rval);
205            }
206          }
207        }
208      });
209
210      return deferreds.length ?
211        $.when.apply($, deferreds).done(done) :
212        done();
213    }
214
215    var gistId = new RegExp('^#/gist/(.*)').exec(window.location.hash);
216
217    if(gistId){
218      playground.gist(gistId[1]).done(handleGist);
219      return;
220    }
221
222    // build deferreds
223    var jsonLdDeferred = handleParameter(
224      getParameterByName('json-ld'), 'markup', 'JSON-LD');
225
226    var frameDeferred = handleParameter(
227      getParameterByName('frame'), 'frame', 'frame');
228
229    var contextDeferred = handleParameter(
230      getParameterByName('context'), 'context', 'context');
231
232    var paramDeferred = handleParameter(
233      getParameterByName('param'), 'param', 'param');
234
235    var startTab = getParameterByName('startTab');
236    if(startTab) {
237       $('#' + startTab).tab('show');
238    }
239
240    playground.copyContext = getParameterByName('copyContext') === "true";
241
242    // wait for ajax if needed
243    // failures handled in AJAX calls
244    $.when(jsonLdDeferred, frameDeferred, contextDeferred, paramDeferred)
245      .done(function() {
246        // Maintain backwards permalink compatability
247        if(queryData.param && !(queryData.frame || queryData.context)) {
248          queryData.frame = queryData.context = queryData.param;
249        }
250        // populate UI with data
251        playground.populateWithJSON(queryData);
252      });
253  };
254
255
256  /**
257   * Used to initialize the UI, call once on document load.
258   */
259  playground.init = function() {
260    // options storage
261    playground.options = {
262      api: {
263        // TODO: add other API options
264        processingMode: '',
265        base: '',
266        baseUrl: '',
267        compactArrays: true,
268        compactToRelative: true,
269        rdfDirection: '',
270        safe: ''
271      }
272    };
273
274    // enable bootstrap tabs
275    $('#input-tabs a,' +
276      '#context-tabs a,' +
277      '#frame-tabs a,' +
278      '#privatekey-rsa-tabs a,' +
279      '#privatekey-koblitz-tabs a').click(function (e) {
280      e.preventDefault();
281      $(this).tab('show');
282    });
283    $('#output-tabs a').click(function (e) {
284      e.preventDefault();
285      $(this).tab('show');
286    }).on("show", playground.tabSelected);
287
288    // show keybaord shortcuts
289    $('.popover-info').popover({
290      placement: "left",
291      html: true,
292      content: $(".popover-info-content").html()
293    });
294
295    CodeMirror.commands.autocomplete = function(cm) {
296      CodeMirror.showHint(cm, CodeMirror.hint.jsonld, {
297        lastParsed: playground.lastParsed[cm.options._playground_key],
298        completeSingle: false,
299        schemata: function(){
300          return playground.schema ? [playground.schema] : [];
301        }
302      });
303    };
304
305    CodeMirror.commands.at_autocomplete = function(cm) {
306      CodeMirror.showHint(cm, CodeMirror.hint.jsonld, {
307        isAt: true,
308        completeSingle: false,
309        lastParsed: playground.lastParsed[cm.options._playground_key],
310        schemata: function(){
311          return playground.schema ? [playground.schema] : [];
312        }
313      });
314    };
315
316    $(".codemirror-input").each(function(){ playground.init.editor(this); });
317    $(".codemirror-output").each(function(){ playground.init.output(this); });
318
319    playground.makeResizer($("#markup-container"), playground.editors);
320    playground.makeResizer($("#output-container"), playground.outputs);
321
322    $("#copy-context").click(function(){
323      playground.toggleCopyContext();
324    });
325
326    $(".editor-option").each(function(){
327      var option = $(this),
328        key = option.data("editor");
329      option.find("input").bind("input", function(){
330        playground.setRemoteUrl(key, this.value);
331      });
332      option.find("button").bind("click", function(){
333        playground.toggleRemote(key);
334      });
335    });
336
337    // setup options
338    $("#options-api-processingMode-default").prop(
339      'checked', playground.options.api.processingMode === '');
340    $("#options-api-processingMode-1-0").prop(
341      'checked', playground.options.api.processingMode === 'json-ld-1.0');
342    $("#options-api-processingMode-1-1").prop(
343      'checked', playground.options.api.processingMode === 'json-ld-1.1');
344
345    $("#options-api-base-default").prop(
346      'checked', playground.options.api.base === '');
347    $("#options-api-base-custom").prop(
348      'checked', playground.options.api.base === 'custom');
349
350    $("#options-api-base-url").value = playground.options.api.baseUrl;
351
352    $("#options-api-compactArrays").prop(
353      'checked', playground.options.api.compactArrays);
354
355    $("#options-api-compactToRelative").prop(
356      'checked', playground.options.api.compactToRelative);
357
358    $("#options-api-rdfDirection-default").prop(
359      'checked', playground.options.api.rdfDirection === '');
360    $("#options-api-rdfDirection-i18n-datatype").prop(
361      'checked', playground.options.api.rdfDireciton === 'i18n-datatype');
362    //$("#options-api-rdfDirection-compound-literal").prop(
363    //  'checked', playground.options.api.rdfDirection === 'compound-literal');
364
365    $("#options-api-safe-default").prop(
366      'checked', playground.options.api.safe === '');
367    $("#options-api-safe-false").prop(
368      'checked', playground.options.api.safe === false);
369    $("#options-api-safe-true").prop(
370      'checked', playground.options.api.safe === true);
371
372    // process on option changes
373    $("#options-api-processingMode-default, " +
374      "#options-api-processingMode-1-0, " +
375      "#options-api-processingMode-1-1").change(function(e) {
376      playground.options.api.processingMode = e.target.value;
377      playground.process();
378    });
379
380    $("#options-api-base-default, " +
381      "#options-api-base-custom").change(function(e) {
382      playground.options.api.base = e.target.value;
383      playground.process();
384    });
385
386    $("#options-api-base-url").on('keypress', function(e) {
387      var keyPressed = e.keyCode || e.which;
388      if(keyPressed === 13) {
389        e.preventDefault();
390        return false;
391      }
392    });
393    $("#options-api-base-url").on('input', function(e) {
394      playground.options.api.baseUrl = e.target.value;
395      playground.process();
396    });
397
398    $("#options-api-compactArrays").change(function(e) {
399      playground.options.api.compactArrays = e.target.checked;
400      playground.process();
401    });
402
403    $("#options-api-compactToRelative").change(function(e) {
404      playground.options.api.compactToRelative = e.target.checked;
405      playground.process();
406    });
407
408    $("#options-api-rdfDirection-default" ).change(function(e) {
409      playground.options.api.rdfDirection = '';
410      playground.process();
411    });
412    $("#options-api-rdfDirection-i18n-datatype").change(function(e) {
413      playground.options.api.rdfDirection = 'i18n-datatype';
414      playground.process();
415    });
416    //$("#options-api-rdfDirection-compound-literal").change(function(e) {
417    //  playground.options.api.rdfDirection = 'compound-literal';
418    //  playground.process();
419    //});
420
421    $("#options-api-safe-default" ).change(function(e) {
422      playground.options.api.safe = '';
423      playground.process();
424    });
425    $("#options-api-safe-false").change(function(e) {
426      playground.options.api.safe = false;
427      playground.process();
428    });
429    $("#options-api-safe-true").change(function(e) {
430      playground.options.api.safe = true;
431      playground.process();
432    });
433
434    $("[title]").tooltip();
435
436    $(window).bind("hashchange", function(){
437      if(window.location.href !== playground.permalink.url){
438        playground.processQueryParameters();
439        $("#permalink").popover("hide");
440      }
441    });
442
443    // load the schema
444    $.ajax({
445        url: "/schemas/jsonld-schema.json",
446        dataType: "json"
447      })
448      .done(function(schema){
449        playground.schema = schema;
450      })
451      .fail(function(){
452        console.warn("Schema could not be loaded. Schema validation disabled.");
453      });
454
455    if(window.location.search || window.location.hash) {
456      playground.processQueryParameters();
457    }
458    $(".loading").fadeOut(function(){
459      $(this).remove();
460      $(".loaded").fadeIn();
461      playground.editor.refresh();
462      playground.editor.refresh(playground.outputs);
463    });
464  };
465
466
467  /**
468   * return a debounced copy of a function
469   * thanks to @cwarden
470   * https://github.com/cwarden/promising-debounce/blob/master/src/debounce.js
471   *
472   * @param a function
473   * @param a number of milliseconds
474   *
475   * @return the function, which will only be called every `delay` milliseconds,
476   *         which will then, in turn, return a $.Deferred
477   */
478  playground.debounce = function(fn, wait, immediate) {
479    var timer = null;
480    return function() {
481      var context = this;
482      var args = arguments;
483      var resolve;
484      var promise = new Promise(function(_resolve) {
485        resolve = _resolve;
486      }).then(function() {
487        return fn.apply(context, args);
488      });
489      if(!!immediate && !timer) {
490        resolve();
491      }
492      if(timer) {
493        clearTimeout(timer);
494      }
495      timer = setTimeout(function() {
496        timer = null;
497        if(!immediate) {
498          resolve();
499        }
500      }, wait);
501      return promise;
502    };
503  };
504
505
506  /**
507   * Initialize a CodeMirror editor
508   *
509   * @param a `<textarea>`
510   *
511   * @return the CodeMirror editor
512   */
513  playground.init.editor = function(node){
514    var key = node.id;
515    var editor;
516
517    // don't use JSON-LD for PEM data
518    if(key === 'privatekey-rsa' || key === 'privatekey-koblitz' ||
519            key === 'publickey-koblitz') {
520      editor = CodeMirror.fromTextArea(node, {
521        matchBrackets: true,
522        autoCloseBrackets: true,
523        lineWrapping: false,
524        mode: 'text',
525        theme: playground.theme,
526        _playground_key: key
527      });
528    } else {
529      editor = CodeMirror.fromTextArea(node, {
530        matchBrackets: true,
531        autoCloseBrackets: true,
532        lineWrapping: true,
533        mode: "application/ld+json",
534        gutters: ["CodeMirror-lint-markers"],
535        theme: playground.theme,
536        lintWith: {
537          getAnnotations: CodeMirror.lint.jsonSchema,
538          async: true,
539          schemata: function(){
540            return playground.schema ? [playground.schema] : [];
541          }
542        },
543        extraKeys: {
544          "Ctrl-Space": "autocomplete"
545        },
546        _playground_key: key
547      });
548    }
549
550    playground.editors[key] = editor;
551
552    // set up 'process' areas to process JSON-LD after typing
553    editor.on("change", function(){
554      if(playground.copyContext && key === "markup"){
555        if(playground.toggleCopyContext.copy()){
556          return;
557        }
558      }
559      playground.process();
560    });
561
562    // check on every keyup for `@`: doesn't get caught by (extra|custom)Keys
563    editor.on("keyup", function(editor, evt) {
564      // these are the keys that move the cursor
565      var noHint = [
566        8, // backspace
567        9, // hey! where's my tab?
568        33, 34, 35, 46, // pg/home/end
569        37, 38, 39, 40, // arrows
570        12, // enter
571        27 //escape
572      ];
573
574      if(noHint.indexOf(evt.keyCode) >= 0) { return; }
575
576      var cursor = editor.getCursor(),
577        token = editor.getTokenAt(cursor),
578        chr = token.string[cursor.ch - token.start - 1];
579      if(chr === "@") {
580        CodeMirror.commands.at_autocomplete(editor, evt);
581      }
582    });
583
584    return editor;
585  };
586
587
588  /**
589   * Initialize a read-only CodeMirror viewer
590   *
591   * @param a `<textarea>`
592   *
593   * @return the CodeMirror editor
594   */
595  playground.init.output = function(node) {
596    var key = node.id,
597      output = playground.outputs[key] = CodeMirror.fromTextArea(node, {
598        readOnly: true,
599        lineWrapping: true,
600        mode: ["canonized", "nquads"].indexOf(key) > -1 ?
601          "text/n-triples" :
602          "application/ld+json",
603        theme: playground.theme
604      });
605    return output;
606  };
607
608
609  /**
610   * Toggle whether the output context will be updated from the input
611   *
612   * @param the new value of the setting. If not provided, invert current
613   *
614   * @return the JSON that was actually set, or `undefined` if nothing was set
615   */
616  playground.toggleCopyContext = function(val){
617    var editor = playground.editors.context;
618
619    playground.copyContext =  val = arguments.length ?
620      Boolean(val) :
621      !playground.copyContext;
622
623    playground.editor.setReadOnly(editor, val);
624
625    setTimeout(function(){
626      $("#copy-context").toggleClass("toggle", val);
627    }, 1);
628
629    if(val){
630      return playground.toggleCopyContext.copy();
631    }
632  };
633
634
635  /**
636   * Copy the context right now.
637   *
638   * @return the JSON that was set, or `undefined` if nothing was set
639   */
640  playground.toggleCopyContext.copy = function(){
641    var editor = playground.editors.context,
642      json = playground.humanize({
643        "@context": playground.lastParsed.markup ?
644          playground.lastParsed.markup["@context"] :
645          {}});
646    if(json !== editor.getValue()){
647      playground.editors.context.setValue(json);
648      return json;
649    }
650  };
651
652
653  /**
654   * Set the remote URL for an editor, then fetch (if enabled).
655   *
656   * @param the key for the editor
657   * @param the value
658   *
659   * @return jQuery deferred, or `undefined`
660   */
661  playground.setRemoteUrl = function(key, val){
662    var opt = $("[data-editor=" + key + "]"),
663      btn = opt.find("button"),
664      inp = opt.find("input");
665
666    playground.remoteUrl[key] = val ? val : null;
667
668    if(inp.val() != val){
669      inp.val(val);
670    }
671
672    // the button state is no longer valid
673    btn.removeClass("btn-danger btn-info");
674
675    return playground.fetchRemote(key);
676  };
677
678
679  /**
680   * Toggle (or set) whether a remote document will be used for an editor.
681   *
682   * @param the key for the editor
683   * @param the value: omit to toggle
684   *
685   * @return jQuery deferred, or `undefined`
686   */
687  playground.toggleRemote = function(key, val){
688    var btn = $("[data-editor=" + key + "] button");
689
690    playground.useRemote[key] = val = arguments.length === 2 ?
691      Boolean(val) :
692      !playground.useRemote[key];
693
694    playground.editor.setReadOnly(key, val);
695
696    // the button state is no longer valid
697    setTimeout(function(){
698      btn.removeClass("btn-danger btn-info" + (!val ? " active" : ""));
699    }, 1);
700    return playground.fetchRemote(key);
701  };
702
703
704  // Cache of fetched remote urls
705  playground.fetchRemoteCache = {};
706  playground.fetchRemoteFails = {};
707
708  /**
709   * Fetch a remote document and populate an editor.
710   *
711   * @param the key for the editor
712   *
713   * @return jQuery deferred, or `undefined`
714   */
715  playground.fetchRemote = playground._fetchRemote = function(key){
716    if(!playground.useRemote[key]){ return; }
717
718    var btn = $("[data-editor=" + key + "] button"),
719      debounced = playground.fetchRemote !== playground._fetchRemote,
720      url = playground.remoteUrl[key],
721      hit = playground.fetchRemoteCache[url],
722      fail = playground.fetchRemoteFails[url],
723      success = function(data){
724        playground.fetchRemoteCache[url] = data;
725        btn.addClass("btn-info active");
726        // setValue always triggers a .process()
727        playground.editors[key].setValue(playground.humanize(data));
728        playground.fetchRemote = playground._fetchRemote;
729      },
730      error = function(err) {
731        playground.fetchRemoteFails[url] = err;
732        btn.addClass("btn-danger active");
733        $('#processing-errors')
734          .text('Error loading ' + key + ' URL: ' + playground.remoteUrl[key])
735          .show();
736
737        playground.fetchRemote = debounced ?
738          playground.fetchRemote :
739          playground.debounce(playground._fetchRemote, 500);
740      };
741
742    return hit ? success(hit) :
743      fail ? error(fail) :
744      jsonld.documentLoader(url).then(function(remoteDoc) {
745        /* Note: Do injection of Link header @context; this could possibly
746        be done in a less obfuscated way (to the user) or use the expandContext
747        API option, or better integrate debouncing in the remote document
748        loader defined elsewhere. However, this approach was the least
749        intrusive to do now to restore Link header functionality and has the
750        advantage of allowing the document to be edited inline w/the
751        injected @context. */
752        if(remoteDoc.contextUrl) {
753          // TODO: flash link header injection notice on UI
754          if(Array.isArray(remoteDoc.document)) {
755            remoteDoc.document = {
756              '@context': remoteDoc.contextUrl,
757              '@graph': remoteDoc.document
758            };
759          } else if(typeof remoteDoc.document === 'object') {
760            // inject @context as first key
761            var obj = {'@context': remoteDoc.contextUrl};
762            for(var key in remoteDoc.document) {
763              obj[key] = remoteDoc.document[key];
764            }
765            remoteDoc.document = obj;
766          }
767        }
768        success(remoteDoc.document);
769      }).catch(error);
770  };
771
772
773  /**
774   * Make one or more editor resizeable together.
775   *
776   * @param parent the dom element to which the button should be attached
777   * @param an object or list of CodeMirror instances to be resized together
778   *
779   * @return the resizer button DOM
780   */
781  playground.makeResizer = function(parent, targets){
782    targets = $.map(targets, function(val){ return val; });
783    var start_y,
784      start_height,
785      handlers = {},
786      btn = $("<button/>", {"class": "btn resizer"})
787        .mousedown(handlers.mousedown = function(evt){
788          start_y = evt.screenY;
789          start_height = targets[0].display.wrapper.clientHeight;
790          $(window)
791            .bind("mousemove.resizer", function(evt){
792             targets.map(function(tgt){
793                tgt.setSize(null, start_height - (start_y - evt.screenY));
794              });
795            })
796            .bind("mouseup.resizer", function(){
797              $(window).unbind(".resizer");
798              btn.blur();
799            });
800        })
801        .appendTo(parent);
802    return btn[0];
803  };
804
805
806  /**
807   * Namespace for editor functions, and utility for doing things against them
808   *
809   * @param a keyed object of editors, the name of an editor, an editor
810   *        or a list of editors. or nothing, which assumes all of them.
811   * @param the action to peform, of the form `function(editor, key)`
812   *
813   * @return the result of the action
814   */
815  playground.editor = function(editors, action){
816    var key,
817      editor;
818    if($.type(editors) === "string"){
819      key = editors;
820      editors = {};
821      editors[key] = playground.editors[key];
822    }else if(editors instanceof CodeMirror){
823      key = editors.getTextArea().id;
824      editor = editors;
825      editors = {};
826      editors[key] = editor;
827    }else if(!editors){
828      editors = playground.editors;
829    }
830    return $.map(editors, action);
831  };
832
833
834  /**
835   * Make a CodeMirror editor (temporarily) read-only.
836   *
837   * @param see `playground.editor`
838   * @param whether the CodeMirror editor should be editable
839   *
840   * @return the new value of the read-only setting
841   */
842  playground.editor.setReadOnly = function(editors, value){
843    value = Boolean(value);
844    playground.editor(editors, function(editor){
845      editor.setOption("readOnly", value);
846      $(editor.getWrapperElement()).toggleClass("read-only", value);
847    });
848    return value;
849  };
850
851
852  /**
853  * Refresh one or more CodeMirror editors, such as after being revealed.
854  *
855  * @param see `playground.editor`
856  */
857  playground.editor.refresh = function(editor){
858    return playground.editor(editor, function(editor){
859      editor.refresh();
860    });
861  };
862
863
864  /**
865   * Callback for when tabs are selected in the UI.
866   *
867   * @param event the event that fired when the tab was selected.
868   *
869   * @return the process() promise
870   */
871  playground.tabSelected = function(evt) {
872    var id = playground.activeTab = evt.target.id;
873
874    if(['tab-compacted', 'tab-flattened', 'tab-framed',
875      /*'tab-signed-rsa', 'tab-signed-koblitz'*/].indexOf(id) > -1) {
876      // these options require more UI inputs, so compress UI space
877      $('#markup-div').removeClass('span12').addClass('span6');
878      $('#frame-div, #privatekey-rsa-div, #privatekey-koblitz-div, ' +
879        '#context-div').hide();
880      if(id === 'tab-compacted' || id === 'tab-flattened') {
881        $('#param-type').html('JSON-LD Context');
882        $('#context-div').show();
883      } else if(id==='tab-framed') {
884        $('#param-type').html('JSON-LD Frame');
885        $('#frame-div').show();
886      } else if(id === 'tab-signed-rsa') {
887        $('#param-type').html('PEM-encoded Private Key');
888        $('#privatekey-rsa-div').show();
889      } else if(id === 'tab-signed-koblitz') {
890        $('#param-type').html('Base 58 Encoded Private Key');
891        $('#privatekey-koblitz-div').show();
892      }
893    }
894    else {
895      // else no input textarea required
896      $('#context-div, #frame-div, #privatekey-rsa-div, ' +
897        '#privatekey-koblitz-div').hide();
898      $('#markup-div').removeClass('span6').addClass('span12');
899      $('#param-type').html('');
900    }
901
902    // refresh all the editors
903    playground.editor.refresh();
904    playground.editor.refresh(playground.outputs);
905
906    // perform processing on the data provided in the input boxes
907    return playground.process();
908  };
909
910
911  /**
912   * Returns a Promise to perform the JSON-LD API action based on the active
913   * tab.
914   *
915   * @param input the JSON-LD object input or null no error.
916   * @param param the JSON-LD param to use.
917   *
918   * @return a promise to perform the action
919   */
920  playground.performAction = function(input, param) {
921    // set options
922    var options = {};
923    if(playground.options.api.processingMode !== '') {
924      options.processingMode = playground.options.api.processingMode;
925    }
926    if(playground.options.api.base === 'custom') {
927      options.base = playground.options.api.baseUrl || '';
928    } else {
929      // default base IRI is based on remote url or set to null
930      options.base =
931        (playground.useRemote.markup && playground.remoteUrl.markup) || null;
932    }
933    options.compactArrays = playground.options.api.compactArrays;
934    options.compactToRelative = playground.options.api.compactToRelative;
935    if(playground.options.api.rdfDirection !== '') {
936      options.rdfDirection = playground.options.api.rdfDirection;
937    }
938    if(playground.options.api.safe !== '') {
939      options.safe = playground.options.api.safe;
940    }
941
942    var promise;
943    if(playground.activeTab === 'tab-compacted') {
944      promise = jsonld.compact(input, param, options);
945    }
946    else if(playground.activeTab === 'tab-expanded') {
947      promise = jsonld.expand(input, options);
948    }
949    else if(playground.activeTab === 'tab-flattened') {
950      promise = jsonld.flatten(input, param, options);
951    }
952    else if(playground.activeTab === 'tab-framed') {
953      promise = jsonld.frame(input, param, options);
954    }
955    else if(playground.activeTab === 'tab-nquads') {
956      options.format = 'application/n-quads';
957      promise = jsonld.toRDF(input, options);
958    }
959    else if(playground.activeTab === 'tab-canonized') {
960      options.format = 'application/n-quads';
961      promise = jsonld.canonize(input, options);
962    }
963    else if(playground.activeTab === 'tab-table') {
964      return jsonld.toRDF(input, options)
965        .then(_datasetToTable($('#table tbody')));
966    }
967    else if(playground.activeTab === 'tab-visualized') {
968      // early return because this isn't an editor
969      return new Promise(function() {
970        $('#visualized').empty();
971        d3.jsonldVis(input, '#visualized');
972      });
973    }
974    else if(playground.activeTab === 'tab-signed-rsa') {
975      options.format = 'application/ld+json';
976
977      var jsigs = window.jsigs || window['jsonld-signatures'];
978      var pkey = playground.editors['privatekey-rsa'].getValue();
979
980      var secCtx = jsigs.SECURITY_CONTEXT_URL;
981      var algorithm;
982      var newerJsigs = 'suites' in jsigs && 'RsaSignature2018' in jsigs.suites;
983      // FIXME: remove when jsonld.js updated
984      var v11Ctx = {'@version': 1.1};
985
986      if(newerJsigs) {
987        algorithm = 'RsaSignature2018';
988      } else {
989        algorithm = 'LinkedDataSignature2015';
990      }
991
992      // add security context to input
993      if(!('@context' in input)) {
994        input['@context'] = secCtx;
995      } else if(Array.isArray(input['@context'])) {
996        if(newerJsigs) {
997          input['@context'].unshift(v11Ctx);
998        }
999        input['@context'].push(secCtx);
1000      } else {
1001        input['@context'] =
1002          [v11Ctx, input['@context'], secCtx];
1003      }
1004
1005      promise = jsigs.promises.sign(input, {
1006        privateKeyPem: pkey,
1007        algorithm: algorithm,
1008        nonce: forge.util.bytesToHex(forge.random.getBytesSync(4)),
1009        domain: 'json-ld.org',
1010        creator: 'https://example.com/jdoe/keys/1'
1011      });
1012    }
1013    else if(playground.activeTab === 'tab-signed-koblitz') {
1014      options.format = 'application/ld+json';
1015
1016      var jsigs = window.jsigs || window['jsonld-signatures'];
1017      var privateKey = playground.editors['privatekey-koblitz'].getValue().trim();
1018
1019      // add security context to input
1020      if(!('@context' in input)) {
1021        input['@context'] = 'https://w3id.org/security/v1';
1022      } else if(Array.isArray(input['@context'])) {
1023        input['@context'].push('https://w3id.org/security/v1');
1024      } else {
1025        input['@context'] =
1026          [input['@context'], 'https://w3id.org/security/v1'];
1027      }
1028
1029      var signed = null;
1030      var publicKeyFromPrivateKey;
1031      try {
1032        publicKeyFromPrivateKey = new bitcoreMessage.Bitcore.PrivateKey(privateKey).toPublicKey();
1033      } catch (e) {
1034        promise = Promise.reject(e)
1035      }
1036
1037      promise = promise || jsigs.promises.sign(input, {
1038        privateKeyWif: privateKey,
1039        algorithm: 'EcdsaKoblitzSignature2016',
1040        creator: 'ecdsa-koblitz-pubkey:' + publicKeyFromPrivateKey
1041      }).then( function(_signed) {
1042        signed = _signed;
1043        var publicKey = playground.editors['publickey-koblitz'].getValue().trim();
1044        var verificationKey = {
1045          '@context': jsigs.SECURITY_CONTEXT_URL,
1046          id: 'ecdsa-koblitz-pubkey:' + publicKey,
1047          type: 'CryptographicKey',
1048          publicKeyWif: publicKey
1049        };
1050        var publicKeyBtcOwner = {
1051          '@context': jsigs.SECURITY_CONTEXT_URL,
1052          publicKey: [verificationKey]
1053        };
1054        return jsigs.promises.verify(signed, {
1055          publicKey: verificationKey,
1056          publicKeyOwner: publicKeyBtcOwner
1057        })
1058      }).then( function(verification) {
1059        if (verification) {
1060          $('#validation-message')
1061          .text('Signature validated successfully using this public key')
1062          .show();
1063        } else {
1064          $('#validation-errors')
1065          .text('Signature was NOT validated using this public key')
1066          .show();
1067        }
1068        return signed;
1069      }).catch( function(e) {
1070        console.error(e.stack)
1071        throw(e)
1072      });
1073    }
1074    else if(playground.activeTab === 'tab-signatures') {
1075      promise = Promise.resolve();
1076    }
1077    else {
1078      promise = Promise.reject(new Error('Invalid tab selection.'));
1079    }
1080
1081    return promise.then(function(result) {
1082      var outputTab = playground.activeTab.substr('tab-'.length);
1083      if(outputTab in playground.outputs) {
1084        result = playground.humanize(result);
1085        playground.outputs[outputTab].setValue(result);
1086      }
1087    });
1088  };
1089
1090  var _datasetToTable = function(tbody) {
1091    return function(dataset) {
1092      tbody.empty();
1093      // simple sort, good enough for this purpose
1094      // FIXME: is this needed?
1095      dataset.sort(function(a, b) {
1096        if(a.graph.value !== b.graph.value) {
1097          return a.graph.value > b.graph.value;
1098        }
1099        if(a.subject.value !== b.subject.value) {
1100          return a.subject.value > b.subject.value;
1101        }
1102        if(a.predicate.value !== b.predicate.value) {
1103          return a.predicate.value > b.predicate.value;
1104        }
1105        return a.object.value > b.object.value;
1106      });
1107      dataset.forEach(function(quad) {
1108        tbody.append(_tableRow(quad));
1109      });
1110    };
1111  };
1112
1113  var RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
1114  var RDF_LANGSTRING = RDF + 'langString';
1115  var XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
1116
1117  var _tableRow = function(quad) {
1118    var g = quad.graph;
1119    var s = quad.subject;
1120    var p = quad.predicate;
1121    var o = quad.object;
1122
1123    var row = $('<tr>');
1124
1125    // subject (IRI)
1126    var td = $('<td>');
1127    if(s.termType === 'NamedNode') {
1128      var a = $('<a>').attr('href', s.value).text(s.value);
1129      td.append(a);
1130    } else {
1131      td.text(s.value);
1132    }
1133    row.append(td);
1134
1135    // predicate (IRI)
1136    td = $('<td>');
1137    if(p.termType === 'NamedNode') {
1138      var a = $('<a>').attr('href', p.value).text(p.value);
1139      td.append(a);
1140    } else {
1141      td.text(p.value);
1142    }
1143    row.append(td);
1144
1145    // object (IRI, bnode, or literal)
1146    if(o.termType === 'NamedNode') {
1147      var a = $('<a>').attr('href', o.value).text(o.value);
1148      row.append($('<td>').append(a));
1149      row.append($('<td>'));
1150      row.append($('<td>'));
1151    } else if(o.termType === 'BlankNode') {
1152      row.append($('<td>').text(o.value));
1153      row.append($('<td>'));
1154      row.append($('<td>'));
1155    } else {
1156      row.append($('<td>').text(o.value));
1157      if(o.datatype.value === RDF_LANGSTRING) {
1158        if(o.language) {
1159          row.append($('<td>').text(o.language));
1160        } else {
1161          row.append($('<td>'));
1162        }
1163        row.append($('<td>'));
1164      } else if(o.datatype.value !== XSD_STRING) {
1165        row.append($('<td>'));
1166        var a = $('<a>').attr('href', o.datatype.value).text(o.datatype.value);
1167        row.append($('<td>').append(a));
1168      } else {
1169        row.append($('<td>'));
1170        row.append($('<td>'));
1171      }
1172    }
1173    /*
1174    td = $('<td>');
1175    if(o.type === 'IRI') {
1176      var a = $('<a>').attr('href', o.value).text(o.value);
1177      td.append(a);
1178    } else if(o.termType === 'BlankNode') {
1179      td.text(o.value);
1180    } else {
1181      var t = o.value;
1182      if(o.datatype === RDF_LANGSTRING) {
1183        if(o.language) {
1184          t += '@' + o.language;
1185        }
1186      } else if(o.datatype !== XSD_STRING) {
1187        t += '^^<' + o.datatype + '>';
1188      }
1189      td.text(t);
1190    }
1191    row.append(td);
1192    */
1193
1194    // TODO: ad lang and datatype columns?
1195
1196    // graph (null, IRI, bnode)
1197    td = $('<td>');
1198    if(g.value !== '') {
1199      if(g.value.indexOf('_:') !== 0) {
1200        var a = $('<a>').attr('href', g.value).text(g.value);
1201        td.append(a);
1202      } else {
1203        td.text(g.value);
1204      }
1205    }
1206    row.append(td);
1207
1208    return row;
1209  };
1210
1211  /**
1212   * Process the JSON-LD markup that has been input and display the output
1213   * in the active tab.
1214   *
1215   * @return a promise to process
1216   */
1217  playground.process = playground._process = function(){
1218    $('#markup-errors').hide().empty();
1219    $('#param-errors').hide().empty();
1220    $('#validation-errors').hide().empty();
1221    $('#validation-message').hide().empty();
1222    $('#processing-errors').hide().empty();
1223    $('#using-context-map').hide();
1224    $('#using-context-map table tbody').empty();
1225    playground.activeContextMap = {};
1226    var errors = false;
1227    var markup = playground.editors.markup.getValue();
1228    var input;
1229
1230    // nothing to process
1231    if(markup === '') {
1232      return;
1233    }
1234
1235    // check to see if the JSON-LD markup is valid JSON
1236    try {
1237      input = playground.lastParsed.markup = JSON.parse(markup);
1238    }
1239    catch(e) {
1240      $('#markup-errors')
1241        .text('JSON markup - ' + e)
1242        .show();
1243      errors = true;
1244    }
1245
1246    // If we're using a param, check to see if it is valid JSON
1247    var needParam = false;
1248    var param = null;
1249    var jsonParam = null;
1250    var paramType = null;
1251
1252    if(playground.activeTab === 'tab-compacted' ||
1253      playground.activeTab === 'tab-flattened') {
1254      jsonParam = playground.editors.context.getValue();
1255      needParam = true;
1256      paramType = "context";
1257    }
1258    else if(playground.activeTab === 'tab-framed') {
1259      jsonParam = playground.editors.frame.getValue();
1260      needParam = true;
1261      paramType = "frame";
1262    }
1263
1264    if(needParam) {
1265      try {
1266        playground.lastParsed[paramType] = param = JSON.parse(jsonParam);
1267      }
1268      catch(e) {
1269        $('#param-errors')
1270          .text($('#param-type').text() + ' - ' + e)
1271          .show();
1272        errors = true;
1273      }
1274    }
1275
1276    var debounced = playground.process !== playground._process;
1277
1278    // no errors, perform the action and display the output
1279    return playground.performAction(input, param)
1280      .then(
1281        function(){
1282          playground.permalink();
1283          playground.process = playground._process;
1284        },
1285        function(err){
1286          // FIXME: add better error handling output
1287          // log to allow console inspection
1288          console.error('Processing Error:', {error: err});
1289          $('#processing-errors')
1290            .append('Processing error:')
1291            .append(
1292              $('<span style="font-size: 0.7em; text-align=right">')
1293                .text(' (See console for details.)'))
1294            .append(
1295              $('<pre>').text(playground.humanize(err)))
1296            .show();
1297          playground.permalink(err);
1298          playground.process = debounced ?
1299            playground.process :
1300            playground.debounce(playground._process, 500);
1301          return err;
1302        }
1303      );
1304  };
1305
1306  playground.gist = function(gistId, description){
1307    var gistUrl = "https://api.github.com/gists";
1308
1309    if(gistId){
1310      var xhr = $.get(gistUrl + "/" + gistId);
1311
1312      playground.lastGist = null;
1313
1314      xhr.done(function(gist){
1315        playground.lastGist = gist;
1316      });
1317
1318      return xhr;
1319    }else{
1320      var files = {
1321          "README.md": {
1322            content:
1323              "# " + description + "\n\n" +
1324              "> This gist was automatically created by the [JSON-LD" +
1325              " Playground](http://json-ld.org/playground). You can see it" +
1326              " loaded there by visiting:\n" +
1327              "```\nhttp://json-ld.org/playground#/gist/<gist id>\n```"
1328          }
1329        },
1330        manifest = {
1331          "@context": {
1332            rdfs: "http://www.w3.org/2000/01/rdf-schema#",
1333            playground: "http://json-ld.org/playground#",
1334
1335            description: "rdfs:label",
1336
1337            startTab: "playground:startTab",
1338            copyContext: "playground:copyContext",
1339
1340            markup: {"@id": "playground:markup", "@type": "@id"},
1341            context: {"@id": "playground:context", "@type": "@id"},
1342            frame: {"@id": "playground:frame", "@type": "@id"}
1343          },
1344          "description": description,
1345          "startTab": playground.activeTab,
1346          "copyContext": playground.copyContext
1347        };
1348
1349      // check the editors for inputs/remotes
1350      $.each(playground.editors, function(key){
1351        if(key === "context" && playground.copyContext){ return; }
1352        if(playground.useRemote[key]){
1353          manifest[key] = playground.remoteUrl[key];
1354        }else if(playground.lastParsed[key]){
1355          manifest[key] = "./" + key + ".jsonld";
1356          files[key + ".jsonld"] = {
1357            content: playground.humanize(playground.lastParsed[key])
1358          };
1359        }
1360      });
1361
1362      files["playground.jsonld"] = {
1363        content: playground.humanize(manifest)
1364      };
1365
1366      var gist = {
1367        description: "JSON-LD Playground: " + description,
1368        public: true,
1369        files: files
1370      };
1371
1372      return $.ajax({
1373        type: "POST",
1374        url: gistUrl,
1375        contentType: "application/json",
1376        dataType: "json",
1377        data: playground.humanize(gist)
1378      });
1379    }
1380  };
1381
1382  playground.gist.title = function(){
1383    return $("<div/>").text("Gist");
1384  };
1385
1386  playground.gist.content = function(){
1387    var content = $("<div/>").css({width: 320});
1388
1389
1390    content.append($("<p/>").text("Try to load a gist into the playground."));
1391
1392    var idWrap = $("<div/>", {"class": "input-append"})
1393      .appendTo(content);
1394
1395    content.append($("<p/>").text(
1396      "Create a public forkable, commentable version of the current playground configuration."
1397    ));
1398
1399    var descWrap = $("<div/>", {"class": "input-append"})
1400      .appendTo(content);
1401
1402    var gistId = $("<input/>", {
1403          "type": "text",
1404          "placeholder": "<gist id> or <user>/<gist id>"
1405        })
1406        .appendTo(idWrap),
1407
1408      gistDesc = $("<input/>", {
1409          "type": "text",
1410          "placeholder": "New Gist description"
1411        })
1412        .appendTo(descWrap);
1413
1414    $("<button/>", {"class": "btn btn-info"})
1415      .css({width: 100})
1416      .append(
1417        $("<i/>", {"class": "icon icon-cloud-download pull-left"}),
1418        $("<span/>").text("Load")
1419      )
1420      .click(function(){
1421        playground.gist(gistId.val()).done(function(gist){
1422          gistDesc.val(gist.description);
1423          window.location.hash = "/gist/" + gist.id;
1424        });
1425      })
1426      .appendTo(idWrap);
1427
1428    $("<button/>", {"class": "btn btn-primary"})
1429      .css({width: 100})
1430      .append(
1431        $("<i/>", {"class": "icon icon-cloud-upload pull-left"}),
1432        $("<span/>").text("Create")
1433      )
1434      .click(function(){
1435        playground.gist(null, gistDesc.val()).done(function(gist){
1436          gistDesc.val(null);
1437          gistId.val(gist.id);
1438        });
1439      })
1440      .appendTo(descWrap);
1441
1442    return content;
1443  };
1444
1445  /**
1446   * Update the permalink button with a `#` link to the current playground
1447   *
1448   * @param the error object, string or object
1449   *
1450   * @return the current permalink URL
1451   */
1452  playground.permalink = function(errors) {
1453    // generate a hash link for current data, starting with the tab
1454    var hash = "",
1455      params = {
1456        startTab: playground.activeTab,
1457        copyContext: playground.copyContext
1458      },
1459      val,
1460      messages;
1461
1462    // check the editors for inputs/remotes
1463    $.each(playground.editors, function(key){
1464      if(key === "context" && params.copyContext){ return; }
1465      val = playground.useRemote[key] ? playground.remoteUrl[key] : null;
1466      val = val ? val : JSON.stringify(playground.lastParsed[key]);
1467      if(val && val !== "null"){
1468        params[key] = val;
1469      }
1470    });
1471
1472    // encode and concat the hash components
1473    $.each(params, function(key, val){
1474      if(!val){ return; }
1475      hash += (hash ? "&" : "#") +
1476        (key === "markup" ? "json-ld" : key) + "=" + encodeURIComponent(val);
1477    });
1478
1479    playground.permalink.url = window.location.href.replace(/[#\?].*$/, "") +
1480      hash;
1481
1482    messages = {
1483      danger: errors === void 0 ? "" :
1484        "This link will show the current errors.",
1485      warning: playground.permalink.url.length < 2048 ? "" :
1486        "This link is longer than 2kb, and may not work."
1487    };
1488
1489    $("#permalink")
1490      .popover("hide")
1491      .attr({href: playground.permalink.url})
1492      .removeClass("hide")
1493      .toggleClass("btn-danger", messages.danger.length !== 0)
1494      .toggleClass("btn-warning", messages.warning.length !== 0)
1495    .find("span")
1496      .text("Permalink");
1497
1498    return playground.permalink.url;
1499  };
1500
1501  playground.permalink.title = function(){
1502    var title = $("<span/>");
1503    title.append(
1504      $("<span/>").text("Share this "),
1505      $("<a/>", {
1506        href: "http://tinyurl.com/create.php?url=" +
1507            playground.permalink.url.replace(/#/, "?"),
1508          target: "_blank",
1509          "class": "pull-right"
1510      }).text("Shorten"));
1511    return title[0];
1512  };
1513
1514  playground.permalink.content = function(){
1515    var tip = $("<p/>"),
1516      inp = $("<input/>", {
1517        "class": "span2",
1518        autofocus: true
1519      })
1520      .val(playground.permalink.url);
1521    tip.append(inp);
1522
1523    setTimeout(function(){
1524      inp[0].select();
1525    });
1526
1527    return tip[0];
1528  };
1529
1530
1531  /**
1532   * Populate the UI with markup, frame, and context JSON. The data parameter
1533   * should have a 'markup' field and optional 'frame' and 'context' fields.
1534   *
1535   * @param data object with optional 'markup', 'frame' and 'context' fields.
1536   *
1537   * @return the process promise, or `undefined` if no data was found
1538   */
1539  playground.populateWithJSON = function(data) {
1540    var hasData = false;
1541
1542    $.each(playground.editors, function(key, editor){
1543      if(key in data && data[key] !== null){
1544        hasData = true;
1545        editor.setValue(playground.humanize(data[key]));
1546      }else{
1547        if(key !== 'privatekey-rsa' && key !== 'privatekey-koblitz' &&
1548                key !== 'publickey-koblitz') {
1549          editor.setValue("{}");
1550        }
1551      }
1552    });
1553
1554    if(playground.copyContext){
1555      playground.toggleCopyContext(true);
1556    }
1557
1558    if(hasData) {
1559      // perform processing on the data provided in the input boxes
1560      return playground.process();
1561    }
1562  };
1563
1564
1565  /**
1566   * Populate the UI with a named example.
1567   *
1568   * @param name the name of the example to pre-populate the input boxes.
1569   *
1570   * @return a promise to process the data, or `undefined`
1571   */
1572  playground.populateWithExample = function(name) {
1573    var data = docs();
1574
1575    if(name in playground.examples) {
1576      // fill the markup with the example
1577      data.markup = playground.examples[name];
1578
1579      if(name in playground.frames) {
1580        // fill the frame with the example
1581        data.frame = playground.frames[name];
1582      }
1583
1584      if(name in playground.contexts) {
1585        // fill the context with the example
1586        data.contexts = playground.contexts[name];
1587      }
1588      else if('@context' in playground.examples[name]) {
1589        // use context from markup as default
1590        data.context = { '@context': playground.examples[name]['@context'] };
1591      }
1592    }
1593
1594    // clean up any remote URLs
1595    $.each(playground.editors, function(key){
1596      playground.toggleRemote(key, false);
1597      playground.setRemoteUrl(key, null);
1598    });
1599    playground.toggleCopyContext(false);
1600
1601    // populate with the example
1602    return playground.populateWithJSON(data);
1603  };
1604
1605
1606  // event handlers
1607  $(document).ready(function() {
1608    // Add custom document loader that uses a context URL map.
1609    var xhrDocumentLoader = jsonld.documentLoaders.xhr();
1610    // FIXME: add UI to let users control and set context mapping
1611    jsonld.documentLoader = function(url) {
1612      if(url in playground.contextMap) {
1613        $('#using-context-map').show();
1614        var modified = playground.contextMap[url];
1615        if(!(modified in playground.activeContextMap)) {
1616          var row = $('<tr>')
1617            .append('<td>' + url + '</td>')
1618            .append('<td>' + modified + '</td>');
1619          $('#using-context-map table tbody').append(row);
1620          playground.activeContextMap[url] = modified;
1621        }
1622
1623        if($('#use-context-map').prop('checked')) {
1624          url = modified;
1625        }
1626      }
1627      // rewrite URLs that we know have secure JSON-LD Contexts
1628      if(url === 'http://schema.org/' || url === 'http://schema.org') {
1629        url = 'https://schema.org/';
1630      }
1631
1632      // if a non-HTTPS URL, use the proxy since we run in HTTPS only mode
1633      if(!url.startsWith('https://')) {
1634        url = [
1635          location.protocol,
1636          '//',
1637          location.host,
1638          // NOTE: using hard-coded path so file can be shared with dev page
1639          //location.pathname,
1640          '/playground/',
1641          'proxy?url=',
1642          url
1643        ].join('');
1644      }
1645
1646      return xhrDocumentLoader(url);
1647    };
1648
1649    // set up buttons to load examples
1650    $('.button').each(function() {
1651      var button = $(this);
1652      button.click(function() {
1653        playground.populateWithExample(button.find('span').text());
1654      });
1655    });
1656
1657    $('#use-context-map').change(function() {
1658      playground.process();
1659    });
1660
1661    $("#permalink").popover({
1662      placement: "left",
1663      title: playground.permalink.title,
1664      content: playground.permalink.content,
1665      html: true
1666    });
1667
1668    $("#gist").popover({
1669      placement: "left",
1670      title: playground.gist.title,
1671      content: playground.gist.content,
1672      html: true
1673    });
1674  });
1675  return playground;
1676}).call(this, this.jQuery, this.CodeMirror, this.jsonld, this.Promise);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.