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