@khanacademy/perseus-score 2.3.6 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,35 +1,8 @@
1
- 'use strict';
2
-
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
- var KAS = require('@khanacademy/kas');
6
- var kmath = require('@khanacademy/kmath');
7
- var perseusCore = require('@khanacademy/perseus-core');
8
- var _ = require('underscore');
9
- var perseusScore = require('@khanacademy/perseus-score');
10
-
11
- function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
12
-
13
- function _interopNamespace(e) {
14
- if (e && e.__esModule) return e;
15
- var n = Object.create(null);
16
- if (e) {
17
- Object.keys(e).forEach(function (k) {
18
- if (k !== 'default') {
19
- var d = Object.getOwnPropertyDescriptor(e, k);
20
- Object.defineProperty(n, k, d.get ? d : {
21
- enumerable: true,
22
- get: function () { return e[k]; }
23
- });
24
- }
25
- });
26
- }
27
- n["default"] = e;
28
- return Object.freeze(n);
29
- }
30
-
31
- var KAS__namespace = /*#__PURE__*/_interopNamespace(KAS);
32
- var ___default = /*#__PURE__*/_interopDefaultLegacy(_);
1
+ import _extends from '@babel/runtime/helpers/extends';
2
+ import * as KAS from '@khanacademy/kas';
3
+ import { KhanMath, geometry, angles, coefficients, number } from '@khanacademy/kmath';
4
+ import { PerseusError, Errors, getDecimalSeparator, GrapherUtil, approximateDeepEqual, approximateEqual, deepClone, getMatrixSize, getWidgetIdsFromContent, getUpgradedWidgetOptions } from '@khanacademy/perseus-core';
5
+ import _ from 'underscore';
33
6
 
34
7
  const MISSING_PERCENT_ERROR = "MISSING_PERCENT_ERROR";
35
8
  const NEEDS_TO_BE_SIMPLIFIED_ERROR = "NEEDS_TO_BE_SIMPLIFIED_ERROR";
@@ -56,135 +29,33 @@ const ErrorCodes = {
56
29
  FILL_ALL_CELLS_ERROR
57
30
  };
58
31
 
59
- /* eslint-disable no-useless-escape */
60
32
  const MAXERROR_EPSILON = Math.pow(2, -42);
