1 // Copyright 2012 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
6 In the absence of any formal way to specify interfaces in JavaScript,
7 here's a skeleton implementation of a playground transport.
10 // Set up any transport state (eg, make a websocket connection).
12 Run: function(body, output, options) {
13 // Compile and run the program 'body' with 'options'.
14 // Call the 'output' callback to display program output.
17 // Kill the running program.
24 // The output callback is called multiple times, and each time it is
25 // passed an object of this form.
27 Kind: 'string', // 'start', 'stdout', 'stderr', 'end'
28 Body: 'string' // content of write or end status message
31 // The first call must be of Kind 'start' with no body.
32 // Subsequent calls may be of Kind 'stdout' or 'stderr'
33 // and must have a non-null Body string.
34 // The final call should be of Kind 'end' with an optional
35 // Body string, signifying a failure ("killed", for example).
37 // The output callback must be of this form.
38 // See PlaygroundOutput (below) for an implementation.
39 function outputCallback(write) {
43 // HTTPTransport is the default transport.
44 // enableVet enables running vet if a program was compiled and ran successfully.
45 // If vet returned any errors, display them before the output of a program.
46 function HTTPTransport(enableVet) {
49 function playback(output, data) {
50 // Backwards compatibility: default values do not affect the output.
51 var events = data.Events || [];
52 var errors = data.Errors || '';
53 var status = data.Status || 0;
54 var isTest = data.IsTest || false;
55 var testsFailed = data.TestsFailed || 0;
58 output({ Kind: 'start' });
60 if (!events || events.length === 0) {
62 if (testsFailed > 0) {
69 (testsFailed > 1 ? 's' : '') +
73 output({ Kind: 'system', Body: '\nAll tests passed.' });
77 output({ Kind: 'end', Body: 'status ' + status + '.' });
80 // errors are displayed only in the case of timeout.
81 output({ Kind: 'end', Body: errors + '.' });
83 output({ Kind: 'end' });
89 var e = events.shift();
91 output({ Kind: e.Kind, Body: e.Message });
95 timeout = setTimeout(function() {
96 output({ Kind: e.Kind, Body: e.Message });
98 }, e.Delay / 1000000);
103 clearTimeout(timeout);
108 function error(output, msg) {
109 output({ Kind: 'start' });
110 output({ Kind: 'stderr', Body: msg });
111 output({ Kind: 'end' });
114 function buildFailed(output, msg) {
115 output({ Kind: 'start' });
116 output({ Kind: 'stderr', Body: msg });
117 output({ Kind: 'system', Body: '\nGo build failed.' });
122 Run: function(body, output, options) {
128 data: { version: 2, body: body, withVet: enableVet },
130 success: function(data) {
131 if (seq != cur) return;
133 if (playing != null) playing.Stop();
135 if (data.Errors === 'process took too long') {
136 // Playback the output that was captured before the timeout.
137 playing = playback(output, data);
139 buildFailed(output, data.Errors);
146 if (data.VetErrors) {
147 // Inject errors from the vet as the first events in the output.
148 data.Events.unshift({
149 Message: 'Go vet exited.\n\n',
153 data.Events.unshift({
154 Message: data.VetErrors,
160 if (!enableVet || data.VetOK || data.VetErrors) {
161 playing = playback(output, data);
165 // In case the server support doesn't support
166 // compile+vet in same request signaled by the
167 // 'withVet' parameter above, also try the old way.
168 // TODO: remove this when it falls out of use.
169 // It is 2019-05-13 now.
171 data: { body: body },
174 success: function(dataVet) {
175 if (dataVet.Errors) {
176 // inject errors from the vet as the first events in the output
177 data.Events.unshift({
178 Message: 'Go vet exited.\n\n',
182 data.Events.unshift({
183 Message: dataVet.Errors,
188 playing = playback(output, data);
191 playing = playback(output, data);
196 error(output, 'Error communicating with remote server.');
201 if (playing != null) playing.Stop();
202 output({ Kind: 'end', Body: 'killed' });
209 function SocketTransport() {
216 if (window.location.protocol == 'http:') {
217 websocket = new WebSocket('ws://' + window.location.host + '/socket');
218 } else if (window.location.protocol == 'https:') {
219 websocket = new WebSocket('wss://' + window.location.host + '/socket');
222 websocket.onclose = function() {
223 console.log('websocket connection closed');
226 websocket.onmessage = function(e) {
227 var m = JSON.parse(e.data);
228 var output = outputs[m.Id];
229 if (output === null) return;
230 if (!started[m.Id]) {
231 output({ Kind: 'start' });
232 started[m.Id] = true;
234 output({ Kind: m.Kind, Body: m.Body });
238 websocket.send(JSON.stringify(m));
242 Run: function(body, output, options) {
243 var thisID = id + '';
245 outputs[thisID] = output;
246 send({ Id: thisID, Kind: 'run', Body: body, Options: options });
249 send({ Id: thisID, Kind: 'kill' });
256 function PlaygroundOutput(el) {
259 return function(write) {
260 if (write.Kind == 'start') {
266 if (write.Kind == 'stdout' || write.Kind == 'stderr') cl = write.Kind;
269 if (write.Kind == 'end') {
270 m = '\nProgram exited' + (m ? ': ' + m : '.');
273 if (m.indexOf('IMAGE:') === 0) {
274 // TODO(adg): buffer all writes before creating image
275 var url = 'data:image/png;base64,' + m.substr(6);
276 var img = document.createElement('img');
282 // ^L clears the screen.
283 var s = m.split('\x0c');
289 m = m.replace(/&/g, '&');
290 m = m.replace(/</g, '<');
291 m = m.replace(/>/g, '>');
293 var needScroll = el.scrollTop + el.offsetHeight == el.scrollHeight;
295 var span = document.createElement('span');
298 el.appendChild(span);
300 if (needScroll) el.scrollTop = el.scrollHeight - el.offsetHeight;
305 function lineHighlight(error) {
306 var regex = /prog.go:([0-9]+)/g;
307 var r = regex.exec(error);
311 .addClass('lineerror');
312 r = regex.exec(error);
315 function highlightOutput(wrappedOutput) {
316 return function(write) {
317 if (write.Body) lineHighlight(write.Body);
318 wrappedOutput(write);
321 function lineClear() {
322 $('.lineerror').removeClass('lineerror');
325 // opts is an object with these keys
326 // codeEl - code editor element
327 // outputEl - program output element
328 // runEl - run button element
329 // fmtEl - fmt button element (optional)
330 // fmtImportEl - fmt "imports" checkbox element (optional)
331 // shareEl - share button element (optional)
332 // shareURLEl - share URL text input element (optional)
333 // shareRedirect - base URL to redirect to on share (optional)
334 // toysEl - toys select element (optional)
335 // enableHistory - enable using HTML5 history API (optional)
336 // transport - playground transport to use (default is HTTPTransport)
337 // enableShortcuts - whether to enable shortcuts (Ctrl+S/Cmd+S to save) (default is false)
338 // enableVet - enable running vet and displaying its errors
339 function playground(opts) {
340 var code = $(opts.codeEl);
341 var transport = opts['transport'] || new HTTPTransport(opts['enableVet']);
344 // autoindent helpers.
345 function insertTabs(n) {
346 // find the selection start and end
347 var start = code[0].selectionStart;
348 var end = code[0].selectionEnd;
349 // split the textarea content into two, and insert n tabs
350 var v = code[0].value;
351 var u = v.substr(0, start);
352 for (var i = 0; i < n; i++) {
356 // set revised content
358 // reset caret position after inserted tabs
359 code[0].selectionStart = start + n;
360 code[0].selectionEnd = start + n;
362 function autoindent(el) {
363 var curpos = el.selectionStart;
367 if (el.value[curpos] == '\t') {
369 } else if (tabs > 0 || el.value[curpos] == '\n') {
373 setTimeout(function() {
378 // NOTE(cbro): e is a jQuery event, not a DOM event.
379 function handleSaveShortcut(e) {
380 if (e.isDefaultPrevented()) return false;
381 if (!e.metaKey && !e.ctrlKey) return false;
382 if (e.key != 'S' && e.key != 's') return false;
387 share(function(url) {
388 window.location.href = url + '.go?download=true';
394 function keyHandler(e) {
395 if (opts.enableShortcuts && handleSaveShortcut(e)) return;
397 if (e.keyCode == 9 && !e.ctrlKey) {
398 // tab (but not ctrl-tab)
403 if (e.keyCode == 13) {
416 autoindent(e.target);
421 code.unbind('keydown').bind('keydown', keyHandler);
422 var outdiv = $(opts.outputEl).empty();
423 var output = $('<pre/>').appendTo(outdiv);
426 return $(opts.codeEl).val();
428 function setBody(text) {
429 $(opts.codeEl).val(text);
431 function origin(href) {
438 var pushedEmpty = window.location.pathname == '/';
439 function inputChanged() {
444 $(opts.shareURLEl).hide();
445 window.history.pushState(null, '', '/');
447 function popState(e) {
451 if (e && e.state && e.state.code) {
452 setBody(e.state.code);
455 var rewriteHistory = false;
458 window.history.pushState &&
459 window.addEventListener &&
462 rewriteHistory = true;
463 code[0].addEventListener('input', inputChanged);
464 window.addEventListener('popstate', popState);
467 function setError(error) {
468 if (running) running.Kill();
470 lineHighlight(error);
478 if (running) running.Kill();
479 output.removeClass('error').text('Waiting for remote server...');
483 running = transport.Run(
485 highlightOutput(PlaygroundOutput(output[0]))
491 var data = { body: body() };
492 if ($(opts.fmtImportEl).is(':checked')) {
493 data['imports'] = 'true';
499 success: function(data) {
501 setError(data.Error);
510 var shareURL; // jQuery element to show the shared URL.
511 var sharing = false; // true if there is a pending request.
512 var shareCallbacks = [];
513 function share(opt_callback) {
514 if (opt_callback) shareCallbacks.push(opt_callback);
519 var sharingData = body();
524 contentType: 'text/plain; charset=utf-8',
525 complete: function(xhr) {
527 if (xhr.status != 200) {
528 alert('Server error; try again.');
531 if (opts.shareRedirect) {
532 window.location = opts.shareRedirect + xhr.responseText;
534 var path = '/p/' + xhr.responseText;
535 var url = origin(window.location) + path;
537 for (var i = 0; i < shareCallbacks.length; i++) {
538 shareCallbacks[i](url);
549 if (rewriteHistory) {
550 var historyData = { code: sharingData };
551 window.history.pushState(historyData, '', path);
559 $(opts.runEl).click(run);
560 $(opts.fmtEl).click(fmt);
563 opts.shareEl !== null &&
564 (opts.shareURLEl !== null || opts.shareRedirect !== null)
566 if (opts.shareURLEl) {
567 shareURL = $(opts.shareURLEl).hide();
569 $(opts.shareEl).click(function() {
574 if (opts.toysEl !== null) {
575 $(opts.toysEl).bind('change', function() {
576 var toy = $(this).val();
577 $.ajax('/doc/play/' + toy, {
580 complete: function(xhr) {
581 if (xhr.status != 200) {
582 alert('Server error; try again.');
585 setBody(xhr.responseText);
592 window.playground = playground;