// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// opts is an object with these keys
-// codeEl - code editor element
-// outputEl - program output element
-// runEl - run button element
-// fmtEl - fmt button 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)
-function playground(opts) {
- var code = $(opts['codeEl']);
-
- // 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);
- }
+// This is a copy of present/js/playground.js from the repository at
+// https://code.google.com/p/go.talks
+// Please make changes to that repository.
- function keyHandler(e) {
- if (e.keyCode == 9) { // tab
- insertTabs(1);
- e.preventDefault();
- return false;
- }
- if (e.keyCode == 13) { // enter
- if (e.shiftKey) { // +shift
- run();
- e.preventDefault();
- return false;
- } else {
- autoindent(e.target);
- }
- }
- return true;
- }
- code.unbind('keydown').bind('keydown', keyHandler);
- var output = $(opts['outputEl']);
+/*
+In the absence of any formal way to specify interfaces in JavaScript,
+here's a skeleton implementation of a playground transport.
- function body() {
- return $(opts['codeEl']).val();
- }
- function setBody(text) {
- $(opts['codeEl']).val(text);
- }
- function origin(href) {
- return (""+href).split("/").slice(0, 3).join("/");
- }
- function loading() {
- output.removeClass("error").html(
- '<div class="loading">Waiting for remote server...</div>'
- );
- }
- var playbackTimeout;
- function playback(pre, events) {
- function show(msg) {
- // ^L clears the screen.
- var msgs = msg.split("\x0c");
- if (msgs.length == 1) {
- pre.text(pre.text() + msg);
- return;
- }
- pre.text(msgs.pop());
- }
+ function Transport() {
+ // Set up any transport state (eg, make a websocket connnection).
+ 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) {
+ }
+*/
+
+function HTTPTransport() {
+ 'use strict';
+
+ // TODO(adg): support stderr
+
+ function playback(output, events) {
+ var timeout;
+ output({Kind: 'start'});
function next() {
- if (events.length == 0) {
- var exit = $('<span class="exit"/>');
- exit.text("\nProgram exited.");
- exit.appendTo(pre);
+ if (events.length === 0) {
+ output({Kind: 'end'});
return;
}
var e = events.shift();
- if (e.Delay == 0) {
- show(e.Message);
+ if (e.Delay === 0) {
+ output({Kind: 'stdout', Body: e.Message});
next();
- } else {
- playbackTimeout = setTimeout(function() {
- show(e.Message);
- next();
- }, e.Delay / 1000000);
+ return;
}
+ timeout = setTimeout(function() {
+ output({Kind: 'stdout', Body: e.Message});
+ next();
+ }, e.Delay / 1000000);
}
next();
- }
- function stopPlayback() {
- clearTimeout(playbackTimeout);
- }
- function setOutput(events, error) {
- stopPlayback();
- output.empty();
- $(".lineerror").removeClass("lineerror");
-
- // Display errors.
- if (error) {
- output.addClass("error");
- var regex = /prog.go:([0-9]+)/g;
- var r;
- while (r = regex.exec(error)) {
- $(".lines div").eq(r[1]-1).addClass("lineerror");
+ return {
+ Stop: function() {
+ clearTimeout(timeout);
}
- $("<pre/>").text(error).appendTo(output);
- return;
}
+ }
- // Display image output.
- if (events.length > 0 && events[0].Message.indexOf("IMAGE:") == 0) {
- var out = "";
- for (var i = 0; i < events.length; i++) {
- out += events[i].Message;
- }
- var url = "data:image/png;base64," + out.substr(6);
- $("<img/>").attr("src", url).appendTo(output);
- return;
- }
+ function error(output, msg) {
+ output({Kind: 'start'});
+ output({Kind: 'stderr', Body: msg});
+ output({Kind: 'end'});
+ }
- // Play back events.
- if (events !== null) {
- var pre = $("<pre/>").appendTo(output);
- playback(pre, events);
+ var seq = 0;
+ return {
+ Run: function(body, output, options) {
+ seq++;
+ var cur = seq;
+ var playing;
+ $.ajax('/compile', {
+ type: 'POST',
+ data: {'version': 2, 'body': body},
+ dataType: 'json',
+ success: function(data) {
+ if (seq != cur) return;
+ if (!data) return;
+ if (playing != null) playing.Stop();
+ if (data.Errors) {
+ error(output, data.Errors);
+ return;
+ }
+ playing = playback(output, data.Events);
+ },
+ 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 = new WebSocket('ws://' + window.location.host + '/socket');
+
+ websocket.onclose = function() {
+ console.log('websocket connection closed');
}
- var pushedEmpty = (window.location.pathname == "/");
- function inputChanged() {
- if (pushedEmpty) {
+ 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;
}
- pushedEmpty = true;
+ output({Kind: m.Kind, Body: m.Body});
+ }
- $(opts['shareURLEl']).hide();
- window.history.pushState(null, "", "/");
+ function send(m) {
+ websocket.send(JSON.stringify(m));
}
- function popState(e) {
- if (e == null) {
- return;
+ 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';
- if (e && e.state && e.state.code) {
- setBody(e.state.code);
+ return function(write) {
+ if (write.Kind == 'start') {
+ el.innerHTML = '';
+ return;
}
- }
- var rewriteHistory = false;
+ var cl = 'system';
+ if (write.Kind == 'stdout' || write.Kind == 'stderr')
+ cl = write.Kind;
- if (window.history &&
- window.history.pushState &&
- window.addEventListener &&
- opts['enableHistory']) {
- rewriteHistory = true;
- code[0].addEventListener('input', inputChanged);
- window.addEventListener('popstate', popState)
- }
+ var m = write.Body;
+ if (write.Kind == 'end')
+ m = '\nProgram exited' + (m?(': '+m):'.');
- var seq = 0;
- function run() {
- loading();
- seq++;
- var cur = seq;
- var data = {
- "version": 2,
- "body": body()
- };
- $.ajax("/compile", {
- data: data,
- type: "POST",
- dataType: "json",
- success: function(data) {
- if (seq != cur) {
- return;
- }
- if (!data) {
- return;
- }
- if (data.Errors) {
- setOutput(null, data.Errors);
- return;
- }
- setOutput(data.Events, false);
- },
- error: function() {
- output.addClass("error").text(
- "Error communicating with remote server."
- );
- }
- });
- }
- $(opts['runEl']).click(run);
-
- $(opts['fmtEl']).click(function() {
- loading();
- $.ajax("/fmt", {
- data: {"body": body()},
- type: "POST",
- dataType: "json",
- success: function(data) {
- if (data.Error) {
- setOutput(null, data.Error);
- return;
- }
- setBody(data.Body);
- setOutput(null);
- }
- });
- });
+ 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;
+ }
- if (opts['shareEl'] != null && (opts['shareURLEl'] != null || opts['shareRedirect'] != null)) {
- var shareURL;
- if (opts['shareURLEl']) {
- shareURL = $(opts['shareURLEl']).hide();
+ // ^L clears the screen.
+ var s = m.split('\x0c');
+ if (s.length > 1) {
+ el.innerHTML = '';
+ m = s.pop();
}
- var sharing = false;
- $(opts['shareEl']).click(function() {
- if (sharing) return;
- sharing = true;
- var sharingData = body();
- $.ajax("/share", {
- processData: false,
- data: sharingData,
- type: "POST",
- complete: function(xhr) {
- sharing = false;
- if (xhr.status != 200) {
- alert("Server error; try again.");
- return;
- }
- if (opts['shareRedirect']) {
- window.location = opts['shareRedirect'] + xhr.responseText;
- }
- if (shareURL) {
- var path = "/p/" + xhr.responseText
- var url = origin(window.location) + path;
- shareURL.show().val(url).focus().select();
-
- if (rewriteHistory) {
- var historyData = {
- "code": sharingData,
- };
- window.history.pushState(historyData, "", path);
- pushedEmpty = false;
- }
- }
- }
- });
- });
- }
- 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);
- }
- });
- });
+ m = m.replace(/&/g, '&');
+ 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)
+ // 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)
+ function playground(opts) {
+ var code = $(opts.codeEl);
+ var transport = opts['transport'] || new HTTPTransport();
+ 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);
+ }
+
+ function keyHandler(e) {
+ if (e.keyCode == 9) { // tab
+ insertTabs(1);
+ e.preventDefault();
+ return false;
+ }
+ if (e.keyCode == 13) { // enter
+ if (e.shiftKey) { // +shift
+ run();
+ e.preventDefault();
+ return false;
+ } else {
+ autoindent(e.target);
+ }
+ }
+ return true;
+ }
+ code.unbind('keydown').bind('keydown', keyHandler);
+ var outdiv = $(opts.outputEl).empty();
+ var output = $('<pre/>').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();
+ $.ajax("/fmt", {
+ data: {"body": body()},
+ type: "POST",
+ dataType: "json",
+ success: function(data) {
+ if (data.Error) {
+ setError(data.Error);
+ } else {
+ setBody(data.Body);
+ setError("");
+ }
+ }
+ });
+ }
+
+ $(opts.runEl).click(run);
+ $(opts.fmtEl).click(fmt);
+
+ if (opts.shareEl !== null && (opts.shareURLEl !== null || opts.shareRedirect !== null)) {
+ var shareURL;
+ if (opts.shareURLEl) {
+ shareURL = $(opts.shareURLEl).hide();
+ }
+ var sharing = false;
+ $(opts.shareEl).click(function() {
+ if (sharing) return;
+ sharing = true;
+ var sharingData = body();
+ $.ajax("/share", {
+ processData: false,
+ data: sharingData,
+ type: "POST",
+ complete: function(xhr) {
+ sharing = false;
+ if (xhr.status != 200) {
+ alert("Server error; try again.");
+ return;
+ }
+ if (opts.shareRedirect) {
+ window.location = opts.shareRedirect + xhr.responseText;
+ }
+ if (shareURL) {
+ var path = "/p/" + xhr.responseText;
+ var url = origin(window.location) + path;
+ shareURL.show().val(url).focus().select();
+
+ if (rewriteHistory) {
+ var historyData = {"code": sharingData};
+ window.history.pushState(historyData, "", path);
+ pushedEmpty = false;
+ }
+ }
+ }
+ });
+ });
+ }
+
+ 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;
+})();