61
-
62
- // TOOD(kevinb): Figure out how this relates to KEScore in
63
- // perseus-all-package/types.js and see if there's a way to
64
- // unify these types.
65
-
66
- /**
67
- * Answer types
68
- *
69
- * Utility for creating answerable questions displayed in exercises
70
- *
71
- * Different answer types produce different kinds of input displays, and do
72
- * different kinds of checking on the solutions.
73
- *
74
- * Each of the objects contain two functions, setup and createValidator.
75
- *
76
- * The setup function takes a solutionarea and solution, and performs setup
77
- * within the solutionarea, and then returns an object which contains:
78
- *
79
- * answer: a function which, when called, will retrieve the current answer from
80
- * the solutionarea, which can then be validated using the validator
81
- * function
82
- * validator: a function returned from the createValidator function (defined
83
- * below)
84
- * solution: the correct answer to the problem
85
- * showGuess: a function which, when given a guess, shows the guess within the
86
- * provided solutionarea
87
- * showGuessCustom: a function which displays parts of a guess that are not
88
- * within the solutionarea; currently only used for custom
89
- * answers
90
- *
91
- * The createValidator function only takes a solution, and it returns a
92
- * function which can be used to validate an answer.
93
- *
94
- * The resulting validator function returns:
95
- * - true: if the answer is fully correct
96
- * - false: if the answer is incorrect
97
- * - "" (the empty string): if no answer has been provided (e.g. the answer box
98
- * is left unfilled)
99
- * - a string: if there is some slight error
100
- *
101
- * In most cases, setup and createValidator don't really need the solution DOM
102
- * element so we have setupFunctional and createValidatorFunctional for them
103
- * which take only $solution.text() and $solution.data(). This makes it easier
104
- * to reuse specific answer types.
105
- *
106
- * TODO(alpert): Think of a less-absurd name for createValidatorFunctional.
107
- *
108
- */
109
33
  const KhanAnswerTypes = {
110
- /*
111
- * predicate answer type
112
- *
113
- * performs simple predicate-based checking of a numeric solution, with
114
- * different kinds of number formats
115
- *
116
- * Uses the data-forms option on the solution to choose which number formats
117
- * are acceptable. Available data-forms:
118
- *
119
- * - integer: 3
120
- * - proper: 3/5
121
- * - improper: 5/3
122
- * - pi: 3 pi
123
- * - log: log(5)
124
- * - percent: 15%
125
- * - mixed: 1 1/3
126
- * - decimal: 1.7
127
- *
128
- * The solution should be a predicate of the form:
129
- *
130
- * function(guess, maxError) {
131
- * return abs(guess - 3) < maxError;
132
- * }
133
- *
134
- */
135
34
  predicate: {
136
35
  defaultForms: "integer, proper, improper, mixed, decimal",
137
36
  createValidatorFunctional: function (predicate, options) {
138
- // Extract the options from the given solution object
139
- options = ___default["default"].extend({
37
+ options = _.extend({
140
38
  simplify: "required",
141
39
  ratio: false,
142
40
  forms: KhanAnswerTypes.predicate.defaultForms
143
41
  }, options);
144
42
  let acceptableForms;
145
- // this is maintaining backwards compatibility
146
- // TODO(merlob) fix all places that depend on this, then delete
147
- if (!___default["default"].isArray(options.forms)) {
43
+ if (!_.isArray(options.forms)) {
148
44
  acceptableForms = options.forms.split(/\s*,\s*/);
149
45
  } else {
150
46
  acceptableForms = options.forms;
151
47
  }
152
-
153
- // TODO(jack): remove options.inexact in favor of options.maxError
154
48
  if (options.inexact === undefined) {
155
- // If we aren't allowing inexact, ensure that we don't have a
156
- // large maxError as well.
157
49
  options.maxError = 0;
158
50
  }
159
- // Allow a small tolerance on maxError, to avoid numerical
160
- // representation issues (2.3 should be correct for a solution of
161
- // 2.45 with maxError=0.15).
162
51
  options.maxError = +options.maxError + MAXERROR_EPSILON;
163
-
164
- // If percent is an acceptable form, make sure it's the last one
165
- // in the list so we don't prematurely complain about not having
166
- // a percent sign when the user entered the correct answer in a
167
- // different form (such as a decimal or fraction)
168
- if (___default["default"].contains(acceptableForms, "percent")) {
169
- acceptableForms = ___default["default"].without(acceptableForms, "percent");
52
+ if (_.contains(acceptableForms, "percent")) {
53
+ acceptableForms = _.without(acceptableForms, "percent");
170
54
  acceptableForms.push("percent");
171
55
  }
172
-
173
- // Take text looking like a fraction, and turn it into a number
174
- const fractionTransformer = function (text) {
175
- text = text
176
- // Replace unicode minus sign with hyphen
177
- .replace(/\u2212/, "-")
178
- // Remove space after +, -
179
- .replace(/([+-])\s+/g, "$1")
180
- // Remove leading/trailing whitespace
181
- .replace(/(^\s*)|(\s*$)/gi, "");
182
-
183
- // Extract numerator and denominator
56
+ const fractionTransformer = function fractionTransformer(text) {
57
+ text = text.replace(/\u2212/, "-").replace(/([+-])\s+/g, "$1").replace(/(^\s*)|(\s*$)/gi, "");
184
58
  const match = text.match(/^([+-]?\d+)\s*\/\s*([+-]?\d+)$/);
185
- // Fractions are represented as "-\frac{numerator}{denominator}"
186
- // in Mobile device input instead of "numerator/denominator" as
187
- // in web-browser.
188
59
  const mobileDeviceMatch = text.match(/^([+-]?)\\frac\{([+-]?\d+)\}\{([+-]?\d+)\}$/);
189
60
  const parsedInt = parseInt(text, 10);
190
61
  if (match || mobileDeviceMatch) {
@@ -204,7 +75,7 @@ const KhanAnswerTypes = {
204
75
  }
205
76
  denom = parseFloat(mobileDeviceMatch[3]);
206
77
  }
207
- simplified = simplified && denom > 0 && (options.ratio || denom !== 1) && kmath.KhanMath.getGCD(num, denom) === 1;
78
+ simplified = simplified && denom > 0 && (options.ratio || denom !== 1) && KhanMath.getGCD(num, denom) === 1;
208
79
  return [{
209
80
  value: num / denom,
210
81
  exact: simplified
@@ -218,23 +89,8 @@ const KhanAnswerTypes = {
218
89
  }
219
90
  return [];
220
91
  };
221
-
222
- /*
223
- * Different forms of numbers
224
- *
225
- * Each function returns a list of objects of the form:
226
- *
227
- * {
228
- * value: numerical value,
229
- * exact: true/false
230
- * }
231
- */
232
92
  const forms = {
233
- // integer, which is encompassed by decimal
234
93
  integer: function (text) {
235
- // Compare the decimal form to the decimal form rounded to
236
- // an integer. Only accept if the user actually entered an
237
- // integer.
238
94
  const decimal = forms.decimal(text);
239
95
  const rounded = forms.decimal(text, 1);
240
96
  if (decimal[0].value != null && decimal[0].value === rounded[0].value || decimal[1].value != null && decimal[1].value === rounded[1].value) {
@@ -242,101 +98,64 @@ const KhanAnswerTypes = {
242
98
  }
243
99
  return [];
244
100
  },
245
- // A proper fraction
246
101
  proper: function (text) {
247
102
  const transformed = fractionTransformer(text);
248
103
  return transformed.flatMap(o => {
249
- // All fractions that are less than 1
250
104
  if (Math.abs(o.value) < 1) {
251
105
  return [o];
252
106
  }
253
107
  return [];
254
108
  });
255
109
  },
256
- // an improper fraction
257
110
  improper: function (text) {
258
- // As our answer keys are always in simplest form, we need
259
- // to check for the existence of a fraction in the input before
260
- // validating the answer. If no fraction is found, we consider
261
- // the answer to be incorrect.
262
111
  const fractionExists = text.includes("/") || text.match(/\\(d?frac)/);
263
112
  if (!fractionExists) {
264
113
  return [];
265
114
  }
266
115
  const transformed = fractionTransformer(text);
267
116
  return transformed.flatMap(o => {
268
- // All fractions that are greater than 1
269
117
  if (Math.abs(o.value) >= 1) {
270
118
  return [o];
271
119
  }
272
120
  return [];
273
121
  });
274
122
  },
275
- // pi-like numbers
276
123
  pi: function (text) {
277
124
  let match;
278
125
  let possibilities = [];
279
-
280
- // Replace unicode minus sign with hyphen
281
126
  text = text.replace(/\u2212/, "-");
282
-
283
- // - pi
284
- // (Note: we also support \pi (for TeX), p, tau (and \tau,
285
- // and t), pau.)
286
127
  if (match = text.match(/^([+-]?)\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)$/i)) {
287
128
  possibilities = [{
288
129
  value: parseFloat(match[1] + "1"),
289
130
  exact: true
290
131
  }];
291
-
292
- // 5 / 6 pi
293
132
  } else if (match = text.match(/^([+-]?\s*\d+\s*(?:\/\s*[+-]?\s*\d+)?)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)$/i)) {
294
133
  possibilities = fractionTransformer(match[1]);
295
-
296
- // 4 5 / 6 pi
297
134
  } else if (match = text.match(/^([+-]?)\s*(\d+)\s*([+-]?\d+)\s*\/\s*([+-]?\d+)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)$/i)) {
298
135
  const sign = parseFloat(match[1] + "1");
299
136
  const integ = parseFloat(match[2]);
300
137
  const num = parseFloat(match[3]);
301
138
  const denom = parseFloat(match[4]);
302
- const simplified = num < denom && kmath.KhanMath.getGCD(num, denom) === 1;
139
+ const simplified = num < denom && KhanMath.getGCD(num, denom) === 1;
303
140
  possibilities = [{
304
141
  value: sign * (integ + num / denom),
305
142
  exact: simplified
306
143
  }];
307
-
308
- // 5 pi / 6
309
144
  } else if (match = text.match(/^([+-]?\s*\d+)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)\s*(?:\/\s*([+-]?\s*\d+))?$/i)) {
310
145
  possibilities = fractionTransformer(match[1] + "/" + match[3]);
311
-
312
- // - pi / 4
313
146
  } else if (match = text.match(/^([+-]?)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)\s*(?:\/\s*([+-]?\d+))?$/i)) {
314
147
  possibilities = fractionTransformer(match[1] + "1/" + match[3]);
315
-
316
- // 0
317
148
  } else if (text === "0") {
318
149
  possibilities = [{
319
150
  value: 0,
320
151
  exact: true
321
152
  }];
322
-
323
- // 0.5 pi (fallback)
324
153
  } else if (match = text.match(/^(.+)\s*\*?\s*(\\?pi|p|\u03c0|\\?tau|t|\u03c4|pau)$/i)) {
325
154
  possibilities = forms.decimal(match[1]);
326
155
  } else {
327
- possibilities = ___default["default"].reduce(KhanAnswerTypes.predicate.defaultForms.split(/\s*,\s*/), function (memo, form) {
156
+ possibilities = _.reduce(KhanAnswerTypes.predicate.defaultForms.split(/\s*,\s*/), function (memo, form) {
328
157
  return memo.concat(forms[form](text));
329
158
  }, []);
330
-
331
- // If the answer is a floating point number that's
332
- // near a multiple of pi, mark is as being possibly
333
- // an approximation of pi. We actually check if
334
- // it's a plausible approximation of pi/12, since
335
- // sometimes the correct answer is like pi/3 or pi/4.
336
- // We also say it's a pi-approximation if it involves
337
- // x/7 (since 22/7 is an approximation of pi.)
338
- // Never mark an integer as being an approximation
339
- // of pi.
340
159
  let approximatesPi = false;
341
160
  const number = parseFloat(text);
342
161
  if (!isNaN(number) && number !== parseInt(text)) {
@@ -349,7 +168,7 @@ const KhanAnswerTypes = {
349
168
  approximatesPi = true;
350
169
  }
351
170
  if (approximatesPi) {
352
- ___default["default"].each(possibilities, function (possibility) {
171
+ _.each(possibilities, function (possibility) {
353
172
  possibility.piApprox = true;
354
173
  });
355
174
  }
@@ -359,9 +178,6 @@ const KhanAnswerTypes = {
359
178
  if (text.match(/\\?tau|t|\u03c4/)) {
360
179
  multiplier = Math.PI * 2;
361
180
  }
362
-
363
- // We're taking an early stand along side xkcd in the
364
- // inevitable ti vs. pau debate... http://xkcd.com/1292
365
181
  if (text.match(/pau/)) {
366
182
  multiplier = Math.PI * 1.5;
367
183
  }
@@ -370,12 +186,8 @@ const KhanAnswerTypes = {
370
186
  });
371
187
  return possibilities;
372
188
  },
373
- // Converts '' to 1 and '-' to -1 so you can write "[___] x"
374
- // and accept sane things
375
189
  coefficient: function (text) {
376
190
  let possibilities = [];
377
-
378
- // Replace unicode minus sign with hyphen
379
191
  text = text.replace(/\u2212/, "-");
380
192
  if (text === "") {
381
193
  possibilities = [{
@@ -390,19 +202,14 @@ const KhanAnswerTypes = {
390
202
  }
391
203
  return possibilities;
392
204
  },
393
- // simple log(c) form
394
205
  log: function (text) {
395
206
  let match;
396
207
  let possibilities = [];
397
-
398
- // Replace unicode minus sign with hyphen
399
208
  text = text.replace(/\u2212/, "-");
400
209
  text = text.replace(/[ \(\)]/g, "");
401
210
  if (match = text.match(/^log\s*(\S+)\s*$/i)) {
402
- // @ts-expect-error - TS2322 - Type '{ value: number | undefined; exact: boolean; }[]' is not assignable to type 'never[]'.
403
211
  possibilities = forms.decimal(match[1]);
404
212
  } else if (text === "0") {
405
- // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'never'. | TS2322 - Type 'boolean' is not assignable to type 'never'.
406
213
  possibilities = [{
407
214
  value: 0,
408
215
  exact: true
@@ -410,10 +217,8 @@ const KhanAnswerTypes = {
410
217
  }
411
218
  return possibilities;
412
219
  },
413
- // Numbers with percent signs
414
220
  percent: function (text) {
415
221
  text = String(text).trim();
416
- // store whether or not there is a percent sign
417
222
  let hasPercentSign = false;
418
223
  if (text.indexOf("%") === text.length - 1) {
419
224
  text = text.substring(0, text.length - 1).trim();
@@ -422,26 +227,18 @@ const KhanAnswerTypes = {
422
227
  const transformed = forms.decimal(text);
423
228
  transformed.forEach(t => {
424
229
  t.exact = hasPercentSign;
425
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
426
230
  t.value = t.value / 100;
427
231
  });
428
232
  return transformed;
429
233
  },
430
- // Mixed numbers, like 1 3/4
431
234
  mixed: function (text) {
432
- const match = text
433
- // Replace unicode minus sign with hyphen
434
- .replace(/\u2212/, "-")
435
- // Remove space after +, -
436
- .replace(/([+-])\s+/g, "$1")
437
- // Extract integer, numerator and denominator
438
- .match(/^([+-]?)(\d+)\s+(\d+)\s*\/\s*(\d+)$/);
235
+ const match = text.replace(/\u2212/, "-").replace(/([+-])\s+/g, "$1").match(/^([+-]?)(\d+)\s+(\d+)\s*\/\s*(\d+)$/);
439
236
  if (match) {
440
237
  const sign = parseFloat(match[1] + "1");
441
238
  const integ = parseFloat(match[2]);
442
239
  const num = parseFloat(match[3]);
443
240
  const denom = parseFloat(match[4]);
444
- const simplified = num < denom && kmath.KhanMath.getGCD(num, denom) === 1;
241
+ const simplified = num < denom && KhanMath.getGCD(num, denom) === 1;
445
242
  return [{
446
243
  value: sign * (integ + num / denom),
447
244
  exact: simplified
@@ -449,28 +246,10 @@ const KhanAnswerTypes = {
449
246
  }
450
247
  return [];
451
248
  },
452
- // Decimal numbers -- compare entered text rounded to
453
- // 'precision' Reciprical of the precision against the correct
454
- // answer. We round to 1/1e10 by default, which is healthily
455
- // less than machine epsilon but should be more than any real
456
- // decimal answer would use. (The 'integer' answer type uses
457
- // precision == 1.)
458
- decimal: function (text) {
459
- let precision = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1e10;
460
- const normal = function (text) {
249
+ decimal: function (text, precision = 1e10) {
250
+ const normal = function normal(text) {
461
251
  text = String(text).trim();
462
- const match = text
463
- // Replace unicode minus sign with hyphen
464
- .replace(/\u2212/, "-")
465
- // Remove space after +, -
466
- .replace(/([+-])\s+/g, "$1")
467
- // Extract integer, numerator and denominator. If
468
- // commas or spaces are used, they must be in the
469
- // "correct" places
470
- .match(/^([+-]?(?:\d{1,3}(?:[, ]?\d{3})*\.?|\d{0,3}(?:[, ]?\d{3})*\.(?:\d{3}[, ]?)*\d{1,3}))$/);
471
-
472
- // You can't start a number with `0,`, to prevent us
473
- // interpeting '0.342' as correct for '342'
252
+ const match = text.replace(/\u2212/, "-").replace(/([+-])\s+/g, "$1").match(/^([+-]?(?:\d{1,3}(?:[, ]?\d{3})*\.?|\d{0,3}(?:[, ]?\d{3})*\.(?:\d{3}[, ]?)*\d{1,3}))$/);
474
253
  const badLeadingZero = text.match(/^0[0,]*,/);
475
254
  if (match && !badLeadingZero) {
476
255
  let x = parseFloat(match[1].replace(/[, ]/g, ""));
@@ -480,7 +259,7 @@ const KhanAnswerTypes = {
480
259
  return x;
481
260
  }
482
261
  };
483
- const commas = function (text) {
262
+ const commas = function commas(text) {
484
263
  text = text.replace(/([\.,])/g, function (_, c) {
485
264
  return c === "." ? "," : ".";
486
265
  });
@@ -495,11 +274,7 @@ const KhanAnswerTypes = {
495
274
  }];
496
275
  }
497
276
  };
498
-
499
- // validator function
500
277
  return function (guess) {
501
- // The fallback variable is used in place of the answer, if no
502
- // answer is provided (i.e. the field is left blank)
503
278
  const fallback = options.fallback != null ? "" + options.fallback : "";
504
279
  guess = String(guess).trim() || fallback;
505
280
  const score = {
@@ -508,30 +283,18 @@ const KhanAnswerTypes = {
508
283
  message: null,
509
284
  guess: guess
510
285
  };
511
-
512
- // iterate over all the acceptable forms, and if one of the
513
- // answers is correct, return true
514
286
  acceptableForms.forEach(form => {
515
287
  const transformed = forms[form](guess);
516
288
  for (let j = 0, l = transformed.length; j < l; j++) {
517
289
  const val = transformed[j].value;
518
290
  const exact = transformed[j].exact;
519
291
  const piApprox = transformed[j].piApprox;
520
- // If a string was returned, and it exactly matches,
521
- // return true
522
292
  if (predicate(val, options.maxError)) {
523
- // If the exact correct number was returned,
524
- // return true
525
293
  if (exact || options.simplify === "optional") {
526
294
  score.correct = true;
527
295
  score.message = options.message || null;
528
- // If the answer is correct, don't say it's
529
- // empty. This happens, for example, with the
530
- // coefficient type where guess === "" but is
531
- // interpreted as "1" which is correct.
532
296
  score.empty = false;
533
297
  } else if (form === "percent") {
534
- // Otherwise, an error was returned
535
298
  score.empty = true;
536
299
  score.message = ErrorCodes.MISSING_PERCENT_ERROR;
537
300
  } else {
@@ -540,9 +303,6 @@ const KhanAnswerTypes = {
540
303
  }
541
304
  score.message = ErrorCodes.NEEDS_TO_BE_SIMPLIFIED_ERROR;
542
305
  }
543
- // The return false below stops the looping of the
544
- // callback since predicate check succeeded.
545
- // No more forms to look to verify the user guess.
546
306
  return false;
547
307
  }
548
308
  if (piApprox && predicate(val, Math.abs(val * 0.001))) {
@@ -553,9 +313,9 @@ const KhanAnswerTypes = {
553
313
  });
554
314
  if (score.correct === false) {
555
315
  let interpretedGuess = false;
556
- ___default["default"].each(forms, function (form) {
557
- const anyAreNaN = ___default["default"].any(form(guess), function (t) {
558
- return t.value != null && !___default["default"].isNaN(t.value);
316
+ _.each(forms, function (form) {
317
+ const anyAreNaN = _.any(form(guess), function (t) {
318
+ return t.value != null && !_.isNaN(t.value);
559
319
  });
560
320
  if (anyAreNaN) {
561
321
  interpretedGuess = true;
@@ -571,83 +331,26 @@ const KhanAnswerTypes = {
571
331
  };
572
332
  }
573
333
  },
574
- /*
575
- * number answer type
576
- *
577
- * wraps the predicate answer type to performs simple number-based checking
578
- * of a solution
579
- */
580
334
  number: {
581
335
  convertToPredicate: function (correctAnswer, options) {
582
336
  const correctFloat = parseFloat(correctAnswer);
583
337
  return [function (guess, maxError) {
584
338
  return Math.abs(guess - correctFloat) < maxError;
585
- }, {
586
- ...options,
339
+ }, _extends({}, options, {
587
340
  type: "predicate"
588
- }];
341
+ })];
589
342
  },
590
343
  createValidatorFunctional: function (correctAnswer, options) {
591
344
  return KhanAnswerTypes.predicate.createValidatorFunctional(...KhanAnswerTypes.number.convertToPredicate(correctAnswer, options));
592
345
  }
593
346
  },
594
- /*
595
- * The expression answer type parses a given expression or equation
596
- * and semantically compares it to the solution. In addition, instant
597
- * feedback is provided by rendering the last answer that fully parsed.
598
- *
599
- * Parsing options:
600
- * functions (e.g. data-functions="f g h")
601
- * A space or comma separated list of single-letter variables that
602
- * should be interpreted as functions. Case sensitive. "e" and "i"
603
- * are reserved.
604
- *
605
- * no functions specified: f(x+y) == fx + fy
606
- * with "f" as a function: f(x+y) != fx + fy
607
- *
608
- * Comparison options:
609
- * same-form (e.g. data-same-form)
610
- * If present, the answer must match the solution's structure in
611
- * addition to evaluating the same. Commutativity and excess negation
612
- * are ignored, but all other changes will trigger a rejection. Useful
613
- * for requiring a particular form of an equation, or if the answer
614
- * must be factored.
615
- *
616
- * example question: Factor x^2 + x - 2
617
- * example solution: (x-1)(x+2)
618
- * accepted answers: (x-1)(x+2), (x+2)(x-1), ---(-x-2)(-1+x), etc.
619
- * rejected answers: x^2+x-2, x*x+x-2, x(x+1)-2, (x-1)(x+2)^1, etc.
620
- * rejection message: Your answer is not in the correct form
621
- *
622
- * simplify (e.g. data-simplify)
623
- * If present, the answer must be fully expanded and simplified. Use
624
- * carefully - simplification is hard and there may be bugs, or you
625
- * might not agree on the definition of "simplified" used. You will
626
- * get an error if the provided solution is not itself fully expanded
627
- * and simplified.
628
- *
629
- * example question: Simplify ((n*x^5)^5) / (n^(-2)*x^2)^-3
630
- * example solution: x^31 / n
631
- * accepted answers: x^31 / n, x^31 / n^1, x^31 * n^(-1), etc.
632
- * rejected answers: (x^25 * n^5) / (x^(-6) * n^6), etc.
633
- * rejection message: Your answer is not fully expanded and simplified
634
- *
635
- * Rendering options:
636
- * times (e.g. data-times)
637
- * If present, explicit multiplication (such as between numbers) will
638
- * be rendered with a cross/x symbol (TeX: \times) instead of the usual
639
- * center dot (TeX: \cdot).
640
- *
641
- * normal rendering: 2 * 3^x -> 2 \cdot 3^{x}
642
- * but with "times": 2 * 3^x -> 2 \times 3^{x}
643
- */
644
347
  expression: {
645
348
  parseSolution: function (solutionString, options) {
646
- let solution = KAS__namespace.parse(solutionString, options);
349
+ let solution = KAS.parse(solutionString, options);
647
350
  if (!solution.parsed) {
648
- throw new perseusCore.PerseusError("The provided solution (" + solutionString + ") didn't parse.", perseusCore.Errors.InvalidInput);
351
+ throw new PerseusError("The provided solution (" + solutionString + ") didn't parse.", Errors.InvalidInput);
649
352
  } else if (options.simplified && !solution.expr.isSimplified()) {
650
- throw new perseusCore.PerseusError("The provided solution (" + solutionString + ") isn't fully expanded and simplified.", perseusCore.Errors.InvalidInput);
353
+ throw new PerseusError("The provided solution (" + solutionString + ") isn't fully expanded and simplified.", Errors.InvalidInput);
651
354
  } else {
652
355
  solution = solution.expr;
653
356
  }
@@ -660,80 +363,37 @@ const KhanAnswerTypes = {
660
363
  correct: false,
661
364
  message: null,
662
365
  guess: guess,
663
- // Setting `ungraded` to true indicates that if the
664
- // guess doesn't match any of the solutions, the guess
665
- // shouldn't be marked as incorrect; instead, `message`
666
- // should be shown to the user. This is different from
667
- // setting `empty` to true, since the behavior of `empty`
668
- // is that `message` only will be shown if the guess is
669
- // graded as empty for every solution.
670
366
  ungraded: false
671
367
  };
672
-
673
- // Don't bother parsing an empty input
674
368
  if (!guess) {
675
- // @ts-expect-error - TS2540 - Cannot assign to 'empty' because it is a read-only property.
676
369
  score.empty = true;
677
370
  return score;
678
371
  }
679
- const answer = KAS__namespace.parse(guess, options);
680
-
681
- // An unsuccessful parse doesn't count as wrong
372
+ const answer = KAS.parse(guess, options);
682
373
  if (!answer.parsed) {
683
- // @ts-expect-error - TS2540 - Cannot assign to 'empty' because it is a read-only property.
684
374
  score.empty = true;
685
375
  return score;
686
376
  }
687
-
688
- // Solution will need to be parsed again if we're creating
689
- // this from a multiple question type
690
377
  if (typeof solution === "string") {
691
378
  solution = KhanAnswerTypes.expression.parseSolution(solution, options);
692
379
  }
693
- const result = KAS__namespace.compare(answer.expr, solution, options);
380
+ const result = KAS.compare(answer.expr, solution, options);
694
381
  if (result.equal) {
695
- // Correct answer
696
- // @ts-expect-error - TS2540 - Cannot assign to 'correct' because it is a read-only property.
697
382
  score.correct = true;
698
383
  } else if (result.wrongVariableNames || result.wrongVariableCase) {
699
- // We don't want to give people an error for getting the
700
- // variable names or the variable case wrong.
701
- // TODO(aasmund): This should ideally have been handled
702
- // under the `result.message` condition, but the
703
- // KAS messages currently aren't translatable.
704
- // @ts-expect-error - TS2540 - Cannot assign to 'ungraded' because it is a read-only property.
705
384
  score.ungraded = true;
706
- // @ts-expect-error - TS2540 - Cannot assign to 'message' because it is a read-only property.
707
385
  score.message = result.wrongVariableCase ? ErrorCodes.WRONG_CASE_ERROR : ErrorCodes.WRONG_LETTER_ERROR;
708
- // Don't tell the use they're "almost there" in this case, that may not be true and isn't helpful.
709
- // @ts-expect-error - TS2339 - Property 'suppressAlmostThere' does not exist on type '{ readonly empty: false; readonly correct: false; readonly message: string | null | undefined; readonly guess: any; readonly ungraded: false; }'.
710
386
  score.suppressAlmostThere = true;
711
387
  } else if (result.message) {
712
- // Nearly correct answer
713
- // TODO(aasmund): This message also isn't translatable;
714
- // need to fix that in KAS
715
- // @ts-expect-error - TS2540 - Cannot assign to 'message' because it is a read-only property.
716
388
  score.message = result.message;
717
389
  } else {
718
- // Replace x with * and see if it would have been correct
719
- // TODO(aasmund): I think this branch is effectively dead,
720
- // because the replacement will only work in situations
721
- // where the variables are wrong (except if the variable
722
- // is x, in which case the replacement won't work either),
723
- // which is handled by another branch. When we implement a
724
- // more sophisticated variable check, revive this or
725
- // remove it completely if it will never come into play.
726
- const answerX = KAS__namespace.parse(guess.replace(/[xX]/g, "*"), options);
390
+ const answerX = KAS.parse(guess.replace(/[xX]/g, "*"), options);
727
391
  if (answerX.parsed) {
728
- const resultX = KAS__namespace.compare(answerX.expr, solution, options);
392
+ const resultX = KAS.compare(answerX.expr, solution, options);
729
393
  if (resultX.equal) {
730
- // @ts-expect-error - TS2540 - Cannot assign to 'ungraded' because it is a read-only property.
731
394
  score.ungraded = true;
732
- // @ts-expect-error - TS2540 - Cannot assign to 'message' because it is a read-only property.
733
395
  score.message = ErrorCodes.MULTIPLICATION_SIGN_ERROR;
734
396
  } else if (resultX.message) {
735
- // TODO(aasmund): I18nize `score.message`
736
- // @ts-expect-error - TS2540 - Cannot assign to 'message' because it is a read-only property.
737
397
  score.message = resultX.message + " Also, I'm a computer. I only understand " + "multiplication if you use an " + "asterisk (*) as the multiplication " + "sign.";
738
398
  }
739
399
  }
@@ -759,28 +419,18 @@ function scoreCategorizer(userInput, rubric) {
759
419
  };
760
420
  }
761
421
 
762
- /**
763
- * Checks userInput from the categorizer widget to see if the user has selected
764
- * a category for each item.
765
- * @param userInput - The user's input corresponding to an array of indices that
766
- * represent the selected category for each row/item.
767
- * @param validationData - An array of strings corresponding to each row/item
768
- * @param strings - Used to provide a validation message
769
- */
770
422
  function validateCategorizer(userInput, validationData) {
771
423
  const incomplete = validationData.items.some((_, i) => userInput.values[i] == null);
772
424
  if (incomplete) {
773
425
  return {
774
426
  type: "invalid",
775
- message: perseusScore.ErrorCodes.INVALID_SELECTION_ERROR
427
+ message: ErrorCodes.INVALID_SELECTION_ERROR
776
428
  };
777
429
  }
778
430
  return null;
779
431
  }
780
432
 
781
433
  function scoreCSProgram(userInput) {
782
- // The CS program can tell us whether it's correct or incorrect,
783
- // and pass an optional message
784
434
  if (userInput.status === "correct") {
785
435
  return {
786
436
  type: "points",
@@ -813,10 +463,6 @@ function scoreDropdown(userInput, rubric) {
813
463
  };
814
464
  }
815
465
 
816
- /**
817
- * Checks if the user has selected an item from the dropdown before scoring.
818
- * This is shown with a userInput value / index other than 0.
819
- */
820
466
  function validateDropdown(userInput) {
821
467
  if (userInput.value === 0) {
822
468
  return {
@@ -827,63 +473,25 @@ function validateDropdown(userInput) {
827
473
  return null;
828
474
  }
829
475
 
830
- /* Content creators input a list of answers which are matched from top to
831
- * bottom. The intent is that they can include spcific solutions which should
832
- * be graded as correct or incorrect (or ungraded!) first, then get more
833
- * general.
834
- *
835
- * We iterate through each answer, trying to match it with the user's input
836
- * using the following angorithm:
837
- * - Try to parse the user's input. If it doesn't parse then return "not
838
- * graded".
839
- * - For each answer:
840
- * ~ Try to validate the user's input against the answer. The answer is
841
- * expected to parse.
842
- * ~ If the user's input validates (the validator judges it "correct"), we've
843
- * matched and can stop considering answers.
844
- * - If there were no matches or the matching answer is considered "ungraded",
845
- * show the user an error. TODO(joel) - what error?
846
- * - Otherwise, pass through the resulting points and message.
847
- */
848
476
  function scoreExpression(userInput, rubric, locale) {
849
- const options = ___default["default"].clone(rubric);
850
- ___default["default"].extend(options, {
851
- decimal_separator: perseusCore.getDecimalSeparator(locale)
477
+ const options = _.clone(rubric);
478
+ _.extend(options, {
479
+ decimal_separator: getDecimalSeparator(locale)
852
480
  });
853
481
  const createValidator = answer => {
854
- // We give options to KAS.parse here because it is parsing the
855
- // solution answer, not the student answer, and we don't want a
856
- // solution to work if the student is using a different language
857
- // (different from the content creation language, ie. English).
858
- const expression = KAS__namespace.parse(answer.value, rubric);
859
- // An answer may not be parsed if the expression was defined
860
- // incorrectly. For example if the answer is using a symbol defined
861
- // in the function variables list for the expression.
482
+ const expression = KAS.parse(answer.value, rubric);
862
483
  if (!expression.parsed) {
863
- /* c8 ignore next */
864
- throw new perseusCore.PerseusError("Unable to parse solution answer for expression", perseusCore.Errors.InvalidInput, {
484
+ throw new PerseusError("Unable to parse solution answer for expression", Errors.InvalidInput, {
865
485
  metadata: {
866
486
  rubric: JSON.stringify(rubric)
867
487
  }
868
488
  });
869
489
  }
870
- return KhanAnswerTypes.expression.createValidatorFunctional(expression.expr, ___default["default"]({}).extend(options, {
490
+ return KhanAnswerTypes.expression.createValidatorFunctional(expression.expr, _({}).extend(options, {
871
491
  simplify: answer.simplify,
872
492
  form: answer.form
873
493
  }));
874
494
  };
875
-
876
- // Find the first answer form that matches the user's input and that
877
- // is considered correct. Also, track whether the input is
878
- // considered "empty" for all answer forms, and keep the validation
879
- // result for the first answer form for which the user's input was
880
- // considered "ungraded".
881
- // (Terminology reminder: the answer forms are provided by the
882
- // assessment items; they are not the user's input. Each one might
883
- // represent a correct answer, an incorrect one (if the exercise
884
- // creator has predicted certain common wrong answers and wants to
885
- // provide guidance via a message), or an ungraded one (same idea,
886
- // but without giving the user an incorrect mark for the question).
887
495
  let matchingAnswerForm;
888
496
  let matchMessage;
889
497
  let allEmpty = true;
@@ -894,34 +502,18 @@ function scoreExpression(userInput, rubric, locale) {
894
502
  continue;
895
503
  }
896
504
  const result = validator(userInput);
897
-
898
- // Short-circuit as soon as the user's input matches some answer
899
- // (independently of whether the answer is correct)
900
505
  if (result.correct) {
901
506
  matchingAnswerForm = answerForm;
902
507
  matchMessage = result.message || "";
903
508
  break;
904
509
  }
905
510
  allEmpty = allEmpty && result.empty;
906
- // If this answer form is correct and the user's input is considered
907
- // "ungraded" for it, we'll want to keep the evaluation result for
908
- // later. If the user's input doesn't match any answer forms, we'll
909
- // show the message from this validation.
910
511
  if (answerForm.considered === "correct" && result.ungraded && !firstUngradedResult) {
911
512
  firstUngradedResult = result;
912
513
  }
913
514
  }
914
-
915
- // Now check to see if we matched any answer form at all, and if
916
- // we did, whether it's considered correct, incorrect, or ungraded
917
515
  if (!matchingAnswerForm) {
918
516
  if (firstUngradedResult) {
919
- // While we didn't directly match with any answer form, we
920
- // did at some point get an "ungraded" validation result,
921
- // which might indicate e.g. a mismatch in variable casing.
922
- // We'll return "invalid", which will let the user try again
923
- // with no penalty, and the hopefully helpful validation
924
- // message.
925
517
  return {
926
518
  type: "invalid",
927
519
  message: firstUngradedResult.message,
@@ -929,14 +521,11 @@ function scoreExpression(userInput, rubric, locale) {
929
521
  };
930
522
  }
931
523
  if (allEmpty) {
932
- // If everything graded as empty, it's invalid.
933
524
  return {
934
525
  type: "invalid",
935
526
  message: null
936
527
  };
937
528
  }
938
- // We fell through all the possibilities and we're not empty,
939
- // so the answer is considered incorrect.
940
529
  return {
941
530
  type: "points",
942
531
  earned: 0,
@@ -949,9 +538,6 @@ function scoreExpression(userInput, rubric, locale) {
949
538
  message: matchMessage
950
539
  };
951
540
  }
952
- // We matched a graded answer form, so we can now tell the user
953
- // whether their input was correct or incorrect, and hand out
954
- // points accordingly
955
541
  return {
956
542
  type: "points",
957
543
  earned: matchingAnswerForm.considered === "correct" ? 1 : 0,
@@ -960,14 +546,6 @@ function scoreExpression(userInput, rubric, locale) {
960
546
  };
961
547
  }
962
548
 
963
- /**
964
- * Checks user input from the expression widget to see if it is scorable.
965
- *
966
- * Note: Most of the expression widget's validation requires the Rubric because
967
- * of its use of KhanAnswerTypes as a core part of scoring.
968
- *
969
- * @see `scoreExpression()` for more details.
970
- */
971
549
  function validateExpression(userInput) {
972
550
  if (userInput === "") {
973
551
  return {
@@ -983,13 +561,13 @@ function getCoefficientsByType(data) {
983
561
  return undefined;
984
562
  }
985
563
  if (data.type === "exponential" || data.type === "logarithm") {
986
- const grader = perseusCore.GrapherUtil.functionForType(data.type);
564
+ const grader = GrapherUtil.functionForType(data.type);
987
565
  return grader.getCoefficients(data.coords, data.asymptote);
988
566
  } else if (data.type === "linear" || data.type === "quadratic" || data.type === "absolute_value" || data.type === "sinusoid" || data.type === "tangent") {
989
- const grader = perseusCore.GrapherUtil.functionForType(data.type);
567
+ const grader = GrapherUtil.functionForType(data.type);
990
568
  return grader.getCoefficients(data.coords);
991
569
  } else {
992
- throw new perseusCore.PerseusError("Invalid grapher type", perseusCore.Errors.InvalidInput);
570
+ throw new PerseusError("Invalid grapher type", Errors.InvalidInput);
993
571
  }
994
572
  }
995
573
  function scoreGrapher(userInput, rubric) {
@@ -1001,17 +579,13 @@ function scoreGrapher(userInput, rubric) {
1001
579
  message: null
1002
580
  };
1003
581
  }
1004
-
1005
- // We haven't moved the coords
1006
582
  if (userInput.coords == null) {
1007
583
  return {
1008
584
  type: "invalid",
1009
585
  message: null
1010
586
  };
1011
587
  }
1012
-
1013
- // Get new function handler for grading
1014
- const grader = perseusCore.GrapherUtil.functionForType(userInput.type);
588
+ const grader = GrapherUtil.functionForType(userInput.type);
1015
589
  const guessCoeffs = getCoefficientsByType(userInput);
1016
590
  const correctCoeffs = getCoefficientsByType(rubric.correct);
1017
591
  if (guessCoeffs == null || correctCoeffs == null) {
@@ -1036,10 +610,7 @@ function scoreGrapher(userInput, rubric) {
1036
610
  };
1037
611
  }
1038
612
 
1039
- // TODO: merge this with scoreCSProgram, it's the same code
1040
613
  function scoreIframe(userInput) {
1041
- // The iframe can tell us whether it's correct or incorrect,
1042
- // and pass an optional message
1043
614
  if (userInput.status === "correct") {
1044
615
  return {
1045
616
  type: "points",
@@ -1067,16 +638,15 @@ const {
1067
638
  canonicalSineCoefficients,
1068
639
  similar,
1069
640
  clockwise
1070
- } = kmath.geometry;
641
+ } = geometry;
1071
642
  const {
1072
643
  getClockwiseAngle
1073
- } = kmath.angles;
644
+ } = angles;
1074
645
  const {
1075
646
  getSinusoidCoefficients,
1076
647
  getQuadraticCoefficients
1077
- } = kmath.coefficients;
648
+ } = coefficients;
1078
649
  function scoreInteractiveGraph(userInput, rubric) {
1079
- // None-type graphs are not graded
1080
650
  if (userInput.type === "none" && rubric.correct.type === "none") {
1081
651
  return {
1082
652
  type: "points",
@@ -1085,22 +655,11 @@ function scoreInteractiveGraph(userInput, rubric) {
1085
655
  message: null
1086
656
  };
1087
657
  }
1088
-
1089
- // When nothing has moved, there will neither be coords nor the
1090
- // circle's center/radius fields. When those fields are absent, skip
1091
- // all these checks; just go mark the answer as empty.
1092
- const hasValue = Boolean(
1093
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
1094
- userInput.coords ||
1095
- // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
1096
- userInput.center && userInput.radius);
658
+ const hasValue = Boolean(userInput.coords || userInput.center && userInput.radius);
1097
659
  if (userInput.type === rubric.correct.type && hasValue) {
1098
660
  if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
1099
661
  const guess = userInput.coords;
1100
662
  const correct = rubric.correct.coords;
1101
-
1102
- // If both of the guess points are on the correct line, it's
1103
- // correct.
1104
663
  if (collinear(correct[0], correct[1], guess[0]) && collinear(correct[0], correct[1], guess[1])) {
1105
664
  return {
1106
665
  type: "points",
@@ -1121,10 +680,9 @@ function scoreInteractiveGraph(userInput, rubric) {
1121
680
  };
1122
681
  }
1123
682
  } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
1124
- // If the parabola coefficients match, it's correct.
1125
683
  const guessCoeffs = getQuadraticCoefficients(userInput.coords);
1126
684
  const correctCoeffs = getQuadraticCoefficients(rubric.correct.coords);
1127
- if (perseusCore.approximateDeepEqual(guessCoeffs, correctCoeffs)) {
685
+ if (approximateDeepEqual(guessCoeffs, correctCoeffs)) {
1128
686
  return {
1129
687
  type: "points",
1130
688
  earned: 1,
@@ -1137,8 +695,7 @@ function scoreInteractiveGraph(userInput, rubric) {
1137
695
  const correctCoeffs = getSinusoidCoefficients(rubric.correct.coords);
1138
696
  const canonicalGuessCoeffs = canonicalSineCoefficients(guessCoeffs);
1139
697
  const canonicalCorrectCoeffs = canonicalSineCoefficients(correctCoeffs);
1140
- // If the canonical coefficients match, it's correct.
1141
- if (perseusCore.approximateDeepEqual(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
698
+ if (approximateDeepEqual(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
1142
699
  return {
1143
700
  type: "points",
1144
701
  earned: 1,
@@ -1147,7 +704,7 @@ function scoreInteractiveGraph(userInput, rubric) {
1147
704
  };
1148
705
  }
1149
706
  } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
1150
- if (perseusCore.approximateDeepEqual(userInput.center, rubric.correct.center) && perseusCore.approximateEqual(userInput.radius, rubric.correct.radius)) {
707
+ if (approximateDeepEqual(userInput.center, rubric.correct.center) && approximateEqual(userInput.radius, rubric.correct.radius)) {
1151
708
  return {
1152
709
  type: "points",
1153
710
  earned: 1,
@@ -1162,14 +719,9 @@ function scoreInteractiveGraph(userInput, rubric) {
1162
719
  }
1163
720
  const guess = userInput.coords.slice();
1164
721
  correct = correct.slice();
1165
- // Everything's already rounded so we shouldn't need to do an
1166
- // eq() comparison but _.isEqual(0, -0) is false, so we'll use
1167
- // eq() anyway. The sort should be fine because it'll stringify
1168
- // it and -0 converted to a string is "0"
1169
- guess?.sort();
1170
- // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'.
722
+ guess == null || guess.sort();
1171
723
  correct.sort();
1172
- if (perseusCore.approximateDeepEqual(guess, correct)) {
724
+ if (approximateDeepEqual(guess, correct)) {
1173
725
  return {
1174
726
  type: "points",
1175
727
  earned: 1,
@@ -1184,14 +736,13 @@ function scoreInteractiveGraph(userInput, rubric) {
1184
736
  if (rubric.correct.match === "similar") {
1185
737
  match = similar(guess, correct, Number.POSITIVE_INFINITY);
1186
738
  } else if (rubric.correct.match === "congruent") {
1187
- match = similar(guess, correct, kmath.number.DEFAULT_TOLERANCE);
739
+ match = similar(guess, correct, number.DEFAULT_TOLERANCE);
1188
740
  } else if (rubric.correct.match === "approx") {
1189
741
  match = similar(guess, correct, 0.1);
1190
742
  } else {
1191
- /* exact */
1192
743
  guess.sort();
1193
744
  correct.sort();
1194
- match = perseusCore.approximateDeepEqual(guess, correct);
745
+ match = approximateDeepEqual(guess, correct);
1195
746
  }
1196
747
  if (match) {
1197
748
  return {
@@ -1202,11 +753,11 @@ function scoreInteractiveGraph(userInput, rubric) {
1202
753
  };
1203
754
  }
1204
755
  } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
1205
- let guess = perseusCore.deepClone(userInput.coords);
1206
- let correct = perseusCore.deepClone(rubric.correct.coords);
1207
- guess = ___default["default"].invoke(guess, "sort").sort();
1208
- correct = ___default["default"].invoke(correct, "sort").sort();
1209
- if (perseusCore.approximateDeepEqual(guess, correct)) {
756
+ let guess = deepClone(userInput.coords);
757
+ let correct = deepClone(rubric.correct.coords);
758
+ guess = _.invoke(guess, "sort").sort();
759
+ correct = _.invoke(correct, "sort").sort();
760
+ if (approximateDeepEqual(guess, correct)) {
1210
761
  return {
1211
762
  type: "points",
1212
763
  earned: 1,
@@ -1217,7 +768,7 @@ function scoreInteractiveGraph(userInput, rubric) {
1217
768
  } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
1218
769
  const guess = userInput.coords;
1219
770
  const correct = rubric.correct.coords;
1220
- if (perseusCore.approximateDeepEqual(guess[0], correct[0]) && collinear(correct[0], correct[1], guess[1])) {
771
+ if (approximateDeepEqual(guess[0], correct[0]) && collinear(correct[0], correct[1], guess[1])) {
1221
772
  return {
1222
773
  type: "points",
1223
774
  earned: 1,
@@ -1229,38 +780,27 @@ function scoreInteractiveGraph(userInput, rubric) {
1229
780
  const coords = userInput.coords;
1230
781
  const correct = rubric.correct.coords;
1231
782
  const allowReflexAngles = rubric.correct.allowReflexAngles;
1232
-
1233
- // While the angle graph should always have 3 points, our types
1234
- // technically allow for null values. We'll check for that here.
1235
- // TODO: (LEMS-2857) We would like to update the type of coords
1236
- // to be non-nullable, as the graph should always have 3 points.
1237
783
  if (!coords) {
1238
784
  return {
1239
785
  type: "invalid",
1240
786
  message: null
1241
787
  };
1242
788
  }
1243
-
1244
- // We need to check both the direction of the angle and the
1245
- // whether the graph allows for reflexive angles in order to
1246
- // to determine if we need to reverse the coords for scoring.
1247
789
  const areClockwise = clockwise([coords[0], coords[2], coords[1]]);
1248
790
  const shouldReverseCoords = areClockwise && !allowReflexAngles;
1249
791
  const guess = shouldReverseCoords ? coords.slice().reverse() : coords;
1250
792
  let match;
1251
793
  if (rubric.correct.match === "congruent") {
1252
- const angles = ___default["default"].map([guess, correct], function (coords) {
794
+ const angles = _.map([guess, correct], function (coords) {
1253
795
  if (!coords) {
1254
796
  return false;
1255
797
  }
1256
798
  const angle = getClockwiseAngle(coords, allowReflexAngles);
1257
799
  return angle;
1258
800
  });
1259
- // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
1260
- match = perseusCore.approximateEqual(...angles);
801
+ match = approximateEqual(...angles);
1261
802
  } else {
1262
- /* exact */
1263
- match = perseusCore.approximateDeepEqual(guess[1], correct[1]) && collinear(correct[1], correct[0], guess[0]) && collinear(correct[1], correct[2], guess[2]);
803
+ match = approximateDeepEqual(guess[1], correct[1]) && collinear(correct[1], correct[0], guess[0]) && collinear(correct[1], correct[2], guess[2]);
1264
804
  }
1265
805
  if (match) {
1266
806
  return {
@@ -1272,11 +812,7 @@ function scoreInteractiveGraph(userInput, rubric) {
1272
812
  }
1273
813
  }
1274
814
  }
1275
-
1276
- // The input wasn't correct, so check if it's a blank input or if it's
1277
- // actually just wrong
1278
- if (!hasValue || ___default["default"].isEqual(userInput, rubric.graph)) {
1279
- // We're where we started.
815
+ if (!hasValue || _.isEqual(userInput, rubric.graph)) {
1280
816
  return {
1281
817
  type: "invalid",
1282
818
  message: null
@@ -1290,8 +826,6 @@ function scoreInteractiveGraph(userInput, rubric) {
1290
826
  };
1291
827
  }
1292
828
 
1293
- // Question state for marker as result of user selected answers.
1294
-
1295
829
  function scoreLabelImageMarker(userInput, rubric) {
1296
830
  const score = {
1297
831
  hasAnswers: false,
@@ -1302,11 +836,9 @@ function scoreLabelImageMarker(userInput, rubric) {
1302
836
  }
1303
837
  if (rubric.length > 0) {
1304
838
  if (userInput && userInput.length === rubric.length) {
1305
- // All correct answers are selected by the user.
1306
839
  score.isCorrect = userInput.every(choice => rubric.includes(choice));
1307
840
  }
1308
841
  } else if (!userInput || userInput.length === 0) {
1309
- // Correct as no answers should be selected by the user.
1310
842
  score.isCorrect = true;
1311
843
  }
1312
844
  return score;
@@ -1321,8 +853,6 @@ function scoreLabelImage(userInput, rubric) {
1321
853
  }
1322
854
  return {
1323
855
  type: "points",
1324
- // Markers with no expected answers are graded as correct if user
1325
- // makes no answer selection.
1326
856
  earned: numCorrect === userInput.markers.length ? 1 : 0,
1327
857
  total: 1,
1328
858
  message: null
@@ -1330,7 +860,7 @@ function scoreLabelImage(userInput, rubric) {
1330
860
  }
1331
861
 
1332
862
  function scoreMatcher(userInput, rubric) {
1333
- const correct = ___default["default"].isEqual(userInput.left, rubric.left) && ___default["default"].isEqual(userInput.right, rubric.right);
863
+ const correct = _.isEqual(userInput.left, rubric.left) && _.isEqual(userInput.right, rubric.right);
1334
864
  return {
1335
865
  type: "points",
1336
866
  earned: correct ? 1 : 0,
@@ -1342,23 +872,20 @@ function scoreMatcher(userInput, rubric) {
1342
872
  function scoreMatrix(userInput, rubric) {
1343
873
  const solution = rubric.answers;
1344
874
  const supplied = userInput.answers;
1345
- const solutionSize = perseusCore.getMatrixSize(solution);
1346
- const suppliedSize = perseusCore.getMatrixSize(supplied);
875
+ const solutionSize = getMatrixSize(solution);
876
+ const suppliedSize = getMatrixSize(supplied);
1347
877
  const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
1348
878
  const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
1349
879
  let message = null;
1350
880
  let incorrect = false;
1351
- ___default["default"](suppliedSize[0]).times(row => {
1352
- ___default["default"](suppliedSize[1]).times(col => {
881
+ _(suppliedSize[0]).times(row => {
882
+ _(suppliedSize[1]).times(col => {
1353
883
  if (!incorrectSize) {
1354
- const validator = createValidator(
1355
- // @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
1356
- solution[row][col], {
884
+ const validator = createValidator(solution[row][col], {
1357
885
  simplify: true
1358
886
  });
1359
887
  const result = validator(supplied[row][col]);
1360
888
  if (result.message) {
1361
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
1362
889
  message = result.message;
1363
890
  }
1364
891
  if (!result.correct) {
@@ -1383,17 +910,9 @@ function scoreMatrix(userInput, rubric) {
1383
910
  };
1384
911
  }
1385
912
 
1386
- /**
1387
- * Checks user input from the matrix widget to see if it is scorable.
1388
- *
1389
- * Note: The matrix widget cannot do much validation without the Scoring
1390
- * Data because of its use of KhanAnswerTypes as a core part of scoring.
1391
- *
1392
- * @see `scoreMatrix()` for more details.
1393
- */
1394
913
  function validateMatrix(userInput) {
1395
914
  const supplied = userInput.answers;
1396
- const suppliedSize = perseusCore.getMatrixSize(supplied);
915
+ const suppliedSize = getMatrixSize(supplied);
1397
916
  for (let row = 0; row < suppliedSize[0]; row++) {
1398
917
  for (let col = 0; col < suppliedSize[1]; col++) {
1399
918
  if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
@@ -1412,7 +931,7 @@ function scoreNumberLine(userInput, rubric) {
1412
931
  const start = rubric.initialX != null ? rubric.initialX : range[0];
1413
932
  const startRel = rubric.isInequality ? "ge" : "eq";
1414
933
  const correctRel = rubric.correctRel || "eq";
1415
- const correctPos = kmath.number.equal(userInput.numLinePosition, rubric.correctX || 0);
934
+ const correctPos = number.equal(userInput.numLinePosition, rubric.correctX || 0);
1416
935
  if (correctPos && correctRel === userInput.rel) {
1417
936
  return {
1418
937
  type: "points",
@@ -1422,7 +941,6 @@ function scoreNumberLine(userInput, rubric) {
1422
941
  };
1423
942
  }
1424
943
  if (userInput.numLinePosition === start && userInput.rel === startRel) {
1425
- // We're where we started.
1426
944
  return {
1427
945
  type: "invalid",
1428
946
  message: null
@@ -1436,17 +954,9 @@ function scoreNumberLine(userInput, rubric) {
1436
954
  };
1437
955
  }
1438
956
 
1439
- /**
1440
- * Checks user input is within the allowed range and not the same as the initial
1441
- * state.
1442
- * @param userInput
1443
- * @see 'scoreNumberLine' for the scoring logic.
1444
- */
1445
957
  function validateNumberLine(userInput) {
1446
958
  const divisionRange = userInput.divisionRange;
1447
959
  const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
1448
-
1449
- // TODO: I don't think isTickCrtl is a thing anymore
1450
960
  if (userInput.isTickCrtl && outsideAllowedRange) {
1451
961
  return {
1452
962
  type: "invalid",
@@ -1456,18 +966,6 @@ function validateNumberLine(userInput) {
1456
966
  return null;
1457
967
  }
1458
968
 
1459
- /*
1460
- * In this file, an `expression` is some portion of valid TeX enclosed in
1461
- * curly brackets.
1462
- */
1463
-
1464
- /*
1465
- * Find the index at which an expression ends, i.e., has an unmatched
1466
- * closing curly bracket. This method assumes that we start with a non-open
1467
- * bracket character and end when we've seen more left than right brackets
1468
- * (rather than assuming that we start with a bracket character and wait for
1469
- * bracket equality).
1470
- */
1471
969
  function findEndpoint(tex, currentIndex) {
1472
970
  let bracketDepth = 0;
1473
971
  for (let i = currentIndex, len = tex.length; i < len; i++) {
@@ -1481,22 +979,10 @@ function findEndpoint(tex, currentIndex) {
1481
979
  return i;
1482
980
  }
1483
981
  }
1484
- // If we never see unbalanced curly brackets, default to the
1485
- // entire string
1486
982
  return tex.length;
1487
983
  }
1488
-
1489
- /*
1490
- * Parses an individual set of curly brackets into TeX.
1491
- */
1492
984
  function parseNextExpression(tex, currentIndex, handler) {
1493
- // Find the first '{' and grab subsequent TeX
1494
- // Ex) tex: '{3}{7}', and we want the '3'
1495
985
  const openBracketIndex = tex.indexOf("{", currentIndex);
1496
-
1497
- // If there is no open bracket, set the endpoint to the end of the string
1498
- // and the expression to an empty string. This helps ensure we don't
1499
- // get stuck in an infinite loop when users handtype TeX.
1500
986
  if (openBracketIndex === -1) {
1501
987
  return {
1502
988
  endpoint: tex.length,
@@ -1504,8 +990,6 @@ function parseNextExpression(tex, currentIndex, handler) {
1504
990
  };
1505
991
  }
1506
992
  const nextExpIndex = openBracketIndex + 1;
1507
-
1508
- // Truncate to only contain remaining TeX
1509
993
  const endpoint = findEndpoint(tex, nextExpIndex);
1510
994
  const expressionTeX = tex.substring(nextExpIndex, endpoint);
1511
995
  const parsedExp = walkTex(expressionTeX, handler);
@@ -1534,62 +1018,27 @@ function walkTex(tex, handler) {
1534
1018
  if (!tex) {
1535
1019
  return "";
1536
1020
  }
1537
-
1538
- // Ex) tex: '2 \dfrac {3}{7}'
1539
1021
  let parsedString = "";
1540
1022
  let currentIndex = 0;
1541
1023
  let nextFrac = getNextFracIndex(tex, currentIndex);
1542
-
1543
- // For each \dfrac, find the two expressions (wrapped in {}) and recur
1544
1024
  while (nextFrac > -1) {
1545
- // Gather first fragment, preceding \dfrac
1546
- // Ex) parsedString: '2 '
1547
1025
  parsedString += tex.substring(currentIndex, nextFrac);
1548
-
1549
- // Remove everything preceding \dfrac, which has been parsed
1550
1026
  currentIndex = nextFrac;
1551
-
1552
- // Parse first expression and move index past it
1553
- // Ex) firstParsedExpression.expression: '3'
1554
1027
  const firstParsedExpression = parseNextExpression(tex, currentIndex, handler);
1555
1028
  currentIndex = firstParsedExpression.endpoint + 1;
1556
-
1557
- // Parse second expression
1558
- // Ex) secondParsedExpression.expression: '7'
1559
1029
  const secondParsedExpression = parseNextExpression(tex, currentIndex, handler);
1560
1030
  currentIndex = secondParsedExpression.endpoint + 1;
1561
-
1562
- // Add expressions to running total of parsed expressions
1563
1031
  if (parsedString.length) {
1564
1032
  parsedString += " ";
1565
1033
  }
1566
-
1567
- // Apply a custom handler based on the parsed subexpressions
1568
1034
  parsedString += handler(firstParsedExpression.expression, secondParsedExpression.expression);
1569
-
1570
- // Find next DFrac, relative to currentIndex
1571
1035
  nextFrac = getNextFracIndex(tex, currentIndex);
1572
1036
  }
1573
-
1574
- // Add remaining TeX, which is \dfrac-free
1575
1037
  parsedString += tex.slice(currentIndex);
1576
1038
  return parsedString;
1577
1039
  }
1578
-
1579
- /*
1580
- * Parse a TeX expression into something interpretable by input-number.
1581
- * The process is concerned with: (1) parsing fractions, i.e., \dfracs; and
1582
- * (2) removing backslash-escaping from certain characters (right now, only
1583
- * percent signs).
1584
- *
1585
- * The basic algorithm for handling \dfracs splits on \dfracs and then recurs
1586
- * on the subsequent "expressions", i.e., the {} pairs that follow \dfrac. The
1587
- * recursion is to allow for nested \dfrac elements.
1588
- *
1589
- * Backslash-escapes are removed with a simple search-and-replace.
1590
- */
1591
1040
  function parseTex(tex) {
1592
- const handler = function (exp1, exp2) {
1041
+ const handler = function handler(exp1, exp2) {
1593
1042
  return exp1 + "/" + exp2;
1594
1043
  };
1595
1044
  const texWithoutFracs = walkTex(tex, handler);
@@ -1621,54 +1070,26 @@ const answerFormButtons = [{
1621
1070
  value: "pi",
1622
1071
  content: "\u03C0"
1623
1072
  }];
1624
-
1625
- // This function checks if the user inputted a percent value, parsing
1626
- // it as a number (and maybe scaling) so that it can be graded.
1627
- // NOTE(michaelpolyak): Unlike `KhanAnswerTypes.number.percent()` which
1628
- // can accept several input forms with or without "%", the decision
1629
- // to parse based on the presence of "%" in the input, is so that we
1630
- // don't accidently scale the user typed value before grading, CP-930.
1631
1073
  function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
1632
- // If the input value is not a string ending with "%", then there's
1633
- // nothing more to do. The value will be graded as inputted by user.
1634
1074
  if (!(typeof inputValue === "string" && inputValue.endsWith("%"))) {
1635
1075
  return inputValue;
1636
1076
  }
1637
1077
  const value = parseFloat(inputValue.slice(0, -1));
1638
- // If the input value stripped of the "%" cannot be parsed as a
1639
- // number (the slice is not really necessary for parseFloat to work
1640
- // if the string starts with a number) then return the original
1641
- // input for grading.
1642
1078
  if (isNaN(value)) {
1643
1079
  return inputValue;
1644
1080
  }
1645
-
1646
- // Next, if all correct answers are in the range of |0,1| then we
1647
- // scale the user typed value. We assume this is the correct thing
1648
- // to do since the input value ends with "%".
1649
1081
  if (normalizedAnswerExpected) {
1650
1082
  return value / 100;
1651
1083
  }
1652
-
1653
- // Otherwise, we return input value (number) stripped of the "%".
1654
1084
  return value;
1655
1085
  }
1656
1086
  function scoreNumericInput(userInput, rubric) {
1657
- const defaultAnswerForms = answerFormButtons.map(e => e["value"])
1658
- // Don't default to validating the answer as a pi answer
1659
- // if answerForm isn't set on the answer
1660
- // https://khanacademy.atlassian.net/browse/LC-691
1661
- .filter(e => e !== "pi");
1087
+ var _matchedAnswer$messag;
1088
+ const defaultAnswerForms = answerFormButtons.map(e => e["value"]).filter(e => e !== "pi");
1662
1089
  const createValidator = answer => {
1090
+ var _answer$answerForms;
1663
1091
  const stringAnswer = `${answer.value}`;
1664
-
1665
- // Always validate against the provided answer forms (pi, decimal, etc.)
1666
- const validatorForms = [...(answer.answerForms ?? [])];
1667
-
1668
- // When an answer is set to strict, we validate using ONLY
1669
- // the provided answerForms. If strict is false, or if there
1670
- // were no provided answer forms, we will include all
1671
- // of the default answer forms in our validator.
1092
+ const validatorForms = [...((_answer$answerForms = answer.answerForms) != null ? _answer$answerForms : [])];
1672
1093
  if (!answer.strict || validatorForms.length === 0) {
1673
1094
  validatorForms.push(...defaultAnswerForms);
1674
1095
  }
@@ -1676,18 +1097,12 @@ function scoreNumericInput(userInput, rubric) {
1676
1097
  message: answer.message,
1677
1098
  simplify: answer.status === "correct" ? answer.simplify : "optional",
1678
1099
  inexact: true,
1679
- // TODO(merlob) backfill / delete
1680
1100
  maxError: answer.maxError,
1681
1101
  forms: validatorForms
1682
1102
  });
1683
1103
  };
1684
-
1685
- // We may have received TeX; try to parse it before grading.
1686
- // If `currentValue` is not TeX, this should be a no-op.
1687
1104
  const currentValue = parseTex(userInput.currentValue);
1688
1105
  const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value != null && Math.abs(answer.value) <= 1);
1689
-
1690
- // The coefficient is an attribute of the widget
1691
1106
  let localValue = currentValue;
1692
1107
  if (rubric.coefficient) {
1693
1108
  if (!localValue) {
@@ -1699,21 +1114,16 @@ function scoreNumericInput(userInput, rubric) {
1699
1114
  const matchedAnswer = rubric.answers.map(answer => {
1700
1115
  const validateFn = createValidator(answer);
1701
1116
  const score = validateFn(maybeParsePercentInput(localValue, normalizedAnswerExpected));
1702
- return {
1703
- ...answer,
1117
+ return _extends({}, answer, {
1704
1118
  score
1705
- };
1119
+ });
1706
1120
  }).find(answer => {
1707
- // NOTE: "answer.score.correct" indicates a match via the validate function.
1708
- // It does NOT indicate that the answer itself is correct.
1709
1121
  return answer.score.correct || answer.status === "correct" && answer.score.empty;
1710
1122
  });
1711
- const result = matchedAnswer?.status === "correct" ? matchedAnswer.score : {
1712
- empty: matchedAnswer?.status === "ungraded",
1713
- correct: matchedAnswer?.status === "correct",
1714
- message: matchedAnswer?.message ?? null,
1715
- guess: localValue
1716
- };
1123
+ const result = (matchedAnswer == null ? void 0 : matchedAnswer.status) === "correct" ? matchedAnswer.score : {
1124
+ empty: (matchedAnswer == null ? void 0 : matchedAnswer.status) === "ungraded",
1125
+ correct: (matchedAnswer == null ? void 0 : matchedAnswer.status) === "correct",
1126
+ message: (_matchedAnswer$messag = matchedAnswer == null ? void 0 : matchedAnswer.message) != null ? _matchedAnswer$messag : null};
1717
1127
  if (result.empty) {
1718
1128
  return {
1719
1129
  type: "invalid",
@@ -1729,7 +1139,7 @@ function scoreNumericInput(userInput, rubric) {
1729
1139
  }
1730
1140
 
1731
1141
  function scoreOrderer(userInput, rubric) {
1732
- const correct = ___default["default"].isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
1142
+ const correct = _.isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
1733
1143
  return {
1734
1144
  type: "points",
1735
1145
  earned: correct ? 1 : 0,
@@ -1738,12 +1148,6 @@ function scoreOrderer(userInput, rubric) {
1738
1148
  };
1739
1149
  }
1740
1150
 
1741
- /**
1742
- * Checks user input from the orderer widget to see if the user has started
1743
- * ordering the options, making the widget scorable.
1744
- * @param userInput
1745
- * @see `scoreOrderer` for more details.
1746
- */
1747
1151
  function validateOrderer(userInput) {
1748
1152
  if (userInput.current.length === 0) {
1749
1153
  return {
@@ -1757,20 +1161,14 @@ function validateOrderer(userInput) {
1757
1161
  function scorePlotter(userInput, rubric) {
1758
1162
  return {
1759
1163
  type: "points",
1760
- earned: perseusCore.approximateDeepEqual(userInput, rubric.correct) ? 1 : 0,
1164
+ earned: approximateDeepEqual(userInput, rubric.correct) ? 1 : 0,
1761
1165
  total: 1,
1762
1166
  message: null
1763
1167
  };
1764
1168
  }
1765
1169
 
1766
- /**
1767
- * Checks user input to confirm it is not the same as the starting values for the graph.
1768
- * This means the user has modified the graph, and the question can be scored.
1769
- *
1770
- * @see 'scorePlotter' for more details on scoring.
1771
- */
1772
1170
  function validatePlotter(userInput, validationData) {
1773
- if (perseusCore.approximateDeepEqual(userInput, validationData.starting)) {
1171
+ if (approximateDeepEqual(userInput, validationData.starting)) {
1774
1172
  return {
1775
1173
  type: "invalid",
1776
1174
  message: null
@@ -1791,7 +1189,6 @@ function scoreRadio(userInput, rubric) {
1791
1189
  type: "invalid",
1792
1190
  message: ErrorCodes.CHOOSE_CORRECT_NUM_ERROR
1793
1191
  };
1794
- // If NOTA and some other answer are checked, ...
1795
1192
  }
1796
1193
  const noneOfTheAboveSelected = rubric.choices.some((choice, index) => choice.isNoneOfTheAbove && userInput.choicesSelected[index]);
1797
1194
  if (noneOfTheAboveSelected && numSelected > 1) {
@@ -1819,14 +1216,6 @@ function scoreRadio(userInput, rubric) {
1819
1216
  };
1820
1217
  }
1821
1218
 
1822
- /**
1823
- * Checks if the user has selected at least one option. Additional validation
1824
- * is done in scoreRadio to check if the number of selected options is correct
1825
- * and if the user has selected both a correct option and the "none of the above"
1826
- * option.
1827
- * @param userInput
1828
- * @see `scoreRadio` for the additional validation logic and the scoring logic.
1829
- */
1830
1219
  function validateRadio(userInput) {
1831
1220
  const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
1832
1221
  return sum + (selected ? 1 : 0);
@@ -1841,7 +1230,7 @@ function validateRadio(userInput) {
1841
1230
  }
1842
1231
 
1843
1232
  function scoreSorter(userInput, rubric) {
1844
- const correct = perseusCore.approximateDeepEqual(userInput.options, rubric.correct);
1233
+ const correct = approximateDeepEqual(userInput.options, rubric.correct);
1845
1234
  return {
1846
1235
  type: "points",
1847
1236
  earned: correct ? 1 : 0,
@@ -1850,20 +1239,7 @@ function scoreSorter(userInput, rubric) {
1850
1239
  };
1851
1240
  }
1852
1241
 
1853
- /**
1854
- * Checks user input for the sorter widget to ensure that the user has made
1855
- * changes before attempting to score the widget.
1856
- * @param userInput
1857
- * @see 'scoreSorter' in 'packages/perseus/src/widgets/sorter/score-sorter.ts'
1858
- * for more details on how the sorter widget is scored.
1859
- */
1860
1242
  function validateSorter(userInput) {
1861
- // If the sorter widget hasn't been changed yet, we treat it as "empty" which
1862
- // prevents the "Check" button from becoming active. We want the user
1863
- // to make a change before trying to move forward. This makes an
1864
- // assumption that the initial order isn't the correct order! However,
1865
- // this should be rare if it happens, and interacting with the list
1866
- // will enable the button, so they won't be locked out of progressing.
1867
1243
  if (!userInput.changed) {
1868
1244
  return {
1869
1245
  type: "invalid",
@@ -1873,15 +1249,8 @@ function validateSorter(userInput) {
1873
1249
  return null;
1874
1250
  }
1875
1251
 
1876
- /**
1877
- * Filters the given table (modelled as a 2D array) to remove any rows that are
1878
- * completely empty.
1879
- *
1880
- * @returns A new table with only non-empty rows.
1881
- */
1882
- const filterNonEmpty = function (table) {
1252
+ const filterNonEmpty = function filterNonEmpty(table) {
1883
1253
  return table.filter(function (row) {
1884
- // Return only rows that are non-empty.
1885
1254
  return row.some(cell => cell);
1886
1255
  });
1887
1256
  };
@@ -1986,10 +1355,6 @@ function scoreInputNumber(userInput, rubric) {
1986
1355
  if (rubric.answerType == null) {
1987
1356
  rubric.answerType = "number";
1988
1357
  }
1989
-
1990
- // note(matthewc): this will get immediately parsed again by
1991
- // `KhanAnswerTypes.number.convertToPredicate`, but a string is
1992
- // expected here
1993
1358
  const stringValue = `${rubric.value}`;
1994
1359
  const val = KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
1995
1360
  simplify: rubric.simplify,
@@ -1997,9 +1362,6 @@ function scoreInputNumber(userInput, rubric) {
1997
1362
  maxError: rubric.maxError,
1998
1363
  forms: inputNumberAnswerTypes[rubric.answerType].forms
1999
1364
  });
2000
-
2001
- // We may have received TeX; try to parse it before grading.
2002
- // If `currentValue` is not TeX, this should be a no-op.
2003
1365
  const currentValue = parseTex(userInput.currentValue);
2004
1366
  const result = val(currentValue);
2005
1367
  if (result.empty) {
@@ -2016,16 +1378,7 @@ function scoreInputNumber(userInput, rubric) {
2016
1378
  };
2017
1379
  }
2018
1380
 
2019
- /**
2020
- * Several widgets don't have "right"/"wrong" scoring logic,
2021
- * so this just says to move on past those widgets
2022
- *
2023
- * TODO(LEMS-2543) widgets that use this probably shouldn't have any
2024
- * scoring logic and the thing scoring an exercise
2025
- * should just know to skip these
2026
- */
2027
- function scoreNoop() {
2028
- let points = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
1381
+ function scoreNoop(points = 0) {
2029
1382
  return {
2030
1383
  type: "points",
2031
1384
  earned: points,
@@ -2034,32 +1387,21 @@ function scoreNoop() {
2034
1387
  };
2035
1388
  }
2036
1389
 
2037
- // The `group` widget is basically a widget hosting a full Perseus system in
2038
- // it. As such, scoring a group means scoring all widgets it contains.
2039
1390
  function scoreGroup(userInput, rubric, locale) {
2040
1391
  const scores = scoreWidgetsFunctional(rubric.widgets, Object.keys(rubric.widgets), userInput, locale);
2041
1392
  return flattenScores(scores);
2042
1393
  }
2043
1394
 
2044
- /**
2045
- * Checks the given user input to see if any answerable widgets have not been
2046
- * "filled in" (ie. if they're empty). Another way to think about this
2047
- * function is that its a check to see if we can score the provided input.
2048
- */
2049
- function emptyWidgetsFunctional(widgets,
2050
- // This is a port of old code, I'm not sure why
2051
- // we need widgetIds vs the keys of the widgets object
2052
- widgetIds, userInputMap, locale) {
1395
+ function emptyWidgetsFunctional(widgets, widgetIds, userInputMap, locale) {
2053
1396
  return widgetIds.filter(id => {
2054
1397
  const widget = widgets[id];
2055
1398
  if (!widget || widget.static === true) {
2056
- // Static widgets shouldn't count as empty
2057
1399
  return false;
2058
1400
  }
2059
1401
  const validator = getWidgetValidator(widget.type);
2060
1402
  const userInput = userInputMap[id];
2061
1403
  const validationData = widget.options;
2062
- const score = validator?.(userInput, validationData, locale);
1404
+ const score = validator == null ? void 0 : validator(userInput, validationData, locale);
2063
1405
  if (score) {
2064
1406
  return scoreIsEmpty(score);
2065
1407
  }
@@ -2085,7 +1427,6 @@ function validateLabelImage(userInput) {
2085
1427
  numAnswered++;
2086
1428
  }
2087
1429
  }
2088
- // We expect all question markers to be answered before grading.
2089
1430
  if (numAnswered !== userInput.markers.length) {
2090
1431
  return {
2091
1432
  type: "invalid",
@@ -2126,10 +1467,12 @@ function registerWidget(type, scorer, validator) {
2126
1467
  };
2127
1468
  }
2128
1469
  const getWidgetValidator = name => {
2129
- return widgets[name]?.validator ?? null;
1470
+ var _widgets$name$validat, _widgets$name;
1471
+ return (_widgets$name$validat = (_widgets$name = widgets[name]) == null ? void 0 : _widgets$name.validator) != null ? _widgets$name$validat : null;
2130
1472
  };
2131
1473
  const getWidgetScorer = name => {
2132
- return widgets[name]?.scorer ?? null;
1474
+ var _widgets$name$scorer, _widgets$name2;
1475
+ return (_widgets$name$scorer = (_widgets$name2 = widgets[name]) == null ? void 0 : _widgets$name2.scorer) != null ? _widgets$name$scorer : null;
2133
1476
  };
2134
1477
  registerWidget("categorizer", scoreCategorizer, validateCategorizer);
2135
1478
  registerWidget("cs-program", scoreCSProgram);
@@ -2169,40 +1512,13 @@ const noScore = {
2169
1512
  total: 0,
2170
1513
  message: null
2171
1514
  };
2172
-
2173
- /**
2174
- * If a widget says that it is empty once it is graded.
2175
- * Trying to encapsulate references to the score format.
2176
- */
2177
1515
  function scoreIsEmpty(score) {
2178
- // HACK(benkomalo): ugh. this isn't great; the Perseus score objects
2179
- // overload the type "invalid" for what should probably be three
2180
- // distinct cases:
2181
- // - truly empty or not fully filled out
2182
- // - invalid or malformed inputs
2183
- // - "almost correct" like inputs where the widget wants to give
2184
- // feedback (e.g. a fraction needs to be reduced, or `pi` should
2185
- // be used instead of 3.14)
2186
- //
2187
- // Unfortunately the coercion happens all over the place, as these
2188
- // Perseus style score objects are created *everywhere* (basically
2189
- // in every widget), so it's hard to change now. We assume that
2190
- // anything with a "message" is not truly empty, and one of the
2191
- // latter two cases for now.
2192
1516
  return score.type === "invalid" && (!score.message || score.message.length === 0);
2193
1517
  }
2194
-
2195
- /**
2196
- * Combine two score objects.
2197
- *
2198
- * Given two score objects for two different widgets, combine them so that
2199
- * if one is wrong, the total score is wrong, etc.
2200
- */
2201
1518
  function combineScores(scoreA, scoreB) {
2202
1519
  let message;
2203
1520
  if (scoreA.type === "points" && scoreB.type === "points") {
2204
1521
  if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
2205
- // TODO(alpert): Figure out how to combine messages usefully
2206
1522
  message = null;
2207
1523
  } else {
2208
1524
  message = scoreA.message || scoreB.message;
@@ -2222,7 +1538,6 @@ function combineScores(scoreA, scoreB) {
2222
1538
  }
2223
1539
  if (scoreA.type === "invalid" && scoreB.type === "invalid") {
2224
1540
  if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
2225
- // TODO(alpert): Figure out how to combine messages usefully
2226
1541
  message = null;
2227
1542
  } else {
2228
1543
  message = scoreA.message || scoreB.message;
@@ -2232,12 +1547,7 @@ function combineScores(scoreA, scoreB) {
2232
1547
  message: message
2233
1548
  };
2234
1549
  }
2235
-
2236
- /**
2237
- * The above checks cover all combinations of score type, so if we get here
2238
- * then something is amiss with our inputs.
2239
- */
2240
- throw new perseusCore.PerseusError("PerseusScore with unknown type encountered", perseusCore.Errors.InvalidInput, {
1550
+ throw new PerseusError("PerseusScore with unknown type encountered", Errors.InvalidInput, {
2241
1551
  metadata: {
2242
1552
  scoreA: JSON.stringify(scoreA),
2243
1553
  scoreB: JSON.stringify(scoreB)
@@ -2247,38 +1557,22 @@ function combineScores(scoreA, scoreB) {
2247
1557
  function flattenScores(widgetScoreMap) {
2248
1558
  return Object.values(widgetScoreMap).reduce(combineScores, noScore);
2249
1559
  }
2250
-
2251
- /**
2252
- * score a Perseus item
2253
- *
2254
- * @param perseusRenderData - the full answer data, includes the correct answer
2255
- * @param userInputMap - the user's input for each widget, mapped by ID
2256
- * @param locale - string locale for math parsing ("de" 1.000,00 vs "en" 1,000.00)
2257
- */
2258
1560
  function scorePerseusItem(perseusRenderData, userInputMap, locale) {
2259
- // There seems to be a chance that PerseusRenderer.widgets might include
2260
- // widget data for widgets that are not in PerseusRenderer.content,
2261
- // so this checks that the widgets are being used before scoring them
2262
- const usedWidgetIds = perseusCore.getWidgetIdsFromContent(perseusRenderData.content);
1561
+ const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
2263
1562
  const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, locale);
2264
1563
  return flattenScores(scores);
2265
1564
  }
2266
-
2267
- // TODO: combine scorePerseusItem with scoreWidgetsFunctional
2268
- function scoreWidgetsFunctional(widgets,
2269
- // This is a port of old code, I'm not sure why
2270
- // we need widgetIds vs the keys of the widgets object
2271
- widgetIds, userInputMap, locale) {
2272
- const upgradedWidgets = perseusCore.getUpgradedWidgetOptions(widgets);
1565
+ function scoreWidgetsFunctional(widgets, widgetIds, userInputMap, locale) {
1566
+ const upgradedWidgets = getUpgradedWidgetOptions(widgets);
2273
1567
  const gradedWidgetIds = widgetIds.filter(id => {
2274
1568
  const props = upgradedWidgets[id];
2275
- const widgetIsGraded = props?.graded == null || props.graded;
2276
- const widgetIsStatic = !!props?.static;
2277
- // Ungraded widgets or widgets set to static shouldn't be graded.
1569
+ const widgetIsGraded = (props == null ? void 0 : props.graded) == null || props.graded;
1570
+ const widgetIsStatic = !!(props != null && props.static);
2278
1571
  return widgetIsGraded && !widgetIsStatic;
2279
1572
  });
2280
1573
  const widgetScores = {};
2281
1574
  gradedWidgetIds.forEach(id => {
1575
+ var _validator;
2282
1576
  const widget = upgradedWidgets[id];
2283
1577
  if (!widget) {
2284
1578
  return;
@@ -2286,10 +1580,7 @@ widgetIds, userInputMap, locale) {
2286
1580
  const userInput = userInputMap[id];
2287
1581
  const validator = getWidgetValidator(widget.type);
2288
1582
  const scorer = getWidgetScorer(widget.type);
2289
-
2290
- // We do validation (empty checks) first and then scoring. If
2291
- // validation fails, it's result is itself a PerseusScore.
2292
- const score = validator?.(userInput, widget.options, locale) ?? scorer?.(userInput, widget.options, locale);
1583
+ const score = (_validator = validator == null ? void 0 : validator(userInput, widget.options, locale)) != null ? _validator : scorer == null ? void 0 : scorer(userInput, widget.options, locale);
2293
1584
  if (score != null) {
2294
1585
  widgetScores[id] = score;
2295
1586
  }
@@ -2297,43 +1588,5 @@ widgetIds, userInputMap, locale) {
2297
1588
  return widgetScores;
2298
1589
  }
2299
1590
 
2300
- exports.ErrorCodes = ErrorCodes;
2301
- exports.KhanAnswerTypes = KhanAnswerTypes;
2302
- exports.emptyWidgetsFunctional = emptyWidgetsFunctional;
2303
- exports.flattenScores = flattenScores;
2304
- exports.getWidgetScorer = getWidgetScorer;
2305
- exports.getWidgetValidator = getWidgetValidator;
2306
- exports.inputNumberAnswerTypes = inputNumberAnswerTypes;
2307
- exports.registerWidget = registerWidget;
2308
- exports.scoreCSProgram = scoreCSProgram;
2309
- exports.scoreCategorizer = scoreCategorizer;
2310
- exports.scoreDropdown = scoreDropdown;
2311
- exports.scoreExpression = scoreExpression;
2312
- exports.scoreGrapher = scoreGrapher;
2313
- exports.scoreIframe = scoreIframe;
2314
- exports.scoreInputNumber = scoreInputNumber;
2315
- exports.scoreInteractiveGraph = scoreInteractiveGraph;
2316
- exports.scoreLabelImage = scoreLabelImage;
2317
- exports.scoreLabelImageMarker = scoreLabelImageMarker;
2318
- exports.scoreMatcher = scoreMatcher;
2319
- exports.scoreMatrix = scoreMatrix;
2320
- exports.scoreNumberLine = scoreNumberLine;
2321
- exports.scoreNumericInput = scoreNumericInput;
2322
- exports.scoreOrderer = scoreOrderer;
2323
- exports.scorePerseusItem = scorePerseusItem;
2324
- exports.scorePlotter = scorePlotter;
2325
- exports.scoreRadio = scoreRadio;
2326
- exports.scoreSorter = scoreSorter;
2327
- exports.scoreTable = scoreTable;
2328
- exports.scoreWidgetsFunctional = scoreWidgetsFunctional;
2329
- exports.validateCategorizer = validateCategorizer;
2330
- exports.validateDropdown = validateDropdown;
2331
- exports.validateExpression = validateExpression;
2332
- exports.validateMatrix = validateMatrix;
2333
- exports.validateNumberLine = validateNumberLine;
2334
- exports.validateOrderer = validateOrderer;
2335
- exports.validatePlotter = validatePlotter;
2336
- exports.validateRadio = validateRadio;
2337
- exports.validateSorter = validateSorter;
2338
- exports.validateTable = validateTable;
1591
+ export { ErrorCodes, KhanAnswerTypes, emptyWidgetsFunctional, flattenScores, getWidgetScorer, getWidgetValidator, inputNumberAnswerTypes, registerWidget, scoreCSProgram, scoreCategorizer, scoreDropdown, scoreExpression, scoreGrapher, scoreIframe, scoreInputNumber, scoreInteractiveGraph, scoreLabelImage, scoreLabelImageMarker, scoreMatcher, scoreMatrix, scoreNumberLine, scoreNumericInput, scoreOrderer, scorePerseusItem, scorePlotter, scoreRadio, scoreSorter, scoreTable, scoreWidgetsFunctional, validateCategorizer, validateDropdown, validateExpression, validateMatrix, validateNumberLine, validateOrderer, validatePlotter, validateRadio, validateSorter, validateTable };
2339
1592
  //# sourceMappingURL=index.js.map