You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

467 lines
12 KiB

  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. In the absence of any formal way to specify interfaces in JavaScript,
  6. here's a skeleton implementation of a playground transport.
  7. function Transport() {
  8. // Set up any transport state (eg, make a websocket connection).
  9. return {
  10. Run: function(body, output, options) {
  11. // Compile and run the program 'body' with 'options'.
  12. // Call the 'output' callback to display program output.
  13. return {
  14. Kill: function() {
  15. // Kill the running program.
  16. }
  17. };
  18. }
  19. };
  20. }
  21. // The output callback is called multiple times, and each time it is
  22. // passed an object of this form.
  23. var write = {
  24. Kind: 'string', // 'start', 'stdout', 'stderr', 'end'
  25. Body: 'string' // content of write or end status message
  26. }
  27. // The first call must be of Kind 'start' with no body.
  28. // Subsequent calls may be of Kind 'stdout' or 'stderr'
  29. // and must have a non-null Body string.
  30. // The final call should be of Kind 'end' with an optional
  31. // Body string, signifying a failure ("killed", for example).
  32. // The output callback must be of this form.
  33. // See PlaygroundOutput (below) for an implementation.
  34. function outputCallback(write) {
  35. }
  36. */
  37. function HTTPTransport() {
  38. 'use strict';
  39. // TODO(adg): support stderr
  40. function playback(output, events) {
  41. var timeout;
  42. output({Kind: 'start'});
  43. function next() {
  44. if (!events || events.length === 0) {
  45. output({Kind: 'end'});
  46. return;
  47. }
  48. var e = events.shift();
  49. if (e.Delay === 0) {
  50. output({Kind: 'stdout', Body: e.Message});
  51. next();
  52. return;
  53. }
  54. timeout = setTimeout(function() {
  55. output({Kind: 'stdout', Body: e.Message});
  56. next();
  57. }, e.Delay / 1000000);
  58. }
  59. next();
  60. return {
  61. Stop: function() {
  62. clearTimeout(timeout);
  63. }
  64. }
  65. }
  66. function error(output, msg) {
  67. output({Kind: 'start'});
  68. output({Kind: 'stderr', Body: msg});
  69. output({Kind: 'end'});
  70. }
  71. var seq = 0;
  72. return {
  73. Run: function(body, output, options) {
  74. seq++;
  75. var cur = seq;
  76. var playing;
  77. $.ajax('/compile', {
  78. type: 'POST',
  79. data: {'version': 2, 'body': body},
  80. dataType: 'json',
  81. success: function(data) {
  82. if (seq != cur) return;
  83. if (!data) return;
  84. if (playing != null) playing.Stop();
  85. if (data.Errors) {
  86. error(output, data.Errors);
  87. return;
  88. }
  89. playing = playback(output, data.Events);
  90. },
  91. error: function() {
  92. error(output, 'Error communicating with remote server.');
  93. }
  94. });
  95. return {
  96. Kill: function() {
  97. if (playing != null) playing.Stop();
  98. output({Kind: 'end', Body: 'killed'});
  99. }
  100. };
  101. }
  102. };
  103. }
  104. function SocketTransport() {
  105. 'use strict';
  106. var id = 0;
  107. var outputs = {};
  108. var started = {};
  109. var websocket = new WebSocket('ws://' + window.location.host + '/socket');
  110. websocket.onclose = function() {
  111. console.log('websocket connection closed');
  112. }
  113. websocket.onmessage = function(e) {
  114. var m = JSON.parse(e.data);
  115. var output = outputs[m.Id];
  116. if (output === null)
  117. return;
  118. if (!started[m.Id]) {
  119. output({Kind: 'start'});
  120. started[m.Id] = true;
  121. }
  122. output({Kind: m.Kind, Body: m.Body});
  123. }
  124. function send(m) {
  125. websocket.send(JSON.stringify(m));
  126. }
  127. return {
  128. Run: function(body, output, options) {
  129. var thisID = id+'';
  130. id++;
  131. outputs[thisID] = output;
  132. send({Id: thisID, Kind: 'run', Body: body, Options: options});
  133. return {
  134. Kill: function() {
  135. send({Id: thisID, Kind: 'kill'});
  136. }
  137. };
  138. }
  139. };
  140. }
  141. function PlaygroundOutput(el) {
  142. 'use strict';
  143. return function(write) {
  144. if (write.Kind == 'start') {
  145. el.innerHTML = '';
  146. return;
  147. }
  148. var cl = 'system';
  149. if (write.Kind == 'stdout' || write.Kind == 'stderr')
  150. cl = write.Kind;
  151. var m = write.Body;
  152. if (write.Kind == 'end') {
  153. m = '\nProgram exited' + (m?(': '+m):'.');
  154. }
  155. if (m.indexOf('IMAGE:') === 0) {
  156. // TODO(adg): buffer all writes before creating image
  157. var url = 'data:image/png;base64,' + m.substr(6);
  158. var img = document.createElement('img');
  159. img.src = url;
  160. el.appendChild(img);
  161. return;
  162. }
  163. // ^L clears the screen.
  164. var s = m.split('\x0c');
  165. if (s.length > 1) {
  166. el.innerHTML = '';
  167. m = s.pop();
  168. }
  169. m = m.replace(/&/g, '&');
  170. m = m.replace(/</g, '&lt;');
  171. m = m.replace(/>/g, '&gt;');
  172. var needScroll = (el.scrollTop + el.offsetHeight) == el.scrollHeight;
  173. var span = document.createElement('span');
  174. span.className = cl;
  175. span.innerHTML = m;
  176. el.appendChild(span);
  177. if (needScroll)
  178. el.scrollTop = el.scrollHeight - el.offsetHeight;
  179. }
  180. }
  181. (function() {
  182. function lineHighlight(error) {
  183. var regex = /prog.go:([0-9]+)/g;
  184. var r = regex.exec(error);
  185. while (r) {
  186. $(".lines div").eq(r[1]-1).addClass("lineerror");
  187. r = regex.exec(error);
  188. }
  189. }
  190. function highlightOutput(wrappedOutput) {
  191. return function(write) {
  192. if (write.Body) lineHighlight(write.Body);
  193. wrappedOutput(write);
  194. }
  195. }
  196. function lineClear() {
  197. $(".lineerror").removeClass("lineerror");
  198. }
  199. // opts is an object with these keys
  200. // codeEl - code editor element
  201. // outputEl - program output element
  202. // runEl - run button element
  203. // fmtEl - fmt button element (optional)
  204. // fmtImportEl - fmt "imports" checkbox element (optional)
  205. // shareEl - share button element (optional)
  206. // shareURLEl - share URL text input element (optional)
  207. // shareRedirect - base URL to redirect to on share (optional)
  208. // toysEl - toys select element (optional)
  209. // enableHistory - enable using HTML5 history API (optional)
  210. // transport - playground transport to use (default is HTTPTransport)
  211. // enableShortcuts - whether to enable shortcuts (Ctrl+S/Cmd+S to save) (default is false)
  212. function playground(opts) {
  213. var code = $(opts.codeEl);
  214. var transport = opts['transport'] || new HTTPTransport();
  215. var running;
  216. // autoindent helpers.
  217. function insertTabs(n) {
  218. // find the selection start and end
  219. var start = code[0].selectionStart;
  220. var end = code[0].selectionEnd;
  221. // split the textarea content into two, and insert n tabs
  222. var v = code[0].value;
  223. var u = v.substr(0, start);
  224. for (var i=0; i<n; i++) {
  225. u += "\t";
  226. }
  227. u += v.substr(end);
  228. // set revised content
  229. code[0].value = u;
  230. // reset caret position after inserted tabs
  231. code[0].selectionStart = start+n;
  232. code[0].selectionEnd = start+n;
  233. }
  234. function autoindent(el) {
  235. var curpos = el.selectionStart;
  236. var tabs = 0;
  237. while (curpos > 0) {
  238. curpos--;
  239. if (el.value[curpos] == "\t") {
  240. tabs++;
  241. } else if (tabs > 0 || el.value[curpos] == "\n") {
  242. break;
  243. }
  244. }
  245. setTimeout(function() {
  246. insertTabs(tabs);
  247. }, 1);
  248. }
  249. // NOTE(cbro): e is a jQuery event, not a DOM event.
  250. function handleSaveShortcut(e) {
  251. if (e.isDefaultPrevented()) return false;
  252. if (!e.metaKey && !e.ctrlKey) return false;
  253. if (e.key != "S" && e.key != "s") return false;
  254. e.preventDefault();
  255. // Share and save
  256. share(function(url) {
  257. window.location.href = url + ".go?download=true";
  258. });
  259. return true;
  260. }
  261. function keyHandler(e) {
  262. if (opts.enableShortcuts && handleSaveShortcut(e)) return;
  263. if (e.keyCode == 9 && !e.ctrlKey) { // tab (but not ctrl-tab)
  264. insertTabs(1);
  265. e.preventDefault();
  266. return false;
  267. }
  268. if (e.keyCode == 13) { // enter
  269. if (e.shiftKey) { // +shift
  270. run();
  271. e.preventDefault();
  272. return false;
  273. } if (e.ctrlKey) { // +control
  274. fmt();
  275. e.preventDefault();
  276. } else {
  277. autoindent(e.target);
  278. }
  279. }
  280. return true;
  281. }
  282. code.unbind('keydown').bind('keydown', keyHandler);
  283. var outdiv = $(opts.outputEl).empty();
  284. var output = $('<pre/>').appendTo(outdiv);
  285. function body() {
  286. return $(opts.codeEl).val();
  287. }
  288. function setBody(text) {
  289. $(opts.codeEl).val(text);
  290. }
  291. function origin(href) {
  292. return (""+href).split("/").slice(0, 3).join("/");
  293. }
  294. var pushedEmpty = (window.location.pathname == "/");
  295. function inputChanged() {
  296. if (pushedEmpty) {
  297. return;
  298. }
  299. pushedEmpty = true;
  300. $(opts.shareURLEl).hide();
  301. window.history.pushState(null, "", "/");
  302. }
  303. function popState(e) {
  304. if (e === null) {
  305. return;
  306. }
  307. if (e && e.state && e.state.code) {
  308. setBody(e.state.code);
  309. }
  310. }
  311. var rewriteHistory = false;
  312. if (window.history && window.history.pushState && window.addEventListener && opts.enableHistory) {
  313. rewriteHistory = true;
  314. code[0].addEventListener('input', inputChanged);
  315. window.addEventListener('popstate', popState);
  316. }
  317. function setError(error) {
  318. if (running) running.Kill();
  319. lineClear();
  320. lineHighlight(error);
  321. output.empty().addClass("error").text(error);
  322. }
  323. function loading() {
  324. lineClear();
  325. if (running) running.Kill();
  326. output.removeClass("error").text('Waiting for remote server...');
  327. }
  328. function run() {
  329. loading();
  330. running = transport.Run(body(), highlightOutput(PlaygroundOutput(output[0])));
  331. }
  332. function fmt() {
  333. loading();
  334. var data = {"body": body()};
  335. if ($(opts.fmtImportEl).is(":checked")) {
  336. data["imports"] = "true";
  337. }
  338. $.ajax("/fmt", {
  339. data: data,
  340. type: "POST",
  341. dataType: "json",
  342. success: function(data) {
  343. if (data.Error) {
  344. setError(data.Error);
  345. } else {
  346. setBody(data.Body);
  347. setError("");
  348. }
  349. }
  350. });
  351. }
  352. var shareURL; // jQuery element to show the shared URL.
  353. var sharing = false; // true if there is a pending request.
  354. var shareCallbacks = [];
  355. function share(opt_callback) {
  356. if (opt_callback) shareCallbacks.push(opt_callback);
  357. if (sharing) return;
  358. sharing = true;
  359. var sharingData = body();
  360. $.ajax("/share", {
  361. processData: false,
  362. data: sharingData,
  363. type: "POST",
  364. complete: function(xhr) {
  365. sharing = false;
  366. if (xhr.status != 200) {
  367. alert("Server error; try again.");
  368. return;
  369. }
  370. if (opts.shareRedirect) {
  371. window.location = opts.shareRedirect + xhr.responseText;
  372. }
  373. var path = "/p/" + xhr.responseText;
  374. var url = origin(window.location) + path;
  375. for (var i = 0; i < shareCallbacks.length; i++) {
  376. shareCallbacks[i](url);
  377. }
  378. shareCallbacks = [];
  379. if (shareURL) {
  380. shareURL.show().val(url).focus().select();
  381. if (rewriteHistory) {
  382. var historyData = {"code": sharingData};
  383. window.history.pushState(historyData, "", path);
  384. pushedEmpty = false;
  385. }
  386. }
  387. }
  388. });
  389. }
  390. $(opts.runEl).click(run);
  391. $(opts.fmtEl).click(fmt);
  392. if (opts.shareEl !== null && (opts.shareURLEl !== null || opts.shareRedirect !== null)) {
  393. if (opts.shareURLEl) {
  394. shareURL = $(opts.shareURLEl).hide();
  395. }
  396. $(opts.shareEl).click(function() {
  397. share();
  398. });
  399. }
  400. if (opts.toysEl !== null) {
  401. $(opts.toysEl).bind('change', function() {
  402. var toy = $(this).val();
  403. $.ajax("/doc/play/"+toy, {
  404. processData: false,
  405. type: "GET",
  406. complete: function(xhr) {
  407. if (xhr.status != 200) {
  408. alert("Server error; try again.");
  409. return;
  410. }
  411. setBody(xhr.responseText);
  412. }
  413. });
  414. });
  415. }
  416. }
  417. window.playground = playground;
  418. })();