From 1856286fc23ec4332b7ccc46d9838fbb96fd4ce0 Mon Sep 17 00:00:00 2001 From: Andrew Gerrand Date: Thu, 4 Jul 2013 14:24:21 +1000 Subject: [PATCH] doc: update playground.js R=dsymonds CC=golang-dev https://golang.org/cl/10933044 --- doc/play/playground.js | 650 +++++++++++++++++++++++++---------------- doc/style.css | 5 +- 2 files changed, 395 insertions(+), 260 deletions(-) diff --git a/doc/play/playground.js b/doc/play/playground.js index 709136627b..849015d1db 100644 --- a/doc/play/playground.js +++ b/doc/play/playground.js @@ -2,295 +2,427 @@ // 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 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( - '
Waiting for remote server...
' - ); - } - 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 = $(''); - 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); } - $("
").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);
-			$("").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 = $("
").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, '>');
+
+		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 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 = $('
').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;
+})();
diff --git a/doc/style.css b/doc/style.css
index ca8006b8dc..37b0e32c22 100644
--- a/doc/style.css
+++ b/doc/style.css
@@ -497,7 +497,10 @@ div.play .buttons a {
 	padding: 10px;
 	cursor: pointer;
 }
-div.play .output .exit {
+.output .stderr {
+	color: #933;
+}
+.output .system {
 	color: #999;
 }
 
-- 
2.48.1