| // 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, '<'); |
| 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 = $('<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(); |
| 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; |
| })(); |