| // 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?backend=' + (options.backend || ''), { |
| 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; |
| } |
| 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) { |
| // Without the n > 0 check, Safari cannot type a blank line at the bottom of a playground snippet. |
| // See go.dev/issue/49794. |
| if (n > 0) { |
| document.execCommand('insertText', false, '\t'.repeat(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 pushedPlay = window.location.pathname == '/play/'; |
| function inputChanged() { |
| if (pushedPlay) { |
| return; |
| } |
| pushedPlay = true; |
| $(opts.shareURLEl).hide(); |
| $(opts.toysEl).show(); |
| var path = window.location.pathname; |
| var i = path.indexOf('/play/'); |
| var p = path.substr(0, i+6); |
| if (opts.versionEl !== null) { |
| var v = $(opts.versionEl).val(); |
| if (v != '') { |
| p += '?v=' + v; |
| } |
| } |
| window.history.pushState(null, '', p); |
| } |
| 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 backend() { |
| if (!opts.versionEl) { |
| return ''; |
| } |
| var vers = $(opts.versionEl); |
| if (!vers) { |
| return ''; |
| } |
| return vers.val(); |
| } |
| |
| 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 runOnly() { |
| loading(); |
| running = transport.Run( |
| body(), |
| highlightOutput(PlaygroundOutput(output[0])), |
| {backend: backend()}, |
| ); |
| } |
| |
| function fmtAnd(run) { |
| loading(); |
| var data = { body: body() }; |
| data['imports'] = 'true'; |
| $.ajax('/_/fmt?backend='+backend(), { |
| data: data, |
| type: 'POST', |
| dataType: 'json', |
| success: function(data) { |
| if (data.Error) { |
| setError(data.Error); |
| } else { |
| setBody(data.Body); |
| setError(''); |
| } |
| run(); |
| }, |
| error: function() { |
| setError('Error communicating with remote server.'); |
| }, |
| }); |
| } |
| |
| function loadShare(id) { |
| $.ajax('/_/share?id='+id, { |
| processData: false, |
| type: 'GET', |
| complete: function(xhr) { |
| if(xhr.status != 200) { |
| setBody('Cannot load shared snippet; try again.'); |
| return; |
| } |
| setBody(xhr.responseText); |
| }, |
| }) |
| } |
| |
| function fmt() { |
| fmtAnd(function(){}); |
| } |
| |
| function run() { |
| fmtAnd(runOnly); |
| } |
| |
| 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 errorMessages = { |
| 413: 'Snippet is too large to share.' |
| }; |
| |
| 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) { |
| var alertMsg = errorMessages[xhr.status] ? errorMessages[xhr.status] : 'Server error; try again.'; |
| alert(alertMsg); |
| return; |
| } |
| if (opts.shareRedirect) { |
| window.location = opts.shareRedirect + xhr.responseText; |
| } |
| var path = '/play/p/' + xhr.responseText; |
| if (opts.versionEl !== null && $(opts.versionEl).val() != "") { |
| path += "?v=" + $(opts.versionEl).val(); |
| } |
| 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(); |
| |
| $(opts.toysEl).hide(); |
| if (rewriteHistory) { |
| var historyData = { code: sharingData }; |
| window.history.pushState(historyData, '', path); |
| pushedPlay = 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(); |
| }); |
| } |
| |
| var path = window.location.pathname; |
| var toyDisable = false; |
| if (path.startsWith('/go.dev/')) { |
| path = path.slice(7); |
| } |
| if (path.startsWith('/play/p/')) { |
| var id = path.slice(8); |
| id = id.replace(/\.go$/, ""); |
| loadShare(id); |
| toyDisable = true; |
| } |
| |
| if (opts.toysEl !== null) { |
| $(opts.toysEl).bind('change', function() { |
| if (toyDisable) { |
| toyDisable = false; |
| return; |
| } |
| 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); |
| if (toy.includes('-dev') && opts.versionEl !== null) { |
| $(opts.versionEl).val('gotip'); |
| } |
| run(); |
| }, |
| }); |
| }); |
| } |
| |
| if (opts.versionEl !== null) { |
| var select = $(opts.versionEl); |
| var v = (new URL(window.location)).searchParams.get('v'); |
| if (v !== null && v != "") { |
| select.val(v); |
| if (select.val() != v) { |
| select.append($('<option>', {value: v, text: 'Backend: ' + v})); |
| select.val(v); |
| } |
| } |
| if (opts.enableHistory) { |
| select.bind('change', inputChanged); |
| } |
| } |
| } |
| |
| window.playground = playground; |
| })(); |