github.com/april1989/origin-go-tools@v0.0.32/godoc/static/playground.js (about) 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. 4 5 /* 6 In the absence of any formal way to specify interfaces in JavaScript, 7 here's a skeleton implementation of a playground transport. 8 9 function Transport() { 10 // Set up any transport state (eg, make a websocket connection). 11 return { 12 Run: function(body, output, options) { 13 // Compile and run the program 'body' with 'options'. 14 // Call the 'output' callback to display program output. 15 return { 16 Kill: function() { 17 // Kill the running program. 18 } 19 }; 20 } 21 }; 22 } 23 24 // The output callback is called multiple times, and each time it is 25 // passed an object of this form. 26 var write = { 27 Kind: 'string', // 'start', 'stdout', 'stderr', 'end' 28 Body: 'string' // content of write or end status message 29 } 30 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). 36 37 // The output callback must be of this form. 38 // See PlaygroundOutput (below) for an implementation. 39 function outputCallback(write) { 40 } 41 */ 42 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) { 47 'use strict'; 48 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; 56 57 var timeout; 58 output({ Kind: 'start' }); 59 function next() { 60 if (!events || events.length === 0) { 61 if (isTest) { 62 if (testsFailed > 0) { 63 output({ 64 Kind: 'system', 65 Body: 66 '\n' + 67 testsFailed + 68 ' test' + 69 (testsFailed > 1 ? 's' : '') + 70 ' failed.', 71 }); 72 } else { 73 output({ Kind: 'system', Body: '\nAll tests passed.' }); 74 } 75 } else { 76 if (status > 0) { 77 output({ Kind: 'end', Body: 'status ' + status + '.' }); 78 } else { 79 if (errors !== '') { 80 // errors are displayed only in the case of timeout. 81 output({ Kind: 'end', Body: errors + '.' }); 82 } else { 83 output({ Kind: 'end' }); 84 } 85 } 86 } 87 return; 88 } 89 var e = events.shift(); 90 if (e.Delay === 0) { 91 output({ Kind: e.Kind, Body: e.Message }); 92 next(); 93 return; 94 } 95 timeout = setTimeout(function() { 96 output({ Kind: e.Kind, Body: e.Message }); 97 next(); 98 }, e.Delay / 1000000); 99 } 100 next(); 101 return { 102 Stop: function() { 103 clearTimeout(timeout); 104 }, 105 }; 106 } 107 108 function error(output, msg) { 109 output({ Kind: 'start' }); 110 output({ Kind: 'stderr', Body: msg }); 111 output({ Kind: 'end' }); 112 } 113 114 function buildFailed(output, msg) { 115 output({ Kind: 'start' }); 116 output({ Kind: 'stderr', Body: msg }); 117 output({ Kind: 'system', Body: '\nGo build failed.' }); 118 } 119 120 var seq = 0; 121 return { 122 Run: function(body, output, options) { 123 seq++; 124 var cur = seq; 125 var playing; 126 $.ajax('/compile', { 127 type: 'POST', 128 data: { version: 2, body: body, withVet: enableVet }, 129 dataType: 'json', 130 success: function(data) { 131 if (seq != cur) return; 132 if (!data) return; 133 if (playing != null) playing.Stop(); 134 if (data.Errors) { 135 if (data.Errors === 'process took too long') { 136 // Playback the output that was captured before the timeout. 137 playing = playback(output, data); 138 } else { 139 buildFailed(output, data.Errors); 140 } 141 return; 142 } 143 if (!data.Events) { 144 data.Events = []; 145 } 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', 150 Kind: 'system', 151 Delay: 0, 152 }); 153 data.Events.unshift({ 154 Message: data.VetErrors, 155 Kind: 'stderr', 156 Delay: 0, 157 }); 158 } 159 160 if (!enableVet || data.VetOK || data.VetErrors) { 161 playing = playback(output, data); 162 return; 163 } 164 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. 170 $.ajax('/vet', { 171 data: { body: body }, 172 type: 'POST', 173 dataType: 'json', 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', 179 Kind: 'system', 180 Delay: 0, 181 }); 182 data.Events.unshift({ 183 Message: dataVet.Errors, 184 Kind: 'stderr', 185 Delay: 0, 186 }); 187 } 188 playing = playback(output, data); 189 }, 190 error: function() { 191 playing = playback(output, data); 192 }, 193 }); 194 }, 195 error: function() { 196 error(output, 'Error communicating with remote server.'); 197 }, 198 }); 199 return { 200 Kill: function() { 201 if (playing != null) playing.Stop(); 202 output({ Kind: 'end', Body: 'killed' }); 203 }, 204 }; 205 }, 206 }; 207 } 208 209 function SocketTransport() { 210 'use strict'; 211 212 var id = 0; 213 var outputs = {}; 214 var started = {}; 215 var websocket; 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'); 220 } 221 222 websocket.onclose = function() { 223 console.log('websocket connection closed'); 224 }; 225 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; 233 } 234 output({ Kind: m.Kind, Body: m.Body }); 235 }; 236 237 function send(m) { 238 websocket.send(JSON.stringify(m)); 239 } 240 241 return { 242 Run: function(body, output, options) { 243 var thisID = id + ''; 244 id++; 245 outputs[thisID] = output; 246 send({ Id: thisID, Kind: 'run', Body: body, Options: options }); 247 return { 248 Kill: function() { 249 send({ Id: thisID, Kind: 'kill' }); 250 }, 251 }; 252 }, 253 }; 254 } 255 256 function PlaygroundOutput(el) { 257 'use strict'; 258 259 return function(write) { 260 if (write.Kind == 'start') { 261 el.innerHTML = ''; 262 return; 263 } 264 265 var cl = 'system'; 266 if (write.Kind == 'stdout' || write.Kind == 'stderr') cl = write.Kind; 267 268 var m = write.Body; 269 if (write.Kind == 'end') { 270 m = '\nProgram exited' + (m ? ': ' + m : '.'); 271 } 272 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'); 277 img.src = url; 278 el.appendChild(img); 279 return; 280 } 281 282 // ^L clears the screen. 283 var s = m.split('\x0c'); 284 if (s.length > 1) { 285 el.innerHTML = ''; 286 m = s.pop(); 287 } 288 289 m = m.replace(/&/g, '&'); 290 m = m.replace(/</g, '<'); 291 m = m.replace(/>/g, '>'); 292 293 var needScroll = el.scrollTop + el.offsetHeight == el.scrollHeight; 294 295 var span = document.createElement('span'); 296 span.className = cl; 297 span.innerHTML = m; 298 el.appendChild(span); 299 300 if (needScroll) el.scrollTop = el.scrollHeight - el.offsetHeight; 301 }; 302 } 303 304 (function() { 305 function lineHighlight(error) { 306 var regex = /prog.go:([0-9]+)/g; 307 var r = regex.exec(error); 308 while (r) { 309 $('.lines div') 310 .eq(r[1] - 1) 311 .addClass('lineerror'); 312 r = regex.exec(error); 313 } 314 } 315 function highlightOutput(wrappedOutput) { 316 return function(write) { 317 if (write.Body) lineHighlight(write.Body); 318 wrappedOutput(write); 319 }; 320 } 321 function lineClear() { 322 $('.lineerror').removeClass('lineerror'); 323 } 324 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']); 342 var running; 343 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++) { 353 u += '\t'; 354 } 355 u += v.substr(end); 356 // set revised content 357 code[0].value = u; 358 // reset caret position after inserted tabs 359 code[0].selectionStart = start + n; 360 code[0].selectionEnd = start + n; 361 } 362 function autoindent(el) { 363 var curpos = el.selectionStart; 364 var tabs = 0; 365 while (curpos > 0) { 366 curpos--; 367 if (el.value[curpos] == '\t') { 368 tabs++; 369 } else if (tabs > 0 || el.value[curpos] == '\n') { 370 break; 371 } 372 } 373 setTimeout(function() { 374 insertTabs(tabs); 375 }, 1); 376 } 377 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; 383 384 e.preventDefault(); 385 386 // Share and save 387 share(function(url) { 388 window.location.href = url + '.go?download=true'; 389 }); 390 391 return true; 392 } 393 394 function keyHandler(e) { 395 if (opts.enableShortcuts && handleSaveShortcut(e)) return; 396 397 if (e.keyCode == 9 && !e.ctrlKey) { 398 // tab (but not ctrl-tab) 399 insertTabs(1); 400 e.preventDefault(); 401 return false; 402 } 403 if (e.keyCode == 13) { 404 // enter 405 if (e.shiftKey) { 406 // +shift 407 run(); 408 e.preventDefault(); 409 return false; 410 } 411 if (e.ctrlKey) { 412 // +control 413 fmt(); 414 e.preventDefault(); 415 } else { 416 autoindent(e.target); 417 } 418 } 419 return true; 420 } 421 code.unbind('keydown').bind('keydown', keyHandler); 422 var outdiv = $(opts.outputEl).empty(); 423 var output = $('<pre/>').appendTo(outdiv); 424 425 function body() { 426 return $(opts.codeEl).val(); 427 } 428 function setBody(text) { 429 $(opts.codeEl).val(text); 430 } 431 function origin(href) { 432 return ('' + href) 433 .split('/') 434 .slice(0, 3) 435 .join('/'); 436 } 437 438 var pushedEmpty = window.location.pathname == '/'; 439 function inputChanged() { 440 if (pushedEmpty) { 441 return; 442 } 443 pushedEmpty = true; 444 $(opts.shareURLEl).hide(); 445 window.history.pushState(null, '', '/'); 446 } 447 function popState(e) { 448 if (e === null) { 449 return; 450 } 451 if (e && e.state && e.state.code) { 452 setBody(e.state.code); 453 } 454 } 455 var rewriteHistory = false; 456 if ( 457 window.history && 458 window.history.pushState && 459 window.addEventListener && 460 opts.enableHistory 461 ) { 462 rewriteHistory = true; 463 code[0].addEventListener('input', inputChanged); 464 window.addEventListener('popstate', popState); 465 } 466 467 function setError(error) { 468 if (running) running.Kill(); 469 lineClear(); 470 lineHighlight(error); 471 output 472 .empty() 473 .addClass('error') 474 .text(error); 475 } 476 function loading() { 477 lineClear(); 478 if (running) running.Kill(); 479 output.removeClass('error').text('Waiting for remote server...'); 480 } 481 function run() { 482 loading(); 483 running = transport.Run( 484 body(), 485 highlightOutput(PlaygroundOutput(output[0])) 486 ); 487 } 488 489 function fmt() { 490 loading(); 491 var data = { body: body() }; 492 if ($(opts.fmtImportEl).is(':checked')) { 493 data['imports'] = 'true'; 494 } 495 $.ajax('/fmt', { 496 data: data, 497 type: 'POST', 498 dataType: 'json', 499 success: function(data) { 500 if (data.Error) { 501 setError(data.Error); 502 } else { 503 setBody(data.Body); 504 setError(''); 505 } 506 }, 507 }); 508 } 509 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); 515 516 if (sharing) return; 517 sharing = true; 518 519 var sharingData = body(); 520 $.ajax('/share', { 521 processData: false, 522 data: sharingData, 523 type: 'POST', 524 contentType: 'text/plain; charset=utf-8', 525 complete: function(xhr) { 526 sharing = false; 527 if (xhr.status != 200) { 528 alert('Server error; try again.'); 529 return; 530 } 531 if (opts.shareRedirect) { 532 window.location = opts.shareRedirect + xhr.responseText; 533 } 534 var path = '/p/' + xhr.responseText; 535 var url = origin(window.location) + path; 536 537 for (var i = 0; i < shareCallbacks.length; i++) { 538 shareCallbacks[i](url); 539 } 540 shareCallbacks = []; 541 542 if (shareURL) { 543 shareURL 544 .show() 545 .val(url) 546 .focus() 547 .select(); 548 549 if (rewriteHistory) { 550 var historyData = { code: sharingData }; 551 window.history.pushState(historyData, '', path); 552 pushedEmpty = false; 553 } 554 } 555 }, 556 }); 557 } 558 559 $(opts.runEl).click(run); 560 $(opts.fmtEl).click(fmt); 561 562 if ( 563 opts.shareEl !== null && 564 (opts.shareURLEl !== null || opts.shareRedirect !== null) 565 ) { 566 if (opts.shareURLEl) { 567 shareURL = $(opts.shareURLEl).hide(); 568 } 569 $(opts.shareEl).click(function() { 570 share(); 571 }); 572 } 573 574 if (opts.toysEl !== null) { 575 $(opts.toysEl).bind('change', function() { 576 var toy = $(this).val(); 577 $.ajax('/doc/play/' + toy, { 578 processData: false, 579 type: 'GET', 580 complete: function(xhr) { 581 if (xhr.status != 200) { 582 alert('Server error; try again.'); 583 return; 584 } 585 setBody(xhr.responseText); 586 }, 587 }); 588 }); 589 } 590 } 591 592 window.playground = playground; 593 })();