go.talks: refactor Playground JavaScript API
These changes will be propagated to the go-tour, go-playground, and go core.
R=dsymonds
CC=golang-dev
https://golang.org/cl/10868045
diff --git a/present/appengine.go b/present/appengine.go
index 98b412c..ae93a5a 100644
--- a/present/appengine.go
+++ b/present/appengine.go
@@ -14,6 +14,6 @@
var basePath = "./present/"
func init() {
- playScript(basePath, "playground.js")
+ playScript(basePath, "HTTPTransport")
present.PlayEnabled = true
}
diff --git a/present/js/play.js b/present/js/play.js
index a30ca74..fc15ce5 100644
--- a/present/js/play.js
+++ b/present/js/play.js
@@ -2,115 +2,97 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-(function() {
- "use strict";
+function initPlayground(transport) {
+ "use strict";
- var runFunc;
- var count = 0;
+ function text(node) {
+ var s = "";
+ for (var i = 0; i < node.childNodes.length; i++) {
+ var n = node.childNodes[i];
+ if (n.nodeType === 1 && n.tagName === "SPAN" && n.className != "number") {
+ var innerText = n.innerText === undefined ? "textContent" : "innerText";
+ s += n[innerText] + "\n";
+ continue;
+ }
+ if (n.nodeType === 1 && n.tagName !== "BUTTON") {
+ s += text(n);
+ }
+ }
+ return s;
+ }
- function getId() {
- return "code" + (count++);
- }
+ function init(code) {
+ var output = document.createElement('div');
+ var outpre = document.createElement('pre');
+ var running;
- function text(node) {
- var s = "";
- for (var i = 0; i < node.childNodes.length; i++) {
- var n = node.childNodes[i];
- if (n.nodeType === 1 && n.tagName === "SPAN" && n.className != "number") {
- var innerText = n.innerText === undefined ? "textContent" : "innerText";
- s += n[innerText] + "\n";
- continue;
- }
- if (n.nodeType === 1 && n.tagName !== "BUTTON") {
- s += text(n);
- }
- }
- return s;
- }
+ // TODO(adg): check that jquery etc is loaded.
+ $(output).resizable({
+ handles: "n,w,nw",
+ minHeight: 27,
+ minWidth: 135,
+ maxHeight: 608,
+ maxWidth: 990
+ });
- function init(code) {
- var id = getId();
+ function onKill() {
+ if (running) running.Kill();
+ }
- var output = document.createElement('div');
- var outpre = document.createElement('pre');
- var stopFunc;
+ function onRun(e) {
+ onKill();
+ output.style.display = "block";
+ outpre.innerHTML = "";
+ run1.style.display = "none";
+ var options = {Race: e.shiftKey};
+ running = transport.Run(text(code), PlaygroundOutput(outpre), options);
+ }
- $(output).resizable({
- handles: "n,w,nw",
- minHeight: 27,
- minWidth: 135,
- maxHeight: 608,
- maxWidth: 990
- });
+ function onClose() {
+ onKill();
+ output.style.display = "none";
+ run1.style.display = "inline-block";
+ }
- function onKill() {
- if (stopFunc) {
- stopFunc();
- }
- }
+ var run1 = document.createElement('button');
+ run1.innerHTML = 'Run';
+ run1.className = 'run';
+ run1.addEventListener("click", onRun, false);
+ var run2 = document.createElement('button');
+ run2.className = 'run';
+ run2.innerHTML = 'Run';
+ run2.addEventListener("click", onRun, false);
+ var kill = document.createElement('button');
+ kill.className = 'kill';
+ kill.innerHTML = 'Kill';
+ kill.addEventListener("click", onKill, false);
+ var close = document.createElement('button');
+ close.className = 'close';
+ close.innerHTML = 'Close';
+ close.addEventListener("click", onClose, false);
- function onRun(e) {
- onKill();
- outpre.innerHTML = "";
- output.style.display = "block";
- run.style.display = "none";
- var options = {Race: e.shiftKey};
- stopFunc = runFunc(text(code), outpre, options);
- }
+ var button = document.createElement('div');
+ button.classList.add('buttons');
+ button.appendChild(run1);
+ // Hack to simulate insertAfter
+ code.parentNode.insertBefore(button, code.nextSibling);
- function onClose() {
- onKill();
- output.style.display = "none";
- run.style.display = "inline-block";
- }
+ var buttons = document.createElement('div');
+ buttons.classList.add('buttons');
+ buttons.appendChild(run2);
+ buttons.appendChild(kill);
+ buttons.appendChild(close);
- var run = document.createElement('button');
- run.innerHTML = 'Run';
- run.className = 'run';
- run.addEventListener("click", onRun, false);
- var run2 = document.createElement('button');
- run2.className = 'run';
- run2.innerHTML = 'Run';
- run2.addEventListener("click", onRun, false);
- var kill = document.createElement('button');
- kill.className = 'kill';
- kill.innerHTML = 'Kill';
- kill.addEventListener("click", onKill, false);
- var close = document.createElement('button');
- close.className = 'close';
- close.innerHTML = 'Close';
- close.addEventListener("click", onClose, false);
+ output.classList.add('output');
+ output.appendChild(buttons);
+ output.appendChild(outpre);
+ output.style.display = "none";
+ code.parentNode.insertBefore(output, button.nextSibling);
+ }
- var button = document.createElement('div');
- button.classList.add('buttons');
- button.appendChild(run);
- // Hack to simulate insertAfter
- code.parentNode.insertBefore(button, code.nextSibling);
+ var play = document.querySelectorAll('div.playground');
+ for (var i = 0; i < play.length; i++) {
+ init(play[i]);
+ }
+}
- var buttons = document.createElement('div');
- buttons.classList.add('buttons');
- buttons.appendChild(run2);
- buttons.appendChild(kill);
- buttons.appendChild(close);
-
- output.classList.add('output');
- output.appendChild(buttons);
- output.appendChild(outpre);
- output.style.display = "none";
- code.parentNode.insertBefore(output, button.nextSibling);
- }
-
- var play = document.querySelectorAll('div.playground');
- for (var i = 0; i < play.length; i++) {
- init(play[i]);
- }
- if (play.length > 0) {
- if (window.connectPlayground) {
- runFunc = window.connectPlayground("ws://" + window.location.host + "/socket");
- } else {
- // If this message is logged,
- // we have neglected to include socket.js or playground.js.
- console.log("No playground transport available.");
- }
- }
-})();
diff --git a/present/js/playground.js b/present/js/playground.js
index 67961c2..d2be24c 100644
--- a/present/js/playground.js
+++ b/present/js/playground.js
@@ -2,9 +2,210 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-(function() {
+/*
+In the absence of any formal way to specify interfaces in JavaScript,
+here's a skeleton implementation of a playground transport.
- // TODO(adg): make these functions operate only on a specific code div
+ 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) {
+ // Append writes to
+ }
+*/
+
+function HTTPTransport() {
+ 'use strict';
+
+ // TODO(adg): support stderr
+
+ function playback(output, events) {
+ var timeout;
+ output({Kind: 'start'});
+ function next() {
+ if (events.length === 0) {
+ output({Kind: 'end'});
+ return;
+ }
+ var e = events.shift();
+ if (e.Delay === 0) {
+ output({Kind: 'stdout', Body: e.Message});
+ next();
+ return;
+ }
+ timeout = setTimeout(function() {
+ output({Kind: 'stdout', 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'});
+ }
+
+ 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');
+ }
+
+ 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, '<');
+ 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);
@@ -13,115 +214,16 @@
r = regex.exec(error);
}
}
+ function highlightOutput(wrappedOutput) {
+ return function(write) {
+ if (write.Body) lineHighlight(write.Body);
+ wrappedOutput(write);
+ }
+ }
function lineClear() {
$(".lineerror").removeClass("lineerror");
}
- function connectPlayground() {
- 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 next() {
- if (events.length === 0) {
- var exit = $('<span class="exit"/>');
- exit.text("\nProgram exited.");
- exit.appendTo(pre);
- return;
- }
- var e = events.shift();
- if (e.Delay === 0) {
- show(e.Message);
- next();
- } else {
- playbackTimeout = setTimeout(function() {
- show(e.Message);
- next();
- }, e.Delay / 1000000);
- }
- }
- next();
- }
-
- function stopPlayback() {
- clearTimeout(playbackTimeout);
- }
-
- function setOutput(output, events, error) {
- stopPlayback();
- output.empty();
- lineClear();
-
- // Display errors.
- if (error) {
- lineHighlight(error);
- output.addClass("error").text(error);
- 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;
- }
-
- // Play back events.
- if (events !== null) {
- playback(output, events);
- }
- }
-
- var seq = 0;
- function runFunc(body, output) {
- output = $(output);
- 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(output, null, data.Errors);
- return;
- }
- setOutput(output, data.Events, false);
- },
- error: function() {
- output.addClass("error").text(
- "Error communicating with remote server."
- );
- }
- });
- return stopPlayback;
- }
-
- return runFunc;
- }
-
// opts is an object with these keys
// codeEl - code editor element
// outputEl - program output element
@@ -132,10 +234,11 @@
// 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 runFunc = connectPlayground();
- var stopFunc;
+ var transport = opts['transport'] || new HTTPTransport();
+ var running;
// autoindent helpers.
function insertTabs(n) {
@@ -227,18 +330,19 @@
}
function setError(error) {
- if (stopFunc) stopFunc();
+ if (running) running.Kill();
lineClear();
lineHighlight(error);
output.empty().addClass("error").text(error);
}
function loading() {
- if (stopFunc) stopFunc();
+ lineClear();
+ if (running) running.Kill();
output.removeClass("error").text('Waiting for remote server...');
}
function run() {
loading();
- stopFunc = runFunc(body(), output);
+ running = transport.Run(body(), highlightOutput(PlaygroundOutput(output[0])));
}
function fmt() {
loading();
@@ -317,7 +421,5 @@
}
}
- window.connectPlayground = connectPlayground;
window.playground = playground;
-
})();
diff --git a/present/js/socket.js b/present/js/socket.js
deleted file mode 100644
index 6e29c05..0000000
--- a/present/js/socket.js
+++ /dev/null
@@ -1,75 +0,0 @@
-// 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.
-
-(function() {
- "use strict";
-
- var websocket, outputs = {};
-
- function onClose() {
- console.log('websocket connection closed');
- }
-
- function sendMessage(m) {
- websocket.send(JSON.stringify(m));
- }
-
- function onMessage(e) {
- var m = JSON.parse(e.data);
- var o = outputs[m.Id];
- if (o === null) {
- return;
- }
- if (m.Kind === "stdout" || m.Kind === "stderr") {
- showMessage(o, m.Body, m.Kind);
- }
- if (m.Kind === "end") {
- var s = "Program exited";
- if (m.Body !== "") {
- s += ": " + m.Body;
- } else {
- s += ".";
- }
- s += "\n";
- showMessage(o, s, "system");
- }
- }
-
- function showMessage(o, m, className) {
- var span = document.createElement("span");
- span.className = className;
- if (m.indexOf("IMAGE:") === 0) {
- var url = "data:image/png;base64," + m.substr(6);
- var img = document.createElement("img");
- img.src = url;
- span.appendChild(img);
- } else {
- m = m.replace(/&/g, "&");
- m = m.replace(/</g, "<");
- span.innerHTML = m;
- }
- var needScroll = (o.scrollTop + o.offsetHeight) == o.scrollHeight;
- o.appendChild(span);
- if (needScroll)
- o.scrollTop = o.scrollHeight - o.offsetHeight;
- }
-
- function run(body, output, options) {
- var id = output.id;
- outputs[id] = output;
- options = options || {};
- options.Race = !!options.Race; // force boolean
- sendMessage({Id: id, Kind: "run", Body: body, Options: options});
- return function() {
- sendMessage({Id: id, Kind: "kill"});
- };
- }
-
- window.connectPlayground = function(addr) {
- websocket = new WebSocket(addr);
- websocket.onmessage = onMessage;
- websocket.onclose = onClose;
- return run;
- };
-})();
diff --git a/present/local.go b/present/local.go
index c800f78..5478ef0 100644
--- a/present/local.go
+++ b/present/local.go
@@ -40,7 +40,7 @@
}
if present.PlayEnabled {
- playScript(basePath, "socket.js")
+ playScript(basePath, "SocketTransport")
http.Handle("/socket", socket.Handler)
}
http.Handle("/static/", http.FileServer(http.Dir(basePath)))
diff --git a/present/play.go b/present/play.go
index f7a8a3e..268919f 100644
--- a/present/play.go
+++ b/present/play.go
@@ -6,24 +6,29 @@
import (
"bytes"
+ "fmt"
"io/ioutil"
"net/http"
"path/filepath"
"time"
)
-// playScript registers an HTTP handler at /play.js that contains all the
-// scripts specified by path, relative to basePath.
-func playScript(root string, path ...string) {
+var scripts = []string{"jquery.js", "jquery-ui.js", "playground.js", "play.js"}
+
+// playScript registers an HTTP handler at /play.js that serves all the
+// scripts specified by the variable above, and appends a line that
+// initializes the playground with the specified transport.
+func playScript(root, transport string) {
modTime := time.Now()
var buf bytes.Buffer
- for _, p := range append(path, "jquery.js", "jquery-ui.js", "play.js") {
+ for _, p := range scripts {
b, err := ioutil.ReadFile(filepath.Join(root, "js", p))
if err != nil {
panic(err)
}
buf.Write(b)
}
+ fmt.Fprintf(&buf, "\ninitPlayground(new %v());\n", transport)
b := buf.Bytes()
http.HandleFunc("/play.js", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "application/javascript")