// Copyright 2012 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. /* In the absence of any formal way to specify interfaces in JavaScript, here's a skeleton implementation of a playground transport. function Transport() { // Set up any transport state (eg, make a websocket connection). return { Run: function(body, output, options) { // Compile and run the program 'body' with 'options'. // Call the 'output' callback to display program output. return { Kill: function() { // Kill the running program. } }; } }; } // The output callback is called multiple times, and each time it is // passed an object of this form. var write = { Kind: 'string', // 'start', 'stdout', 'stderr', 'end' Body: 'string' // content of write or end status message } // The first call must be of Kind 'start' with no body. // Subsequent calls may be of Kind 'stdout' or 'stderr' // and must have a non-null Body string. // The final call should be of Kind 'end' with an optional // Body string, signifying a failure ("killed", for example). // The output callback must be of this form. // See PlaygroundOutput (below) for an implementation. function outputCallback(write) { } */ // HTTPTransport is the default transport. // enableVet enables running vet if a program was compiled and ran successfully. // If vet returned any errors, display them before the output of a program. function HTTPTransport(enableVet) { 'use strict'; function playback(output, data) { // Backwards compatibility: default values do not affect the output. var events = data.Events || []; var errors = data.Errors || ''; var status = data.Status || 0; var isTest = data.IsTest || false; var testsFailed = data.TestsFailed || 0; var timeout; output({ Kind: 'start' }); function next() { if (!events || events.length === 0) { if (isTest) { if (testsFailed > 0) { output({ Kind: 'system', Body: '\n' + testsFailed + ' test' + (testsFailed > 1 ? 's' : '') + ' failed.', }); } else { output({ Kind: 'system', Body: '\nAll tests passed.' }); } } else { if (status > 0) { output({ Kind: 'end', Body: 'status ' + status + '.' }); } else { if (errors !== '') { // errors are displayed only in the case of timeout. output({ Kind: 'end', Body: errors + '.' }); } else { output({ Kind: 'end' }); } } } return; } var e = events.shift(); if (e.Delay === 0) { output({ Kind: e.Kind, Body: e.Message }); next(); return; } timeout = setTimeout(function() { output({ Kind: e.Kind, Body: e.Message }); next(); }, e.Delay / 1000000); } next(); return { Stop: function() { clearTimeout(timeout); }, }; } function error(output, msg) { output({ Kind: 'start' }); output({ Kind: 'stderr', Body: msg }); output({ Kind: 'end' }); } function buildFailed(output, msg) { output({ Kind: 'start' }); output({ Kind: 'stderr', Body: msg }); output({ Kind: 'system', Body: '\nGo build failed.' }); } var seq = 0; return { Run: function(body, output, options) { seq++; var cur = seq; var playing; $.ajax('/compile', { type: 'POST', data: { version: 2, body: body, withVet: enableVet }, dataType: 'json', success: function(data) { if (seq != cur) return; if (!data) return; if (playing != null) playing.Stop(); if (data.Errors) { if (data.Errors === 'process took too long') { // Playback the output that was captured before the timeout. playing = playback(output, data); } else { buildFailed(output, data.Errors); } return; } if (!data.Events) { data.Events = []; } if (data.VetErrors) { // Inject errors from the vet as the first events in the output. data.Events.unshift({ Message: 'Go vet exited.\n\n', Kind: 'system', Delay: 0, }); data.Events.unshift({ Message: data.VetErrors, Kind: 'stderr', Delay: 0, }); } if (!enableVet || data.VetOK || data.VetErrors) { playing = playback(output, data); return; } // In case the server support doesn't support // compile+vet in same request signaled by the // 'withVet' parameter above, also try the old way. // TODO: remove this when it falls out of use. // It is 2019-05-13 now. $.ajax('/vet', { data: { body: body }, type: 'POST', dataType: 'json', success: function(dataVet) { if (dataVet.Errors) { // inject errors from the vet as the first events in the output data.Events.unshift({ Message: 'Go vet exited.\n\n', Kind: 'system', Delay: 0, }); data.Events.unshift({ Message: dataVet.Errors, Kind: 'stderr', Delay: 0, }); } playing = playback(output, data); }, error: function() { playing = playback(output, data); }, }); }, error: function() { error(output, 'Error communicating with remote server.'); }, }); return { Kill: function() { if (playing != null) playing.Stop(); output({ Kind: 'end', Body: 'killed' }); }, }; }, }; } function SocketTransport() { 'use strict'; var id = 0; var outputs = {}; var started = {}; var websocket; if (window.location.protocol == 'http:') { websocket = new WebSocket('ws://' + window.location.host + '/socket'); } else if (window.location.protocol == 'https:') { websocket = new WebSocket('wss://' + window.location.host + '/socket'); } websocket.onclose = function() { console.log('websocket connection closed'); }; websocket.onmessage = function(e) { var m = JSON.parse(e.data); var output = outputs[m.Id]; if (output === null) return; if (!started[m.Id]) { output({ Kind: 'start' }); started[m.Id] = true; } output({ Kind: m.Kind, Body: m.Body }); }; function send(m) { websocket.send(JSON.stringify(m)); } return { Run: function(body, output, options) { var thisID = id + ''; id++; outputs[thisID] = output; send({ Id: thisID, Kind: 'run', Body: body, Options: options }); return { Kill: function() { send({ Id: thisID, Kind: 'kill' }); }, }; }, }; } function PlaygroundOutput(el) { 'use strict'; return function(write) { if (write.Kind == 'start') { el.innerHTML = ''; return; } var cl = 'system'; if (write.Kind == 'stdout' || write.Kind == 'stderr') cl = write.Kind; var m = write.Body; if (write.Kind == 'end') { m = '\nProgram exited' + (m ? ': ' + m : '.'); } if (m.indexOf('IMAGE:') === 0) { // TODO(adg): buffer all writes before creating image var url = 'data:image/png;base64,' + m.substr(6); var img = document.createElement('img'); img.src = url; el.appendChild(img); return; } // ^L clears the screen. var s = m.split('\x0c'); if (s.length > 1) { el.innerHTML = ''; m = s.pop(); } m = m.replace(/&/g, '&'); m = m.replace(//g, '>'); var needScroll = el.scrollTop + el.offsetHeight == el.scrollHeight; var span = document.createElement('span'); span.className = cl; span.innerHTML = m; el.appendChild(span); if (needScroll) el.scrollTop = el.scrollHeight - el.offsetHeight; }; } (function() { function lineHighlight(error) { var regex = /prog.go:([0-9]+)/g; var r = regex.exec(error); while (r) { $('.lines div') .eq(r[1] - 1) .addClass('lineerror'); r = regex.exec(error); } } function highlightOutput(wrappedOutput) { return function(write) { if (write.Body) lineHighlight(write.Body); wrappedOutput(write); }; } function lineClear() { $('.lineerror').removeClass('lineerror'); } // opts is an object with these keys // codeEl - code editor element // outputEl - program output element // runEl - run button element // fmtEl - fmt button element (optional) // fmtImportEl - fmt "imports" checkbox element (optional) // shareEl - share button element (optional) // shareURLEl - share URL text input element (optional) // shareRedirect - base URL to redirect to on share (optional) // toysEl - toys select element (optional) // enableHistory - enable using HTML5 history API (optional) // transport - playground transport to use (default is HTTPTransport) // enableShortcuts - whether to enable shortcuts (Ctrl+S/Cmd+S to save) (default is false) // enableVet - enable running vet and displaying its errors function playground(opts) { var code = $(opts.codeEl); var transport = opts['transport'] || new HTTPTransport(opts['enableVet']); var running; // autoindent helpers. function insertTabs(n) { // find the selection start and end var start = code[0].selectionStart; var end = code[0].selectionEnd; // split the textarea content into two, and insert n tabs var v = code[0].value; var u = v.substr(0, start); for (var i = 0; i < n; i++) { u += '\t'; } u += v.substr(end); // set revised content code[0].value = u; // reset caret position after inserted tabs code[0].selectionStart = start + n; code[0].selectionEnd = start + n; } function autoindent(el) { var curpos = el.selectionStart; var tabs = 0; while (curpos > 0) { curpos--; if (el.value[curpos] == '\t') { tabs++; } else if (tabs > 0 || el.value[curpos] == '\n') { break; } } setTimeout(function() { insertTabs(tabs); }, 1); } // NOTE(cbro): e is a jQuery event, not a DOM event. function handleSaveShortcut(e) { if (e.isDefaultPrevented()) return false; if (!e.metaKey && !e.ctrlKey) return false; if (e.key != 'S' && e.key != 's') return false; e.preventDefault(); // Share and save share(function(url) { window.location.href = url + '.go?download=true'; }); return true; } function keyHandler(e) { if (opts.enableShortcuts && handleSaveShortcut(e)) return; if (e.keyCode == 9 && !e.ctrlKey) { // tab (but not ctrl-tab) insertTabs(1); e.preventDefault(); return false; } if (e.keyCode == 13) { // enter if (e.shiftKey) { // +shift run(); e.preventDefault(); return false; } if (e.ctrlKey) { // +control fmt(); e.preventDefault(); } else { autoindent(e.target); } } return true; } code.unbind('keydown').bind('keydown', keyHandler); var outdiv = $(opts.outputEl).empty(); var output = $('
').appendTo(outdiv);

    function body() {
      return $(opts.codeEl).val();
    }
    function setBody(text) {
      $(opts.codeEl).val(text);
    }
    function origin(href) {
      return ('' + href)
        .split('/')
        .slice(0, 3)
        .join('/');
    }

    var pushedEmpty = window.location.pathname == '/';
    function inputChanged() {
      if (pushedEmpty) {
        return;
      }
      pushedEmpty = true;
      $(opts.shareURLEl).hide();
      window.history.pushState(null, '', '/');
    }
    function popState(e) {
      if (e === null) {
        return;
      }
      if (e && e.state && e.state.code) {
        setBody(e.state.code);
      }
    }
    var rewriteHistory = false;
    if (
      window.history &&
      window.history.pushState &&
      window.addEventListener &&
      opts.enableHistory
    ) {
      rewriteHistory = true;
      code[0].addEventListener('input', inputChanged);
      window.addEventListener('popstate', popState);
    }

    function setError(error) {
      if (running) running.Kill();
      lineClear();
      lineHighlight(error);
      output
        .empty()
        .addClass('error')
        .text(error);
    }
    function loading() {
      lineClear();
      if (running) running.Kill();
      output.removeClass('error').text('Waiting for remote server...');
    }
    function run() {
      loading();
      running = transport.Run(
        body(),
        highlightOutput(PlaygroundOutput(output[0]))
      );
    }

    function fmt() {
      loading();
      var data = { body: body() };
      if ($(opts.fmtImportEl).is(':checked')) {
        data['imports'] = 'true';
      }
      $.ajax('/fmt', {
        data: data,
        type: 'POST',
        dataType: 'json',
        success: function(data) {
          if (data.Error) {
            setError(data.Error);
          } else {
            setBody(data.Body);
            setError('');
          }
        },
      });
    }

    var shareURL; // jQuery element to show the shared URL.
    var sharing = false; // true if there is a pending request.
    var shareCallbacks = [];
    function share(opt_callback) {
      if (opt_callback) shareCallbacks.push(opt_callback);

      if (sharing) return;
      sharing = true;

      var sharingData = body();
      $.ajax('/share', {
        processData: false,
        data: sharingData,
        type: 'POST',
        contentType: 'text/plain; charset=utf-8',
        complete: function(xhr) {
          sharing = false;
          if (xhr.status != 200) {
            alert('Server error; try again.');
            return;
          }
          if (opts.shareRedirect) {
            window.location = opts.shareRedirect + xhr.responseText;
          }
          var path = '/p/' + xhr.responseText;
          var url = origin(window.location) + path;

          for (var i = 0; i < shareCallbacks.length; i++) {
            shareCallbacks[i](url);
          }
          shareCallbacks = [];

          if (shareURL) {
            shareURL
              .show()
              .val(url)
              .focus()
              .select();

            if (rewriteHistory) {
              var historyData = { code: sharingData };
              window.history.pushState(historyData, '', path);
              pushedEmpty = false;
            }
          }
        },
      });
    }

    $(opts.runEl).click(run);
    $(opts.fmtEl).click(fmt);

    if (
      opts.shareEl !== null &&
      (opts.shareURLEl !== null || opts.shareRedirect !== null)
    ) {
      if (opts.shareURLEl) {
        shareURL = $(opts.shareURLEl).hide();
      }
      $(opts.shareEl).click(function() {
        share();
      });
    }

    if (opts.toysEl !== null) {
      $(opts.toysEl).bind('change', function() {
        var toy = $(this).val();
        $.ajax('/doc/play/' + toy, {
          processData: false,
          type: 'GET',
          complete: function(xhr) {
            if (xhr.status != 200) {
              alert('Server error; try again.');
              return;
            }
            setBody(xhr.responseText);
          },
        });
      });
    }
  }

  window.playground = playground;
})();