console/console.js

  1. /* Console
  2. * =====================
  3. * A console represents a text console that allows the user to print, println,
  4. * and read an integer using readInt, and read a float using readFloat. These
  5. * functions pop up prompt dialogs and make sure that the results are actually
  6. * of the desired type.
  7. *
  8. * @author Jeremy Keeshin July 9, 2012
  9. *
  10. */
  11. 'use strict';
  12. var safeEval = require('codehs-js-utils').safeEval;
  13. var lines = [];
  14. var solution = null;
  15. var TESTER_MESSAGE = '#tester-message';
  16. var PUBLIC_METHODS = [];
  17. var language = window.pageSpecific ? window.pageSpecific.preferredLang : undefined;
  18. /**
  19. * Set up an instance of the console library.
  20. * @constructor
  21. */
  22. function CodeHSConsole() {
  23. this.internalOutput = [];
  24. this.internalOutputBuffer = '';
  25. }
  26. /**
  27. * Adds a method to the public methods array.
  28. * @param {string} name - Name of the method.
  29. */
  30. CodeHSConsole.registerPublicMethod = function(name) {
  31. PUBLIC_METHODS.push(name);
  32. };
  33. /**
  34. * Generate strings for the public methods to bring them to the
  35. * public namespace without having to call them with the console instance.
  36. * @returns {string} Line broken function definitions.
  37. */
  38. CodeHSConsole.getNamespaceModifcationString = function() {
  39. var result = '';
  40. for (var i = 0; i < PUBLIC_METHODS.length; i++) {
  41. var curMethod = PUBLIC_METHODS[i];
  42. // Actually create a method in this scope with the name of the
  43. // method so the student can easily access it. For example, we
  44. // might have a method like CodeHSConsole.prototype.print, but we
  45. // want the student to be able to access it with just `print`, but
  46. // the proper context for this.
  47. result +=
  48. 'function ' +
  49. curMethod +
  50. '(){\n' +
  51. '\treturn __console__.' +
  52. curMethod +
  53. '.apply(__console__, arguments);\n' +
  54. '}\n';
  55. }
  56. return result;
  57. };
  58. /**
  59. * Generate stub strings for the public methods to bring them to the
  60. * namespace without having to call them with the console instance.
  61. * @returns {string} Line broken function definitions.
  62. */
  63. CodeHSConsole.getStubString = function() {
  64. var result = '';
  65. _.each(PUBLIC_METHODS, function(method) {
  66. result += 'function ' + method + '(){\n' + '\treturn 0;\n' + '}\n';
  67. });
  68. return result;
  69. };
  70. /**
  71. * Set the solution code for a given exercise.
  72. * @param {string} soln - Solution code.
  73. */
  74. CodeHSConsole.setSolution = function(soln) {
  75. solution = soln;
  76. };
  77. /**
  78. * Check the console output for correctness against solution code.
  79. * returns {object} Dictionary containing boolean of success and message.
  80. */
  81. CodeHSConsole.prototype.checkOutput = function() {
  82. if (!solution) {
  83. return;
  84. }
  85. var message;
  86. switch (language) {
  87. case 'es':
  88. message = '<strong>¡Gran trabajo!</strong> ¡Lo lograste!';
  89. break;
  90. default:
  91. message = '<strong>Nice job!</strong> You got it!';
  92. }
  93. var graded = {
  94. success: true,
  95. message: message,
  96. };
  97. if ($('#console').html().length === 0) {
  98. graded.success = false;
  99. graded.message = "You didn't print anything.";
  100. } else if (lines.length != solution.length) {
  101. graded.success = false;
  102. graded.message =
  103. '<strong>Not quite.</strong> Take a look at the ' +
  104. 'example output in the exercise tab.';
  105. } else {
  106. for (var i = 0; i < lines.length; i++) {
  107. var line = lines[i];
  108. var correct = solution[i];
  109. var regex = new RegExp(correct);
  110. if (line.search(regex) !== 0) {
  111. graded.success = false;
  112. graded.message =
  113. '<strong>Not quite.</strong> Take a look ' +
  114. 'at the example output in the exercise tab.';
  115. }
  116. }
  117. }
  118. $(TESTER_MESSAGE).html(graded.message);
  119. if (graded.success) {
  120. $(TESTER_MESSAGE)
  121. .removeClass('gone')
  122. .removeClass('alert-error')
  123. .addClass('alert-info');
  124. } else {
  125. $(TESTER_MESSAGE)
  126. .removeClass('gone')
  127. .removeClass('alert-info')
  128. .addClass('alert-error');
  129. }
  130. return graded;
  131. };
  132. var bufferedOutputToArray = function(bufferedOutput) {
  133. var bufferedOutputArray = bufferedOutput.split('\n');
  134. // remove the trailing "" that happens if the final element is a \n
  135. if (bufferedOutputArray[bufferedOutputArray.length - 1].length === 0) {
  136. bufferedOutputArray = bufferedOutputArray.slice(0, -1);
  137. }
  138. return bufferedOutputArray;
  139. };
  140. /**
  141. * A non-dom-mutating print for use in autograders.
  142. */
  143. CodeHSConsole.prototype.quietPrint = function(string) {
  144. if (!this.internalOutputBuffer) {
  145. this.internalOutputBuffer = '';
  146. }
  147. this.internalOutputBuffer += string;
  148. };
  149. /**
  150. * A non-dom-mutating println for use in autograders.
  151. */
  152. CodeHSConsole.prototype.quietPrintln = function(anything) {
  153. this.quietPrint(anything + '\n');
  154. };
  155. /**
  156. * Gets the internal output.
  157. */
  158. CodeHSConsole.prototype.flushQuietOutput = function() {
  159. if (!this.internalOutputBuffer) {
  160. this.internalOutputBuffer = '';
  161. }
  162. if (!this.internalOutput) {
  163. this.internalOutput = [];
  164. }
  165. var output = this.internalOutput.concat(bufferedOutputToArray(this.internalOutputBuffer));
  166. this.internalOutput = [];
  167. this.internalOutputBuffer = '';
  168. return output;
  169. };
  170. /**
  171. * Get the output from the console.
  172. * @returns {string}
  173. */
  174. CodeHSConsole.getOutput = function() {
  175. return $('#console').text();
  176. };
  177. /**
  178. * Check if the console exists.
  179. * Important to check before attempting to select and extract output.
  180. */
  181. CodeHSConsole.exists = function() {
  182. return $('#console').exists();
  183. };
  184. /**
  185. * Clear the console's text.
  186. */
  187. CodeHSConsole.clear = function() {
  188. lines = [];
  189. $('#console').html('');
  190. $(TESTER_MESSAGE).addClass('gone');
  191. };
  192. /**
  193. * Private method used to read a line.
  194. * @param {string} str - The line to be read.
  195. * @param {boolean} looping - Unsure. This is a messy method.
  196. */
  197. CodeHSConsole.prototype.readLinePrivate = function(str, looping) {
  198. if (typeof looping === 'undefined' || !looping) {
  199. this.print(str);
  200. }
  201. var console = $('#console');
  202. var lines;
  203. var result;
  204. if (console.length) {
  205. $('#console').css('margin-top', '180px');
  206. // take max 20 lines, last line is prompt string so we remove and
  207. // add extra spacing before putting it back on
  208. lines = _.takeRight(
  209. $('#console')
  210. .text()
  211. .split('\n'),
  212. 21
  213. );
  214. lines.pop();
  215. var text = lines.concat(['', '', str]).join('\n');
  216. result = prompt(text);
  217. $('#console').css('margin-top', '0px');
  218. } else {
  219. lines = this.internalOutput.slice(-10);
  220. result = prompt(lines.join('\n'));
  221. }
  222. if (typeof looping === 'undefined' || !looping) {
  223. this.println(result);
  224. }
  225. return result;
  226. };
  227. /**
  228. * This is how you run the code, but get access to the
  229. * state of the console library. The current instance
  230. * becomes accessible in the code.
  231. * @param {string} code - The code from the editor.
  232. */
  233. CodeHSConsole.prototype.runCode = function(code, options) {
  234. options = options || {};
  235. var lineOffset = options.lineOffset || 0;
  236. var publicMethodStrings = CodeHSConsole.getNamespaceModifcationString();
  237. // This code will create a local (to the student's program) `console`
  238. // variable, so console.log will be an alias to `println` so student code
  239. // can act more like "real" javascript
  240. var consoleOverride = ';var console = {}; console.log = println;\n';
  241. // To prevent issues with the native `Set`, we swap it out here.
  242. var setOverride = ';var __nativeSet=window.Set;var Set=window.chsSet;';
  243. var setRestore = ';window.Set=__nativeSet;';
  244. var wrap = '';
  245. wrap += publicMethodStrings;
  246. wrap += consoleOverride;
  247. wrap += setOverride;
  248. if (!options.overrideInfiniteLoops) {
  249. // tool all while loops
  250. var whileLoopRegEx = /while\s*\((.*)\)\s*{/gm;
  251. var forLoopRegEx = /for\s*\((.*)\)\s*{/gm;
  252. var doWhileRegEx = /do\s*\{/gm;
  253. // Inject into while loops
  254. code = code.replace(whileLoopRegEx, function(match, p1, offset, string) {
  255. var lineNumber = string.slice(0, offset).split('\n').length - lineOffset;
  256. var c =
  257. "if(___nloops++>15000){var e = new Error('Your while loop on line " +
  258. lineNumber +
  259. " may contain an infinite loop. Exiting.'); e.name = 'InfiniteLoop'; e.lineNumber = " +
  260. lineNumber +
  261. '; throw e;}';
  262. return 'var ___nloops=0;while(' + p1 + ') {' + c;
  263. });
  264. // Inject into while loops
  265. // See comment above for while loops.
  266. code = code.replace(forLoopRegEx, function(match, p1, offset, string) {
  267. var lineNumber = string.slice(0, offset).split('\n').length - lineOffset;
  268. var c =
  269. "if(___nloops++>15000){var e = new Error('Your for loop on line " +
  270. lineNumber +
  271. " may contain an infinite loop. Exiting.'); e.name = 'InfiniteLoop'; e.lineNumber = " +
  272. lineNumber +
  273. '; throw e;}';
  274. return 'var ___nloops=0;for(' + p1 + '){' + c;
  275. });
  276. // Inject into do-while loops
  277. code = code.replace(doWhileRegEx, function(match, offset, string) {
  278. var lineNumber = string.slice(0, offset).split('\n').length - lineOffset;
  279. var c =
  280. "if(___nloops++>15000){var e = new Error('Your do-while loop on line " +
  281. lineNumber +
  282. " may contain an infinite loop. Exiting.'); e.name = 'InfiniteLoop'; e.lineNumber = " +
  283. lineNumber +
  284. '; throw e;}';
  285. return 'var ___nloops=0;do {' + c;
  286. });
  287. }
  288. wrap += code;
  289. wrap += "\n\nif(typeof start == 'function') {start();} ";
  290. wrap += setRestore;
  291. wrap += '__console__.checkOutput();';
  292. this.internalOutput = [];
  293. return safeEval(wrap, this, '__console__');
  294. };
  295. /**
  296. * Method to test whether the code is requesting user input at all.
  297. * @param {string} code - The code from the editor
  298. */
  299. CodeHSConsole.prototype.hasUserinput = function(code) {
  300. return code.match(new RegExp('readLine|readInt|readFloat|readBoolean|readNumber'));
  301. };
  302. /** ************* PUBLIC METHODS *******************/
  303. /**
  304. * Clear the console.
  305. */
  306. CodeHSConsole.prototype.clear = function() {
  307. if (arguments.length !== 0) {
  308. throw new Error('You should not pass any arguments to clear');
  309. }
  310. CodeHSConsole.clear();
  311. };
  312. CodeHSConsole.registerPublicMethod('clear');
  313. /**
  314. * Print a line to the console.
  315. * @param {string} ln - The string to print.
  316. */
  317. CodeHSConsole.prototype.print = function(ln) {
  318. if (arguments.length !== 1) {
  319. throw new Error('You should pass exactly 1 argument to print');
  320. }
  321. var console = $('#console');
  322. if (console.length) {
  323. console.html($('#console').html() + ln);
  324. console.scrollTop($('#console')[0].scrollHeight);
  325. lines = console.html().split('\n');
  326. lines.splice(lines.length - 1, 1);
  327. } else {
  328. // we must be running outside of the site.
  329. // if there's a print attached to the console, use that, otherwise log like normal.
  330. this.internalOutput.push(ln);
  331. typeof window.console.print === 'function'
  332. ? window.console.print(ln)
  333. : window.console.log(ln);
  334. }
  335. };
  336. CodeHSConsole.registerPublicMethod('print');
  337. /**
  338. * Print a line to the console.
  339. * @param {string} ln - The string to print.
  340. */
  341. CodeHSConsole.prototype.println = function(ln) {
  342. if (arguments.length === 0) {
  343. ln = '';
  344. } else if (arguments.length !== 1) {
  345. throw new Error('You should pass exactly 1 argument to println');
  346. } else {
  347. this.print(ln + '\n');
  348. $('#console').scrollTop();
  349. }
  350. };
  351. CodeHSConsole.registerPublicMethod('println');
  352. /* Read a number from the user using JavaScripts prompt function.
  353. * We make sure here to check a few things.
  354. *
  355. * 1. If the user checks "Prevent this page from creating additional dialogs," we handle
  356. * that gracefully, by checking for a loop, and then returning a DEFAULT value.
  357. * 2. That we can properly parse a number according to the parse function PARSEFN passed in
  358. * as a parameter. For floats it is just parseFloat, but for ints it is our special parseInt
  359. * which actually does not even allow floats, even they they can properly be parsed as ints.
  360. * 3. The errorMsgType is a string helping us figure out what to print if it is not of the right
  361. * type.
  362. */
  363. CodeHSConsole.prototype.readNumber = function(str, parseFn, errorMsgType) {
  364. var DEFAULT = 0; // If we get into an infinite loop, return DEFAULT.
  365. var INFINITE_LOOP_CHECK = 100;
  366. var prompt = str;
  367. var looping = false;
  368. var loopCount = 0;
  369. // eslint-disable-next-line no-constant-condition
  370. while (true) {
  371. var result = this.readLinePrivate(prompt, looping);
  372. if (result === null) {
  373. return null;
  374. }
  375. result = parseFn(result);
  376. // Then it was okay.
  377. if (!isNaN(result)) {
  378. return result;
  379. }
  380. if (result === null) {
  381. return DEFAULT;
  382. }
  383. if (loopCount > INFINITE_LOOP_CHECK) {
  384. return DEFAULT;
  385. }
  386. prompt = 'That was not ' + errorMsgType + '. Please try again. ' + str;
  387. looping = true;
  388. loopCount++;
  389. }
  390. };
  391. CodeHSConsole.registerPublicMethod('readNumber');
  392. /**
  393. * Read a line from the user.
  394. * @param {str} str - A message associated with the modal asking for input.
  395. * @returns {str} The result of the readLine prompt.
  396. */
  397. CodeHSConsole.prototype.readLine = function(str) {
  398. if (arguments.length !== 1) {
  399. throw new Error('You should pass exactly 1 argument to readLine');
  400. }
  401. return this.readLinePrivate(str, false);
  402. };
  403. CodeHSConsole.registerPublicMethod('readLine');
  404. /**
  405. * Read a bool from the user.
  406. * @param {str} str - A message associated with the modal asking for input.
  407. * @returns {str} The result of the readBoolean prompt.
  408. */
  409. CodeHSConsole.prototype.readBoolean = function(str) {
  410. if (arguments.length !== 1) {
  411. throw new Error('You should pass exactly 1 argument to readBoolean');
  412. }
  413. return this.readNumber(
  414. str,
  415. function(x) {
  416. if (x === null) {
  417. return NaN;
  418. }
  419. x = x.toLowerCase();
  420. if (x == 'true' || x == 'yes') {
  421. return true;
  422. }
  423. if (x == 'false' || x == 'no') {
  424. return false;
  425. }
  426. return NaN;
  427. },
  428. 'a boolean (true/false)'
  429. );
  430. };
  431. CodeHSConsole.registerPublicMethod('readBoolean');
  432. /* Read an int with our special parseInt function which doesnt allow floats, even
  433. * though they are successfully parsed as ints.
  434. * @param {str} str - A message associated with the modal asking for input.
  435. * @returns {str} The result of the readInt prompt.
  436. */
  437. CodeHSConsole.prototype.readInt = function(str) {
  438. if (arguments.length !== 1) {
  439. throw new Error('You should pass exactly 1 argument to readInt');
  440. }
  441. return this.readNumber(
  442. str,
  443. function(x) {
  444. var resultInt = parseInt(x);
  445. var resultFloat = parseFloat(x);
  446. // Make sure the value when parsed as both an int and a float are the same
  447. if (resultInt == resultFloat) {
  448. return resultInt;
  449. }
  450. return NaN;
  451. },
  452. 'an integer'
  453. );
  454. };
  455. CodeHSConsole.registerPublicMethod('readInt');
  456. /* Read a float with our safe helper function.
  457. * @param {str} str - A message associated with the modal asking for input.
  458. * @returns {str} The result of the readFloat prompt.
  459. */
  460. CodeHSConsole.prototype.readFloat = function(str) {
  461. if (arguments.length !== 1) {
  462. throw new Error('You should pass exactly 1 argument to readFloat');
  463. }
  464. return this.readNumber(str, parseFloat, 'a float');
  465. };
  466. CodeHSConsole.registerPublicMethod('readFloat');
  467. module.exports = {
  468. CodeHSConsole: CodeHSConsole,
  469. PUBLIC_METHODS: PUBLIC_METHODS,
  470. };