| // 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('https://play.golang.org/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; | |
| })(); |