@khanacademy/perseus 43.0.0 → 43.1.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.
Files changed (38) hide show
  1. package/dist/components/number-input.d.ts +5 -3
  2. package/dist/components/text-input.d.ts +4 -2
  3. package/dist/es/index.js +475 -306
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/es/strings.js +7 -3
  6. package/dist/es/strings.js.map +1 -1
  7. package/dist/index.js +465 -296
  8. package/dist/index.js.map +1 -1
  9. package/dist/perseus-types.d.ts +14 -7
  10. package/dist/strings.js +7 -3
  11. package/dist/strings.js.map +1 -1
  12. package/dist/util/parse-perseus-json/general-purpose-parsers/defaulted.d.ts +1 -1
  13. package/dist/util/parse-perseus-json/general-purpose-parsers/index.d.ts +2 -0
  14. package/dist/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.d.ts +7 -0
  15. package/dist/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.typetest.d.ts +1 -0
  16. package/dist/util/parse-perseus-json/general-purpose-parsers/unknown.d.ts +2 -0
  17. package/dist/util/parse-perseus-json/perseus-parsers/images-map.d.ts +5 -0
  18. package/dist/util/parse-perseus-json/perseus-parsers/widget.d.ts +4 -0
  19. package/dist/validation.types.d.ts +14 -6
  20. package/dist/widgets/categorizer/categorizer.d.ts +2 -2
  21. package/dist/widgets/categorizer/score-categorizer.d.ts +2 -2
  22. package/dist/widgets/categorizer/validate-categorizer.d.ts +15 -0
  23. package/dist/widgets/dropdown/validate-dropdown.d.ts +10 -0
  24. package/dist/widgets/interactive-graphs/graphs/components/movable-point.d.ts +11 -7
  25. package/dist/widgets/interactive-graphs/graphs/components/use-control-point.d.ts +7 -3
  26. package/dist/widgets/interactive-graphs/types.d.ts +1 -0
  27. package/dist/widgets/interactive-graphs/utils.d.ts +6 -6
  28. package/dist/widgets/matrix/matrix.d.ts +12 -5
  29. package/dist/widgets/measurer/measurer.d.ts +1 -1
  30. package/dist/widgets/orderer/validate-orderer.d.ts +12 -0
  31. package/dist/widgets/plotter/plotter.d.ts +2 -2
  32. package/dist/widgets/plotter/score-plotter.d.ts +2 -2
  33. package/dist/widgets/plotter/validate-plotter.d.ts +12 -0
  34. package/dist/widgets/radio/radio.d.ts +1 -1
  35. package/dist/widgets/radio/validate-radio.d.ts +14 -0
  36. package/dist/widgets/sorter/validate-sorter.d.ts +13 -0
  37. package/package.json +4 -4
  38. package/dist/util/parse-perseus-json/general-purpose-parsers/compose-parsers.d.ts +0 -2
package/dist/index.js CHANGED
@@ -2882,8 +2882,6 @@ async function getImageSizeModern(url) {
2882
2882
  });
2883
2883
  }
2884
2884
 
2885
- /* eslint-disable @babel/no-invalid-this, getter-return, one-var */
2886
-
2887
2885
  // TODO: dedupe this with Coord in interactive2/types.js
2888
2886
 
2889
2887
  let supportsPassive = false;
@@ -2960,8 +2958,8 @@ function shuffle$3(array, randomSeed) {
2960
2958
  do {
2961
2959
  // Fischer-Yates shuffle
2962
2960
  for (let top = shuffled.length; top > 0; top--) {
2963
- const newEnd = Math.floor(random() * top),
2964
- temp = shuffled[newEnd];
2961
+ const newEnd = Math.floor(random() * top);
2962
+ const temp = shuffled[newEnd];
2965
2963
 
2966
2964
  // @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
2967
2965
  shuffled[newEnd] = shuffled[top - 1];
@@ -3293,13 +3291,13 @@ function eq$4(x, y) {
3293
3291
  * Deep approximate equality on primitives, numbers, arrays, and objects.
3294
3292
  * Recursive.
3295
3293
  */
3296
- function deepEq$6(x, y) {
3294
+ function deepEq$7(x, y) {
3297
3295
  if (Array.isArray(x) && Array.isArray(y)) {
3298
3296
  if (x.length !== y.length) {
3299
3297
  return false;
3300
3298
  }
3301
3299
  for (let i = 0; i < x.length; i++) {
3302
- if (!deepEq$6(x[i], y[i])) {
3300
+ if (!deepEq$7(x[i], y[i])) {
3303
3301
  return false;
3304
3302
  }
3305
3303
  }
@@ -3317,10 +3315,10 @@ function deepEq$6(x, y) {
3317
3315
  if (typeof x === "object" && typeof y === "object" && !!x && !!y) {
3318
3316
  return x === y || ___default["default"].all(x, function (v, k) {
3319
3317
  // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
3320
- return deepEq$6(y[k], v);
3318
+ return deepEq$7(y[k], v);
3321
3319
  }) && ___default["default"].all(y, function (v, k) {
3322
3320
  // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
3323
- return deepEq$6(x[k], v);
3321
+ return deepEq$7(x[k], v);
3324
3322
  });
3325
3323
  }
3326
3324
  if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
@@ -3599,7 +3597,7 @@ const Util = {
3599
3597
  tickStepFromNumTicks,
3600
3598
  constrainedTickStepsFromTickSteps,
3601
3599
  eq: eq$4,
3602
- deepEq: deepEq$6,
3600
+ deepEq: deepEq$7,
3603
3601
  parseQueryString,
3604
3602
  updateQueryString: updateQueryString$2,
3605
3603
  strongEncodeURIComponent,
@@ -9002,7 +9000,6 @@ var Tex$1 = {
9002
9000
  }
9003
9001
  };
9004
9002
 
9005
- /* eslint-disable @babel/no-invalid-this */
9006
9003
  const {
9007
9004
  processMath: processMath$1
9008
9005
  } = Tex$1;
@@ -10632,7 +10629,9 @@ const getClipPoint = function (graph, coord, angle) {
10632
10629
  return clipPoint;
10633
10630
  };
10634
10631
 
10635
- /* eslint-disable @babel/no-invalid-this */
10632
+ /**
10633
+ * Default methods for a wrapped movable.
10634
+ */
10636
10635
 
10637
10636
  /*
10638
10637
  * attr() and animate() simply pass the
@@ -10890,7 +10889,7 @@ class WrappedLine extends WrappedDrawing {
10890
10889
  */
10891
10890
  const {
10892
10891
  eq: eq$3,
10893
- deepEq: deepEq$5
10892
+ deepEq: deepEq$6
10894
10893
  } = Util;
10895
10894
 
10896
10895
  // This should really be a readonly tuple of [number, number]
@@ -11050,7 +11049,7 @@ function similar(coords1, coords2, tolerance) {
11050
11049
  angles = rotate(angles, i);
11051
11050
  // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
11052
11051
  sides = rotate(sides, i);
11053
- if (deepEq$5(angles1, angles)) {
11052
+ if (deepEq$6(angles1, angles)) {
11054
11053
  const sidePairs = ___default["default"].zip(sides1, sides);
11055
11054
  const factors = ___default["default"].map(sidePairs, function (pair) {
11056
11055
  return pair[0] / pair[1];
@@ -13989,7 +13988,7 @@ ___default["default"].extend(MovableAngle.prototype, {
13989
13988
 
13990
13989
  /* eslint-disable @babel/no-invalid-this */
13991
13990
  const nestedMap$1 = Util.nestedMap;
13992
- const deepEq$4 = Util.deepEq;
13991
+ const deepEq$5 = Util.deepEq;
13993
13992
 
13994
13993
  /**
13995
13994
  * A base class for all Graphie Movables
@@ -14086,7 +14085,7 @@ const createSimpleClass = function (addFunction) {
14086
14085
  this._prevProps = this.props;
14087
14086
  },
14088
14087
  modify: function (graphie) {
14089
- if (!deepEq$4(this.props, this._prevProps)) {
14088
+ if (!deepEq$5(this.props, this._prevProps)) {
14090
14089
  this.remove();
14091
14090
  this.add(graphie);
14092
14091
  this._prevProps = this.props;
@@ -16744,7 +16743,7 @@ var Movables = {
16744
16743
  const GraphieMovable = GraphieClasses.GraphieMovable;
16745
16744
  const createGraphie = GraphUtils.createGraphie;
16746
16745
  const {
16747
- deepEq: deepEq$3,
16746
+ deepEq: deepEq$4,
16748
16747
  nestedMap
16749
16748
  } = Util;
16750
16749
  const {
@@ -16782,7 +16781,7 @@ class Graphie extends React__namespace.Component {
16782
16781
  if (this.props.setup !== prevProps.setup) {
16783
16782
  Log.error("<Graphie> was given a new setup function. " + "This is a bad idea; please refactor your code to give " + "the same setup function reference to <Graphie> on " + "every render.", perseusCore.Errors.Internal);
16784
16783
  }
16785
- if (!deepEq$3(this.props.options, prevProps.options) || !deepEq$3(this.props.box, prevProps.box) || !deepEq$3(this.props.range, prevProps.range)) {
16784
+ if (!deepEq$4(this.props.options, prevProps.options) || !deepEq$4(this.props.box, prevProps.box) || !deepEq$4(this.props.range, prevProps.range)) {
16786
16785
  this._setupGraphie();
16787
16786
  }
16788
16787
  this._updateMovables();
@@ -18557,7 +18556,6 @@ const styles$C = aphrodite.StyleSheet.create({
18557
18556
  }
18558
18557
  });
18559
18558
 
18560
- /* eslint-disable no-useless-escape, no-prototype-builtins */
18561
18559
  const rules$1 = {
18562
18560
  ...pureMarkdown.pureMarkdownRules,
18563
18561
  // NOTE: basically ignored by JIPT. wraps everything at the outer layer
@@ -18734,7 +18732,7 @@ const rules$1 = {
18734
18732
  // The href prop should always be here, but just in case somehow the article's
18735
18733
  // link was not put together properly, let's make sure it's there so we
18736
18734
  // don't break the entire page.
18737
- const isKAUrl = href ? href.match(/https?:\/\/[^\/]*khanacademy.org|^\//) : false;
18735
+ const isKAUrl = href ? href.match(/https?:\/\/[^/]*khanacademy.org|^\//) : false;
18738
18736
  if (!isKAUrl) {
18739
18737
  // Prevents "reverse tabnabbing" phishing attacks
18740
18738
  // @ts-expect-error - TS2322 - Type '"noopener noreferrer"' is not assignable to type 'null'.
@@ -21144,6 +21142,7 @@ function uniqueIdForInput() {
21144
21142
  return `${prefix}${lastId}`;
21145
21143
  }
21146
21144
  class TextInput extends React__namespace.Component {
21145
+ inputRef = /*#__PURE__*/React__namespace.createRef();
21147
21146
  static defaultProps = {
21148
21147
  value: "",
21149
21148
  disabled: false
@@ -21157,33 +21156,32 @@ class TextInput extends React__namespace.Component {
21157
21156
  this.id = uniqueIdForInput();
21158
21157
  }
21159
21158
  }
21159
+ _getInput = () => {
21160
+ if (!this.inputRef.current) {
21161
+ throw new perseusCore.PerseusError("Input ref accessed before set", perseusCore.Errors.Internal);
21162
+ }
21163
+ return this.inputRef.current;
21164
+ };
21160
21165
  focus = () => {
21161
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'focus' does not exist on type 'Element | Text'.
21162
- ReactDOM__default["default"].findDOMNode(this).focus();
21166
+ this._getInput().focus();
21163
21167
  };
21164
21168
  blur = () => {
21165
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'blur' does not exist on type 'Element | Text'.
21166
- ReactDOM__default["default"].findDOMNode(this).blur();
21169
+ this._getInput().blur();
21167
21170
  };
21168
21171
  getValue = () => {
21169
- // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'.
21170
- return ReactDOM__default["default"].findDOMNode(this)?.value;
21172
+ return this.inputRef.current?.value;
21171
21173
  };
21172
21174
  getStringValue = () => {
21173
- // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'.
21174
- return ReactDOM__default["default"].findDOMNode(this)?.value.toString();
21175
+ return this.inputRef.current?.value.toString();
21175
21176
  };
21176
21177
  setSelectionRange = (selectionStart, selectionEnd) => {
21177
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'setSelectionRange' does not exist on type 'Element | Text'.
21178
- ReactDOM__default["default"].findDOMNode(this).setSelectionRange(selectionStart, selectionEnd);
21178
+ this._getInput().setSelectionRange(selectionStart, selectionEnd);
21179
21179
  };
21180
21180
  getSelectionStart = () => {
21181
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'.
21182
- return ReactDOM__default["default"].findDOMNode(this).selectionStart;
21181
+ return this._getInput().selectionStart;
21183
21182
  };
21184
21183
  getSelectionEnd = () => {
21185
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionEnd' does not exist on type 'Element | Text'.
21186
- return ReactDOM__default["default"].findDOMNode(this).selectionEnd;
21184
+ return this._getInput().selectionEnd;
21187
21185
  };
21188
21186
  render() {
21189
21187
  const {
@@ -21198,6 +21196,7 @@ class TextInput extends React__namespace.Component {
21198
21196
  } = this.props;
21199
21197
  const formattedValue = value === null ? "" : value.toString();
21200
21198
  return /*#__PURE__*/React__namespace.createElement(wonderBlocksForm.TextField, {
21199
+ ref: this.inputRef,
21201
21200
  style: style,
21202
21201
  disabled: disabled,
21203
21202
  id: this.id,
@@ -22219,7 +22218,7 @@ function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
22219
22218
  return value / 100;
22220
22219
  }
22221
22220
 
22222
- // Otherwise, we return input valuåe (number) stripped of the "%".
22221
+ // Otherwise, we return input value (number) stripped of the "%".
22223
22222
  return value;
22224
22223
  }
22225
22224
  function scoreNumericInput(userInput, rubric, strings) {
@@ -26277,7 +26276,15 @@ const styles$p = aphrodite.StyleSheet.create({
26277
26276
  }
26278
26277
  });
26279
26278
 
26280
- function scoreRadio(userInput, rubric, strings) {
26279
+ /**
26280
+ * Checks if the user has selected at least one option. Additional validation
26281
+ * is done in scoreRadio to check if the number of selected options is correct
26282
+ * and if the user has selected both a correct option and the "none of the above"
26283
+ * option.
26284
+ * @param userInput
26285
+ * @see `scoreRadio` for the additional validation logic and the scoring logic.
26286
+ */
26287
+ function validateRadio(userInput) {
26281
26288
  const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
26282
26289
  return sum + (selected ? 1 : 0);
26283
26290
  }, 0);
@@ -26287,6 +26294,17 @@ function scoreRadio(userInput, rubric, strings) {
26287
26294
  message: null
26288
26295
  };
26289
26296
  }
26297
+ return null;
26298
+ }
26299
+
26300
+ function scoreRadio(userInput, rubric, strings) {
26301
+ const validationError = validateRadio(userInput);
26302
+ if (validationError) {
26303
+ return validationError;
26304
+ }
26305
+ const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
26306
+ return sum + (selected ? 1 : 0);
26307
+ }, 0);
26290
26308
  const numCorrect = rubric.choices.reduce((sum, currentChoice) => {
26291
26309
  return currentChoice.correct ? sum + 1 : sum;
26292
26310
  }, 0);
@@ -26766,18 +26784,17 @@ const transform = (widgetOptions, strings, problemNum) => {
26766
26784
  };
26767
26785
  const propUpgrades$1 = {
26768
26786
  "1": v0props => {
26769
- let choices;
26770
- let hasNoneOfTheAbove;
26771
- if (!v0props.noneOfTheAbove) {
26772
- choices = v0props.choices;
26773
- hasNoneOfTheAbove = false;
26774
- } else {
26787
+ const {
26788
+ noneOfTheAbove,
26789
+ ...rest
26790
+ } = v0props;
26791
+ if (noneOfTheAbove) {
26775
26792
  throw new Error("radio widget v0 no longer supports auto noneOfTheAbove");
26776
26793
  }
26777
- return ___default["default"].extend(___default["default"].omit(v0props, "noneOfTheAbove"), {
26778
- choices: choices,
26779
- hasNoneOfTheAbove: hasNoneOfTheAbove
26780
- });
26794
+ return {
26795
+ ...rest,
26796
+ hasNoneOfTheAbove: false
26797
+ };
26781
26798
  }
26782
26799
  };
26783
26800
  var Radio = {
@@ -26814,23 +26831,36 @@ const getPromptJSON$n = (renderProps, userInput) => {
26814
26831
  };
26815
26832
  };
26816
26833
 
26817
- function scoreCategorizer(userInput, rubric, strings) {
26818
- let completed = true;
26819
- let allCorrect = true;
26820
- rubric.values.forEach((value, i) => {
26821
- if (userInput.values[i] == null) {
26822
- completed = false;
26823
- }
26824
- if (userInput.values[i] !== value) {
26825
- allCorrect = false;
26826
- }
26827
- });
26828
- if (!completed) {
26834
+ /**
26835
+ * Checks userInput from the categorizer widget to see if the user has selected
26836
+ * a category for each item.
26837
+ * @param userInput - The user's input corresponding to an array of indices that
26838
+ * represent the selected category for each row/item.
26839
+ * @param validationData - An array of strings corresponding to each row/item
26840
+ * @param strings - Used to provide a validation message
26841
+ */
26842
+ function validateCategorizer(userInput, validationData, strings) {
26843
+ const incomplete = validationData.items.some((_, i) => userInput.values[i] == null);
26844
+ if (incomplete) {
26829
26845
  return {
26830
26846
  type: "invalid",
26831
26847
  message: strings.invalidSelection
26832
26848
  };
26833
26849
  }
26850
+ return null;
26851
+ }
26852
+
26853
+ function scoreCategorizer(userInput, scoringData, strings) {
26854
+ const validationError = validateCategorizer(userInput, scoringData, strings);
26855
+ if (validationError) {
26856
+ return validationError;
26857
+ }
26858
+ let allCorrect = true;
26859
+ scoringData.values.forEach((value, i) => {
26860
+ if (userInput.values[i] !== value) {
26861
+ allCorrect = false;
26862
+ }
26863
+ });
26834
26864
  return {
26835
26865
  type: "points",
26836
26866
  earned: allCorrect ? 1 : 0,
@@ -27384,15 +27414,26 @@ const getPromptJSON$k = (renderProps, userInput) => {
27384
27414
  };
27385
27415
  };
27386
27416
 
27387
- function scoreDropdown(userInput, rubric) {
27388
- const selected = userInput.value;
27389
- if (selected === 0) {
27417
+ /**
27418
+ * Checks if the user has selected an item from the dropdown before scoring.
27419
+ * This is shown with a userInput value / index other than 0.
27420
+ */
27421
+ function validateDropdown(userInput) {
27422
+ if (userInput.value === 0) {
27390
27423
  return {
27391
27424
  type: "invalid",
27392
27425
  message: null
27393
27426
  };
27394
27427
  }
27395
- const correct = rubric.choices[selected - 1].correct;
27428
+ return null;
27429
+ }
27430
+
27431
+ function scoreDropdown(userInput, rubric) {
27432
+ const validationError = validateDropdown(userInput);
27433
+ if (validationError) {
27434
+ return validationError;
27435
+ }
27436
+ const correct = rubric.choices[userInput.value - 1].correct;
27396
27437
  return {
27397
27438
  type: "points",
27398
27439
  earned: correct ? 1 : 0,
@@ -27782,8 +27823,6 @@ const getNextState = (currentState, answerable) => {
27782
27823
  }
27783
27824
  };
27784
27825
 
27785
- // exports has no 'transform'
27786
-
27787
27826
  // A Graded Group is more or less a Group widget that displays a check
27788
27827
  // answer button below the rendered content. When clicked, the widget grades
27789
27828
  // the stuff inside and displays feedback about whether the inputted answer was
@@ -28453,7 +28492,6 @@ const getPromptJSON$g = (renderProps, userInput) => {
28453
28492
  };
28454
28493
  };
28455
28494
 
28456
- /* eslint-disable one-var */
28457
28495
  // @ts-expect-error - TS2339 - Property 'Plot' does not exist on type 'typeof Graphie'.
28458
28496
  const Plot$1 = Graphie.Plot;
28459
28497
  const DEFAULT_BACKGROUND_IMAGE = {
@@ -28554,14 +28592,14 @@ const Linear = ___default["default"].extend({}, PlotDefaults, {
28554
28592
  return [m, b];
28555
28593
  },
28556
28594
  getFunctionForCoeffs: function (coeffs, x) {
28557
- const m = coeffs[0],
28558
- b = coeffs[1];
28595
+ const m = coeffs[0];
28596
+ const b = coeffs[1];
28559
28597
  return m * x + b;
28560
28598
  },
28561
28599
  getEquationString: function (coords) {
28562
28600
  const coeffs = this.getCoefficients(coords);
28563
- const m = coeffs[0],
28564
- b = coeffs[1];
28601
+ const m = coeffs[0];
28602
+ const b = coeffs[1];
28565
28603
  return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
28566
28604
  }
28567
28605
  });
@@ -28585,9 +28623,9 @@ const Quadratic = ___default["default"].extend({}, PlotDefaults, {
28585
28623
  return [a, b, c];
28586
28624
  },
28587
28625
  getFunctionForCoeffs: function (coeffs, x) {
28588
- const a = coeffs[0],
28589
- b = coeffs[1],
28590
- c = coeffs[2];
28626
+ const a = coeffs[0];
28627
+ const b = coeffs[1];
28628
+ const c = coeffs[2];
28591
28629
  return (a * x + b) * x + c;
28592
28630
  },
28593
28631
  getPropsForCoeffs: function (coeffs) {
@@ -28599,9 +28637,9 @@ const Quadratic = ___default["default"].extend({}, PlotDefaults, {
28599
28637
  },
28600
28638
  getEquationString: function (coords) {
28601
28639
  const coeffs = this.getCoefficients(coords);
28602
- const a = coeffs[0],
28603
- b = coeffs[1],
28604
- c = coeffs[2];
28640
+ const a = coeffs[0];
28641
+ const b = coeffs[1];
28642
+ const c = coeffs[2];
28605
28643
  return "y = " + a.toFixed(3) + "x^2 + " + b.toFixed(3) + "x + " + c.toFixed(3);
28606
28644
  }
28607
28645
  });
@@ -28620,10 +28658,10 @@ const Sinusoid = ___default["default"].extend({}, PlotDefaults, {
28620
28658
  return [a, b, c, d];
28621
28659
  },
28622
28660
  getFunctionForCoeffs: function (coeffs, x) {
28623
- const a = coeffs[0],
28624
- b = coeffs[1],
28625
- c = coeffs[2],
28626
- d = coeffs[3];
28661
+ const a = coeffs[0];
28662
+ const b = coeffs[1];
28663
+ const c = coeffs[2];
28664
+ const d = coeffs[3];
28627
28665
  return a * Math.sin(b * x - c) + d;
28628
28666
  },
28629
28667
  getPropsForCoeffs: function (coeffs) {
@@ -28636,10 +28674,10 @@ const Sinusoid = ___default["default"].extend({}, PlotDefaults, {
28636
28674
  },
28637
28675
  getEquationString: function (coords) {
28638
28676
  const coeffs = this.getCoefficients(coords);
28639
- const a = coeffs[0],
28640
- b = coeffs[1],
28641
- c = coeffs[2],
28642
- d = coeffs[3];
28677
+ const a = coeffs[0];
28678
+ const b = coeffs[1];
28679
+ const c = coeffs[2];
28680
+ const d = coeffs[3];
28643
28681
  return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
28644
28682
  },
28645
28683
  areEqual: function (coeffs1, coeffs2) {
@@ -28659,18 +28697,18 @@ const Tangent = ___default["default"].extend({}, PlotDefaults, {
28659
28697
  return [a, b, c, d];
28660
28698
  },
28661
28699
  getFunctionForCoeffs: function (coeffs, x) {
28662
- const a = coeffs[0],
28663
- b = coeffs[1],
28664
- c = coeffs[2],
28665
- d = coeffs[3];
28700
+ const a = coeffs[0];
28701
+ const b = coeffs[1];
28702
+ const c = coeffs[2];
28703
+ const d = coeffs[3];
28666
28704
  return a * Math.tan(b * x - c) + d;
28667
28705
  },
28668
28706
  getEquationString: function (coords) {
28669
28707
  const coeffs = this.getCoefficients(coords);
28670
- const a = coeffs[0],
28671
- b = coeffs[1],
28672
- c = coeffs[2],
28673
- d = coeffs[3];
28708
+ const a = coeffs[0];
28709
+ const b = coeffs[1];
28710
+ const c = coeffs[2];
28711
+ const d = coeffs[3];
28674
28712
  return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
28675
28713
  },
28676
28714
  areEqual: function (coeffs1, coeffs2) {
@@ -28729,9 +28767,9 @@ const Exponential = ___default["default"].extend({}, PlotDefaults, {
28729
28767
  return [a, b, c];
28730
28768
  },
28731
28769
  getFunctionForCoeffs: function (coeffs, x) {
28732
- const a = coeffs[0],
28733
- b = coeffs[1],
28734
- c = coeffs[2];
28770
+ const a = coeffs[0];
28771
+ const b = coeffs[1];
28772
+ const c = coeffs[2];
28735
28773
  return a * Math.exp(b * x) + c;
28736
28774
  },
28737
28775
  getEquationString: function (coords, asymptote) {
@@ -28739,9 +28777,9 @@ const Exponential = ___default["default"].extend({}, PlotDefaults, {
28739
28777
  return null;
28740
28778
  }
28741
28779
  const coeffs = this.getCoefficients(coords, asymptote);
28742
- const a = coeffs[0],
28743
- b = coeffs[1],
28744
- c = coeffs[2];
28780
+ const a = coeffs[0];
28781
+ const b = coeffs[1];
28782
+ const c = coeffs[2];
28745
28783
  return "y = " + a.toFixed(3) + "e^(" + b.toFixed(3) + "x) + " + c.toFixed(3);
28746
28784
  }
28747
28785
  });
@@ -28786,9 +28824,9 @@ const Logarithm = ___default["default"].extend({}, PlotDefaults, {
28786
28824
  }
28787
28825
  },
28788
28826
  getFunctionForCoeffs: function (coeffs, x, asymptote) {
28789
- const a = coeffs[0],
28790
- b = coeffs[1],
28791
- c = coeffs[2];
28827
+ const a = coeffs[0];
28828
+ const b = coeffs[1];
28829
+ const c = coeffs[2];
28792
28830
  return a * Math.log(b * x + c);
28793
28831
  },
28794
28832
  getEquationString: function (coords, asymptote) {
@@ -28796,9 +28834,9 @@ const Logarithm = ___default["default"].extend({}, PlotDefaults, {
28796
28834
  return null;
28797
28835
  }
28798
28836
  const coeffs = this.getCoefficients(coords, asymptote);
28799
- const a = coeffs[0],
28800
- b = coeffs[1],
28801
- c = coeffs[2];
28837
+ const a = coeffs[0];
28838
+ const b = coeffs[1];
28839
+ const c = coeffs[2];
28802
28840
  return "y = ln(" + a.toFixed(3) + "x + " + b.toFixed(3) + ") + " + c.toFixed(3);
28803
28841
  }
28804
28842
  });
@@ -28822,16 +28860,16 @@ const AbsoluteValue = ___default["default"].extend({}, PlotDefaults, {
28822
28860
  return [m, horizontalOffset, verticalOffset];
28823
28861
  },
28824
28862
  getFunctionForCoeffs: function (coeffs, x) {
28825
- const m = coeffs[0],
28826
- horizontalOffset = coeffs[1],
28827
- verticalOffset = coeffs[2];
28863
+ const m = coeffs[0];
28864
+ const horizontalOffset = coeffs[1];
28865
+ const verticalOffset = coeffs[2];
28828
28866
  return m * Math.abs(x - horizontalOffset) + verticalOffset;
28829
28867
  },
28830
28868
  getEquationString: function (coords) {
28831
28869
  const coeffs = this.getCoefficients(coords);
28832
- const m = coeffs[0],
28833
- horizontalOffset = coeffs[1],
28834
- verticalOffset = coeffs[2];
28870
+ const m = coeffs[0];
28871
+ const horizontalOffset = coeffs[1];
28872
+ const verticalOffset = coeffs[2];
28835
28873
  return "y = " + m.toFixed(3) + "| x - " + horizontalOffset.toFixed(3) + "| + " + verticalOffset.toFixed(3);
28836
28874
  }
28837
28875
  });
@@ -31432,67 +31470,6 @@ function isUnlimitedGraphState(state) {
31432
31470
  return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
31433
31471
  }
31434
31472
 
31435
- /**
31436
- * Replace all text outside of the $ TeX blocks with `\\text{...}`
31437
- * This way, the entire resulting string can be rendered within <TeX>
31438
- * and the text outside of the $ blocks will be non-TeX text.
31439
- */
31440
- function replaceOutsideTeX(mathString) {
31441
- // All the information we need is in the first section,
31442
- // whether it's typed as "blockmath" or "paragraph"
31443
- const firstSection = pureMarkdown.parse(mathString)[0];
31444
-
31445
- // If it's blockMath, the outer level has the full math content.
31446
- if (firstSection.type === "blockMath") {
31447
- return firstSection.content;
31448
- }
31449
-
31450
- // If it's a paragraph, we need to iterate through the sections
31451
- // to look for individual math blocks.
31452
- const condensedNodes = condenseTextNodes(firstSection.content);
31453
- let result = "";
31454
- for (const piece of condensedNodes) {
31455
- piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
31456
- }
31457
- return result;
31458
- }
31459
- // Helper function for replaceOutsideTeX()
31460
- // Condense adjacent text nodes into a single text node
31461
- function condenseTextNodes(nodes) {
31462
- const result = [];
31463
- if (!nodes) {
31464
- return result;
31465
- }
31466
- let currentText = "";
31467
- for (const node of nodes) {
31468
- if (node.type === "math") {
31469
- if (currentText) {
31470
- result.push({
31471
- type: "text",
31472
- content: currentText
31473
- });
31474
- currentText = "";
31475
- }
31476
- result.push(node);
31477
- } else {
31478
- currentText += node.content;
31479
- }
31480
- }
31481
- if (currentText) {
31482
- result.push({
31483
- type: "text",
31484
- content: currentText
31485
- });
31486
- }
31487
- return result;
31488
- }
31489
-
31490
- // Helper function for replaceOutsideTeX()
31491
- function escapeSpecialChars(str) {
31492
- // Escape $, \, {, and } characters
31493
- return str.replace(/([$\\{}])/g, "\\$1");
31494
- }
31495
-
31496
31473
  /**
31497
31474
  * Parse a string of text and math into a list of objects with type and content
31498
31475
  *
@@ -31525,6 +31502,29 @@ const mathOnlyParser = SimpleMarkdown__default["default"].parserFor({
31525
31502
  inline: true
31526
31503
  });
31527
31504
 
31505
+ /**
31506
+ * Replace all text outside of the $ TeX blocks with `\\text{...}`
31507
+ * This way, the entire resulting string can be rendered within <TeX>
31508
+ * and the text outside of the $ blocks will be non-TeX text.
31509
+ */
31510
+ function replaceOutsideTeX(mathString) {
31511
+ // All the information we need is in the first section,
31512
+ // whether it's typed as "blockmath" or "paragraph"
31513
+ const parsed = mathOnlyParser(mathString);
31514
+ let result = "";
31515
+ for (const piece of parsed) {
31516
+ piece.type === "math" ? result += "$" + piece.content + "$" : piece.type === "specialCharacter" ? result += escapeIfUnescaped(piece.content) : result += piece.content;
31517
+ }
31518
+ return `\\text{${result}}`;
31519
+ }
31520
+ function escapeIfUnescaped(character) {
31521
+ if (character.length === 1) {
31522
+ return "\\" + character;
31523
+ } else {
31524
+ return character;
31525
+ }
31526
+ }
31527
+
31528
31528
  function AxisLabels() {
31529
31529
  const {
31530
31530
  range,
@@ -32092,6 +32092,7 @@ const getNumericFormat = KhanMath.getNumericFormat;
32092
32092
  */
32093
32093
  class NumberInput extends React__namespace.Component {
32094
32094
  static contextType = PerseusI18nContext;
32095
+ inputRef = /*#__PURE__*/React__namespace.createRef();
32095
32096
  static propTypes = {
32096
32097
  value: PropTypes__default["default"].number,
32097
32098
  format: PropTypes__default["default"].string,
@@ -32118,23 +32119,24 @@ class NumberInput extends React__namespace.Component {
32118
32119
  this._setValue(this.props.value, this.state.format);
32119
32120
  }
32120
32121
  }
32122
+ _getInput = () => {
32123
+ if (!this.inputRef.current) {
32124
+ throw new perseusCore.PerseusError("Input ref accessed before set", perseusCore.Errors.Internal);
32125
+ }
32126
+ return this.inputRef.current;
32127
+ };
32121
32128
 
32122
32129
  /* Return the current "value" of this input
32123
32130
  * If empty, it returns the placeholder (if it is a number) or null
32124
32131
  */
32125
32132
  getValue = () => {
32126
- return this.parseInputValue(
32127
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'value' does not exist on type 'Element | Text'.
32128
- ReactDOM__default["default"].findDOMNode(this.refs.input).value // eslint-disable-line react/no-string-refs
32129
- );
32133
+ return this.parseInputValue(this._getInput().value);
32130
32134
  };
32131
32135
 
32132
32136
  /* Return the current string value of this input */
32133
32137
  getStringValue = () => {
32134
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'value' does not exist on type 'Element | Text'.
32135
- return ReactDOM__default["default"].findDOMNode(this.refs.input).value.toString(); // eslint-disable-line react/no-string-refs
32138
+ return this._getInput().toString();
32136
32139
  };
32137
-
32138
32140
  parseInputValue = value => {
32139
32141
  if (value === "") {
32140
32142
  const placeholder = this.props.placeholder;
@@ -32146,26 +32148,21 @@ class NumberInput extends React__namespace.Component {
32146
32148
 
32147
32149
  /* Set text input focus to this input */
32148
32150
  focus = () => {
32149
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'focus' does not exist on type 'Element | Text'.
32150
- ReactDOM__default["default"].findDOMNode(this.refs.input).focus(); // eslint-disable-line react/no-string-refs
32151
+ this._getInput().focus();
32151
32152
  this._handleFocus();
32152
32153
  };
32153
32154
  blur = () => {
32154
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'blur' does not exist on type 'Element | Text'.
32155
- ReactDOM__default["default"].findDOMNode(this.refs.input).blur(); // eslint-disable-line react/no-string-refs
32155
+ this._getInput().blur();
32156
32156
  this._handleBlur();
32157
32157
  };
32158
32158
  setSelectionRange = (selectionStart, selectionEnd) => {
32159
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'setSelectionRange' does not exist on type 'Element | Text'.
32160
- ReactDOM__default["default"].findDOMNode(this).setSelectionRange(selectionStart, selectionEnd);
32159
+ this._getInput().setSelectionRange(selectionStart, selectionEnd);
32161
32160
  };
32162
32161
  getSelectionStart = () => {
32163
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'.
32164
- return ReactDOM__default["default"].findDOMNode(this).selectionStart;
32162
+ return this._getInput().selectionStart;
32165
32163
  };
32166
32164
  getSelectionEnd = () => {
32167
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionEnd' does not exist on type 'Element | Text'.
32168
- return ReactDOM__default["default"].findDOMNode(this).selectionEnd;
32165
+ return this._getInput().selectionEnd;
32169
32166
  };
32170
32167
  _checkValidity = value => {
32171
32168
  if (value == null) {
@@ -32225,9 +32222,7 @@ class NumberInput extends React__namespace.Component {
32225
32222
  }
32226
32223
  };
32227
32224
  _setValue = (val, format) => {
32228
- // eslint-disable-next-line react/no-string-refs
32229
- // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'val' does not exist on type 'JQueryStatic'.
32230
- $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.input)).val(toNumericString(val, format));
32225
+ this._getInput().value = toNumericString(val, format);
32231
32226
  };
32232
32227
  render() {
32233
32228
  let classes = classNames__default["default"]({
@@ -32251,10 +32246,8 @@ class NumberInput extends React__namespace.Component {
32251
32246
  } = this.props;
32252
32247
  return /*#__PURE__*/React__namespace.createElement("input", _extends({}, restProps, {
32253
32248
  className: classes,
32254
- type: "text"
32255
- // eslint-disable-next-line react/no-string-refs
32256
- ,
32257
- ref: "input",
32249
+ type: "text",
32250
+ ref: this.inputRef,
32258
32251
  onChange: this._handleChange,
32259
32252
  onFocus: this._handleFocus,
32260
32253
  onBlur: this._handleBlur,
@@ -33749,15 +33742,18 @@ function useControlPoint(params) {
33749
33742
  } = useGraphConfig();
33750
33743
  const {
33751
33744
  point,
33752
- sequenceNumber,
33745
+ ariaDescribedBy,
33746
+ ariaLabel,
33747
+ ariaLive = "polite",
33753
33748
  color,
33754
- cursor,
33755
33749
  constrain = p => snap(snapStep, p),
33750
+ cursor,
33751
+ forwardedRef = noop$1,
33752
+ sequenceNumber = 1,
33756
33753
  onMove = noop$1,
33757
33754
  onClick = noop$1,
33758
33755
  onFocus = noop$1,
33759
- onBlur = noop$1,
33760
- forwardedRef = noop$1
33756
+ onBlur = noop$1
33761
33757
  } = params;
33762
33758
  const {
33763
33759
  strings,
@@ -33780,6 +33776,13 @@ function useControlPoint(params) {
33780
33776
  onMove,
33781
33777
  constrainKeyboardMovement: constrain
33782
33778
  });
33779
+
33780
+ // if custom aria label is not provided, will use default of sequence number and point coordinates
33781
+ const pointAriaLabel = ariaLabel || strings.srPointAtCoordinates({
33782
+ num: sequenceNumber,
33783
+ x: srFormatNumber(point[X], locale),
33784
+ y: srFormatNumber(point[Y], locale)
33785
+ });
33783
33786
  React.useLayoutEffect(() => {
33784
33787
  setForwardedRef(forwardedRef, focusableHandleRef.current);
33785
33788
  }, [forwardedRef]);
@@ -33789,15 +33792,9 @@ function useControlPoint(params) {
33789
33792
  tabIndex: disableKeyboardInteraction ? -1 : 0,
33790
33793
  ref: focusableHandleRef,
33791
33794
  role: "button",
33792
- "aria-label": strings.srPointAtCoordinates({
33793
- num: sequenceNumber,
33794
- x: srFormatNumber(point[X], locale),
33795
- y: srFormatNumber(point[Y], locale)
33796
- })
33797
- // aria-live="assertive" causes the new location of the point to be
33798
- // announced immediately on move.
33799
- ,
33800
- "aria-live": "assertive",
33795
+ "aria-describedby": ariaDescribedBy,
33796
+ "aria-label": pointAriaLabel,
33797
+ "aria-live": ariaLive,
33801
33798
  onFocus: event => {
33802
33799
  onFocus(event);
33803
33800
  setFocused(true);
@@ -35095,6 +35092,7 @@ const MafsGraph = props => {
35095
35092
  const uniqueId = React__namespace.useId();
35096
35093
  const descriptionId = `interactive-graph-description-${uniqueId}`;
35097
35094
  const interactiveElementsDescriptionId = `interactive-graph-interactive-elements-description-${uniqueId}`;
35095
+ const unlimitedGraphKeyboardPromptId = `unlimited-graph-keyboard-prompt-${uniqueId}`;
35098
35096
  const graphRef = React__namespace.useRef(null);
35099
35097
  const {
35100
35098
  analytics
@@ -35167,7 +35165,7 @@ const MafsGraph = props => {
35167
35165
  handleKeyboardEvent(event, state, dispatch);
35168
35166
  },
35169
35167
  "aria-label": fullGraphAriaLabel,
35170
- "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId),
35168
+ "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && "unlimited-graph-keyboard-prompt"),
35171
35169
  ref: graphRef,
35172
35170
  tabIndex: 0,
35173
35171
  onFocus: event => {
@@ -35237,6 +35235,7 @@ const MafsGraph = props => {
35237
35235
  height: height
35238
35236
  }, /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, props.showProtractor && /*#__PURE__*/React__namespace.createElement(Protractor, null), graph)))), interactionPrompt && /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
35239
35237
  style: {
35238
+ display: interactionPrompt ? undefined : "hidden",
35240
35239
  textAlign: "center",
35241
35240
  backgroundColor: "white",
35242
35241
  border: "1px solid #21242C52",
@@ -35248,7 +35247,9 @@ const MafsGraph = props => {
35248
35247
  top: "50%",
35249
35248
  transform: "translateY(-50%)"
35250
35249
  }
35251
- }, /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelMedium, null, strings.graphKeyboardPrompt))), renderGraphControls({
35250
+ }, /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelMedium, {
35251
+ id: unlimitedGraphKeyboardPromptId
35252
+ }, strings.graphKeyboardPrompt))), renderGraphControls({
35252
35253
  state,
35253
35254
  dispatch,
35254
35255
  width,
@@ -35311,6 +35312,10 @@ const renderPolygonGraphControls = props => {
35311
35312
  } = props;
35312
35313
  const shouldShowRemoveButton = showRemovePointButton && focusedPointIndex !== null;
35313
35314
 
35315
+ // We want to disable the closePolygon button when
35316
+ // there are not enough coords to make a polygon
35317
+ const disableCloseButton = coords.length < 3;
35318
+
35314
35319
  // If polygon is closed, show open button.
35315
35320
  // If polygon is open, show close button.
35316
35321
  const polygonButton = closedPolygon ? /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
@@ -35328,12 +35333,12 @@ const renderPolygonGraphControls = props => {
35328
35333
  // Conditional disable when there are less than 3 points in
35329
35334
  // the graph
35330
35335
  ,
35331
- disabled: coords.length < 3,
35336
+ disabled: disableCloseButton,
35332
35337
  style: {
35333
35338
  width: "100%",
35334
35339
  marginLeft: "20px"
35335
35340
  },
35336
- tabIndex: 0,
35341
+ tabIndex: disableCloseButton ? -1 : 0,
35337
35342
  onClick: () => {
35338
35343
  props.dispatch(actions.polygon.closePolygon());
35339
35344
  }
@@ -36257,8 +36262,12 @@ function doMovePoint(state, action) {
36257
36262
  newValue: newValue
36258
36263
  });
36259
36264
 
36260
- // Reject the move if it would cause the sides of the polygon to cross
36261
- if (polygonSidesIntersect(newCoords)) {
36265
+ // Boolean value to track whether we can let the polygon sides interact.
36266
+ // They can interact if it's an unlimited polygon that is open.
36267
+ const polygonSidesCanIntersect = state.numSides === "unlimited" && !state.closedPolygon;
36268
+
36269
+ // Reject the move if it would cause the sides of the polygon to cross.
36270
+ if (!polygonSidesCanIntersect && polygonSidesIntersect(newCoords)) {
36262
36271
  return state;
36263
36272
  }
36264
36273
  return {
@@ -36861,7 +36870,7 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
36861
36870
  });
36862
36871
 
36863
36872
  const eq$1 = Util.eq;
36864
- const deepEq$2 = Util.deepEq;
36873
+ const deepEq$3 = Util.deepEq;
36865
36874
  function scoreInteractiveGraph(userInput, rubric) {
36866
36875
  // None-type graphs are not graded
36867
36876
  if (userInput.type === "none" && rubric.correct.type === "none") {
@@ -36911,7 +36920,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36911
36920
  // If the parabola coefficients match, it's correct.
36912
36921
  const guessCoeffs = getQuadraticCoefficients(userInput.coords);
36913
36922
  const correctCoeffs = getQuadraticCoefficients(rubric.correct.coords);
36914
- if (deepEq$2(guessCoeffs, correctCoeffs)) {
36923
+ if (deepEq$3(guessCoeffs, correctCoeffs)) {
36915
36924
  return {
36916
36925
  type: "points",
36917
36926
  earned: 1,
@@ -36925,7 +36934,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36925
36934
  const canonicalGuessCoeffs = canonicalSineCoefficients$1(guessCoeffs);
36926
36935
  const canonicalCorrectCoeffs = canonicalSineCoefficients$1(correctCoeffs);
36927
36936
  // If the canonical coefficients match, it's correct.
36928
- if (deepEq$2(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
36937
+ if (deepEq$3(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
36929
36938
  return {
36930
36939
  type: "points",
36931
36940
  earned: 1,
@@ -36934,7 +36943,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36934
36943
  };
36935
36944
  }
36936
36945
  } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
36937
- if (deepEq$2(userInput.center, rubric.correct.center) && eq$1(userInput.radius, rubric.correct.radius)) {
36946
+ if (deepEq$3(userInput.center, rubric.correct.center) && eq$1(userInput.radius, rubric.correct.radius)) {
36938
36947
  return {
36939
36948
  type: "points",
36940
36949
  earned: 1,
@@ -36956,7 +36965,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36956
36965
  guess?.sort();
36957
36966
  // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'.
36958
36967
  correct.sort();
36959
- if (deepEq$2(guess, correct)) {
36968
+ if (deepEq$3(guess, correct)) {
36960
36969
  return {
36961
36970
  type: "points",
36962
36971
  earned: 1,
@@ -36978,7 +36987,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36978
36987
  /* exact */
36979
36988
  guess.sort();
36980
36989
  correct.sort();
36981
- match = deepEq$2(guess, correct);
36990
+ match = deepEq$3(guess, correct);
36982
36991
  }
36983
36992
  if (match) {
36984
36993
  return {
@@ -36993,7 +37002,7 @@ function scoreInteractiveGraph(userInput, rubric) {
36993
37002
  let correct = Util.deepClone(rubric.correct.coords);
36994
37003
  guess = ___default["default"].invoke(guess, "sort").sort();
36995
37004
  correct = ___default["default"].invoke(correct, "sort").sort();
36996
- if (deepEq$2(guess, correct)) {
37005
+ if (deepEq$3(guess, correct)) {
36997
37006
  return {
36998
37007
  type: "points",
36999
37008
  earned: 1,
@@ -37004,7 +37013,7 @@ function scoreInteractiveGraph(userInput, rubric) {
37004
37013
  } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
37005
37014
  const guess = userInput.coords;
37006
37015
  const correct = rubric.correct.coords;
37007
- if (deepEq$2(guess[0], correct[0]) && collinear(correct[0], correct[1], guess[1])) {
37016
+ if (deepEq$3(guess[0], correct[0]) && collinear(correct[0], correct[1], guess[1])) {
37008
37017
  return {
37009
37018
  type: "points",
37010
37019
  earned: 1,
@@ -37031,7 +37040,7 @@ function scoreInteractiveGraph(userInput, rubric) {
37031
37040
  /* exact */
37032
37041
  match =
37033
37042
  // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
37034
- deepEq$2(guess[1], correct[1]) &&
37043
+ deepEq$3(guess[1], correct[1]) &&
37035
37044
  // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
37036
37045
  collinear(correct[1], correct[0], guess[0]) &&
37037
37046
  // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
@@ -41293,14 +41302,20 @@ class Measurer extends React__namespace.Component {
41293
41302
  }
41294
41303
  const propUpgrades = {
41295
41304
  "1": v0props => {
41296
- const v1props = ___default["default"](v0props).chain().omit("imageUrl", "imageTop", "imageLeft").extend({
41305
+ const {
41306
+ imageUrl,
41307
+ imageTop,
41308
+ imageLeft,
41309
+ ...rest
41310
+ } = v0props;
41311
+ return {
41312
+ ...rest,
41297
41313
  image: {
41298
- url: v0props.imageUrl,
41299
- top: v0props.imageTop,
41300
- left: v0props.imageLeft
41314
+ url: imageUrl,
41315
+ top: imageTop,
41316
+ left: imageLeft
41301
41317
  }
41302
- }).value();
41303
- return v1props;
41318
+ };
41304
41319
  }
41305
41320
  };
41306
41321
  var Measurer$1 = {
@@ -42834,14 +42849,28 @@ const getPromptJSON$5 = (renderProps, userInput) => {
42834
42849
  };
42835
42850
  };
42836
42851
 
42837
- function scoreOrderer(userInput, rubric) {
42852
+ /**
42853
+ * Checks user input from the orderer widget to see if the user has started
42854
+ * ordering the options, making the widget scorable.
42855
+ * @param userInput
42856
+ * @see `scoreOrderer` for more details.
42857
+ */
42858
+ function validateOrderer(userInput) {
42838
42859
  if (userInput.current.length === 0) {
42839
42860
  return {
42840
42861
  type: "invalid",
42841
42862
  message: null
42842
42863
  };
42843
42864
  }
42844
- const correct = ___default["default"].isEqual(userInput.current, ___default["default"].pluck(rubric.correctOptions, "content"));
42865
+ return null;
42866
+ }
42867
+
42868
+ function scoreOrderer(userInput, rubric) {
42869
+ const validateError = validateOrderer(userInput);
42870
+ if (validateError) {
42871
+ return validateError;
42872
+ }
42873
+ const correct = ___default["default"].isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
42845
42874
  return {
42846
42875
  type: "points",
42847
42876
  earned: correct ? 1 : 0,
@@ -43017,15 +43046,13 @@ class Card extends React__namespace.Component {
43017
43046
  }))));
43018
43047
  }
43019
43048
  }
43020
- const NORMAL = "normal";
43021
- const HORIZONTAL = "horizontal";
43022
43049
  class Orderer extends React__namespace.Component {
43023
43050
  static defaultProps = {
43024
43051
  current: [],
43025
43052
  options: [],
43026
43053
  correctOptions: [],
43027
- height: NORMAL,
43028
- layout: HORIZONTAL,
43054
+ height: "normal",
43055
+ layout: "horizontal",
43029
43056
  linterContext: PerseusLinter.linterContextDefault
43030
43057
  };
43031
43058
  state = {
@@ -43178,7 +43205,7 @@ class Orderer extends React__namespace.Component {
43178
43205
  };
43179
43206
  findCorrectIndex = (draggable, list) => {
43180
43207
  // Find the correct index for a card given the current cards.
43181
- const isHorizontal = this.props.layout === HORIZONTAL;
43208
+ const isHorizontal = this.props.layout === "horizontal";
43182
43209
  // eslint-disable-next-line react/no-string-refs
43183
43210
  // @ts-expect-error - TS2769 - No overload matches this call.
43184
43211
  const $dragList = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.dragList));
@@ -43228,28 +43255,24 @@ class Orderer extends React__namespace.Component {
43228
43255
  if (draggable == null) {
43229
43256
  return false;
43230
43257
  }
43231
- const isHorizontal = this.props.layout === HORIZONTAL,
43232
- // @ts-expect-error - TS2769 - No overload matches this call.
43233
- $draggable = $__default["default"](ReactDOM__default["default"].findDOMNode(draggable)),
43234
- // eslint-disable-next-line react/no-string-refs
43235
- // @ts-expect-error - TS2769 - No overload matches this call.
43236
- $bank = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.bank)),
43237
- // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'.
43238
- draggableOffset = $draggable.offset(),
43239
- // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'.
43240
- bankOffset = $bank.offset(),
43241
- // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'.
43242
- draggableHeight = $draggable.outerHeight(true),
43243
- // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'.
43244
- bankHeight = $bank.outerHeight(true),
43245
- // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'.
43246
- bankWidth = $bank.outerWidth(true),
43247
- // eslint-disable-next-line react/no-string-refs
43248
- dragList = ReactDOM__default["default"].findDOMNode(this.refs.dragList);
43249
- // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'width' does not exist on type 'JQueryStatic'.
43250
- $__default["default"](dragList).width();
43251
- const // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'.
43252
- draggableWidth = $draggable.outerWidth(true);
43258
+ const isHorizontal = this.props.layout === "horizontal";
43259
+ // @ts-expect-error - TS2769 - No overload matches this call.
43260
+ const $draggable = $__default["default"](ReactDOM__default["default"].findDOMNode(draggable));
43261
+ // eslint-disable-next-line react/no-string-refs
43262
+ // @ts-expect-error - TS2769 - No overload matches this call.
43263
+ const $bank = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.bank));
43264
+ // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'.
43265
+ const draggableOffset = $draggable.offset();
43266
+ // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'.
43267
+ const bankOffset = $bank.offset();
43268
+ // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'.
43269
+ const draggableHeight = $draggable.outerHeight(true);
43270
+ // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'.
43271
+ const bankHeight = $bank.outerHeight(true);
43272
+ // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'.
43273
+ const bankWidth = $bank.outerWidth(true);
43274
+ // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'.
43275
+ const draggableWidth = $draggable.outerWidth(true);
43253
43276
  if (isHorizontal) {
43254
43277
  return draggableOffset.top + draggableHeight / 2 < bankOffset.top + bankHeight;
43255
43278
  }
@@ -43668,18 +43691,36 @@ const getPromptJSON$3 = () => {
43668
43691
  };
43669
43692
 
43670
43693
  const {
43671
- deepEq: deepEq$1
43694
+ deepEq: deepEq$2
43672
43695
  } = Util;
43673
- function scorePlotter(userInput, rubric) {
43674
- if (deepEq$1(userInput, rubric.starting)) {
43696
+
43697
+ /**
43698
+ * Checks user input to confirm it is not the same as the starting values for the graph.
43699
+ * This means the user has modified the graph, and the question can be scored.
43700
+ *
43701
+ * @see 'scorePlotter' for more details on scoring.
43702
+ */
43703
+ function validatePlotter(userInput, validationData) {
43704
+ if (deepEq$2(userInput, validationData.starting)) {
43675
43705
  return {
43676
43706
  type: "invalid",
43677
43707
  message: null
43678
43708
  };
43679
43709
  }
43710
+ return null;
43711
+ }
43712
+
43713
+ const {
43714
+ deepEq: deepEq$1
43715
+ } = Util;
43716
+ function scorePlotter(userInput, scoringData) {
43717
+ const validationError = validatePlotter(userInput, scoringData);
43718
+ if (validationError) {
43719
+ return validationError;
43720
+ }
43680
43721
  return {
43681
43722
  type: "points",
43682
- earned: deepEq$1(userInput, rubric.correct) ? 1 : 0,
43723
+ earned: deepEq$1(userInput, scoringData.correct) ? 1 : 0,
43683
43724
  total: 1,
43684
43725
  message: null
43685
43726
  };
@@ -44566,7 +44607,14 @@ const getPromptJSON$1 = userInput => {
44566
44607
  };
44567
44608
  };
44568
44609
 
44569
- function scoreSorter(userInput, rubric) {
44610
+ /**
44611
+ * Checks user input for the sorter widget to ensure that the user has made
44612
+ * changes before attempting to score the widget.
44613
+ * @param userInput
44614
+ * @see 'scoreSorter' in 'packages/perseus/src/widgets/sorter/score-sorter.ts'
44615
+ * for more details on how the sorter widget is scored.
44616
+ */
44617
+ function validateSorter(userInput) {
44570
44618
  // If the sorter widget hasn't been changed yet, we treat it as "empty" which
44571
44619
  // prevents the "Check" button from becoming active. We want the user
44572
44620
  // to make a change before trying to move forward. This makes an
@@ -44579,6 +44627,14 @@ function scoreSorter(userInput, rubric) {
44579
44627
  message: null
44580
44628
  };
44581
44629
  }
44630
+ return null;
44631
+ }
44632
+
44633
+ function scoreSorter(userInput, rubric) {
44634
+ const validationError = validateSorter(userInput);
44635
+ if (validationError) {
44636
+ return validationError;
44637
+ }
44582
44638
  const correct = Util.deepEq(userInput.options, rubric.correct);
44583
44639
  return {
44584
44640
  type: "points",
@@ -45153,7 +45209,7 @@ const init = function () {
45153
45209
 
45154
45210
  // This file is processed by a Rollup plugin (replace) to inject the production
45155
45211
  const libName = "@khanacademy/perseus";
45156
- const libVersion = "43.0.0";
45212
+ const libVersion = "43.1.0";
45157
45213
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45158
45214
 
45159
45215
  const apiVersion = {
@@ -45438,7 +45494,6 @@ const styles$3 = aphrodite.StyleSheet.create({
45438
45494
  }
45439
45495
  });
45440
45496
 
45441
- /* eslint-disable @khanacademy/ts-no-error-suppressions */
45442
45497
  // Use of UNSAFE_componentWillReceiveProps:
45443
45498
  // eslint-disable-next-line react/no-unsafe
45444
45499
  class HintsRenderer extends React__namespace.Component {
@@ -48664,6 +48719,27 @@ function pair(parseA, parseB) {
48664
48719
  };
48665
48720
  }
48666
48721
 
48722
+ function pipeParsers(p) {
48723
+ return new ParserPipeline(p);
48724
+ }
48725
+ class ParserPipeline {
48726
+ constructor(parser) {
48727
+ this.parser = parser;
48728
+ }
48729
+ then(nextParser) {
48730
+ return new ParserPipeline(composeParsers(this.parser, nextParser));
48731
+ }
48732
+ }
48733
+ function composeParsers(parserA, parserB) {
48734
+ return (rawValue, ctx) => {
48735
+ const partialResult = parserA(rawValue, ctx);
48736
+ if (isFailure(partialResult)) {
48737
+ return partialResult;
48738
+ }
48739
+ return parserB(partialResult.value, ctx);
48740
+ };
48741
+ }
48742
+
48667
48743
  function record(parseKey, parseValue) {
48668
48744
  return (rawValue, ctx) => {
48669
48745
  if (!isObject(rawValue)) {
@@ -48744,6 +48820,20 @@ function either(parseA, parseB) {
48744
48820
  };
48745
48821
  }
48746
48822
 
48823
+ function defaulted(parser, fallback) {
48824
+ return (rawValue, ctx) => {
48825
+ if (rawValue == null) {
48826
+ return success(fallback(rawValue));
48827
+ }
48828
+ return parser(rawValue, ctx);
48829
+ };
48830
+ }
48831
+
48832
+ const parseImages = defaulted(record(string, object({
48833
+ width: number,
48834
+ height: number
48835
+ })), () => ({}));
48836
+
48747
48837
  function parseWidget(parseType, parseOptions) {
48748
48838
  return object({
48749
48839
  type: parseType,
@@ -48752,10 +48842,21 @@ function parseWidget(parseType, parseOptions) {
48752
48842
  alignment: optional(string),
48753
48843
  options: parseOptions,
48754
48844
  key: optional(number),
48755
- version: object({
48845
+ version: optional(object({
48756
48846
  major: number,
48757
48847
  minor: number
48758
- })
48848
+ }))
48849
+ });
48850
+ }
48851
+ function parseWidgetWithVersion(parseVersion, parseType, parseOptions) {
48852
+ return object({
48853
+ type: parseType,
48854
+ static: optional(boolean),
48855
+ graded: optional(boolean),
48856
+ alignment: optional(string),
48857
+ options: parseOptions,
48858
+ key: optional(number),
48859
+ version: parseVersion
48759
48860
  });
48760
48861
  }
48761
48862
 
@@ -48763,7 +48864,7 @@ const parseCategorizerWidget = parseWidget(constant("categorizer"), object({
48763
48864
  items: array(string),
48764
48865
  categories: array(string),
48765
48866
  randomizeItems: boolean,
48766
- static: boolean,
48867
+ static: defaulted(boolean, () => false),
48767
48868
  values: array(number),
48768
48869
  highlightLint: optional(boolean),
48769
48870
  linterContext: optional(object({
@@ -48795,7 +48896,7 @@ const parseDefinitionWidget = parseWidget(constant("definition"), object({
48795
48896
 
48796
48897
  const parseDropdownWidget = parseWidget(constant("dropdown"), object({
48797
48898
  placeholder: string,
48798
- static: boolean,
48899
+ static: defaulted(boolean, () => false),
48799
48900
  choices: array(object({
48800
48901
  content: string,
48801
48902
  correct: boolean
@@ -48818,9 +48919,12 @@ const parseAnswerForm = object({
48818
48919
  form: boolean,
48819
48920
  simplify: boolean,
48820
48921
  considered: enumeration("correct", "wrong", "ungraded"),
48821
- key: optional(string)
48922
+ key: pipeParsers(optional(union(string).or(number).parser)).then((key, ctx) => ctx.success(String(key))).parser
48822
48923
  });
48823
- const parseExpressionWidget = parseWidget(constant("expression"), object({
48924
+ const parseExpressionWidgetV1 = parseWidgetWithVersion(object({
48925
+ major: constant(1),
48926
+ minor: number
48927
+ }), constant("expression"), object({
48824
48928
  answerForms: array(parseAnswerForm),
48825
48929
  functions: array(string),
48826
48930
  times: boolean,
@@ -48829,23 +48933,69 @@ const parseExpressionWidget = parseWidget(constant("expression"), object({
48829
48933
  buttonSets: array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")),
48830
48934
  buttonsVisible: optional(enumeration("always", "never", "focused"))
48831
48935
  }));
48936
+ const parseExpressionWidgetV0 = parseWidgetWithVersion(optional(object({
48937
+ major: constant(0),
48938
+ minor: number
48939
+ })), constant("expression"), object({
48940
+ functions: array(string),
48941
+ times: boolean,
48942
+ visibleLabel: optional(string),
48943
+ ariaLabel: optional(string),
48944
+ form: boolean,
48945
+ simplify: boolean,
48946
+ value: string,
48947
+ buttonSets: array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")),
48948
+ buttonsVisible: optional(enumeration("always", "never", "focused"))
48949
+ }));
48950
+ function migrateV0ToV1(widget, ctx) {
48951
+ const {
48952
+ options
48953
+ } = widget;
48954
+ return ctx.success({
48955
+ ...widget,
48956
+ version: {
48957
+ major: 1,
48958
+ minor: 0
48959
+ },
48960
+ options: {
48961
+ times: options.times,
48962
+ buttonSets: options.buttonSets,
48963
+ functions: options.functions,
48964
+ buttonsVisible: options.buttonsVisible,
48965
+ visibleLabel: options.visibleLabel,
48966
+ ariaLabel: options.ariaLabel,
48967
+ answerForms: [{
48968
+ considered: "correct",
48969
+ form: options.form,
48970
+ simplify: options.simplify,
48971
+ value: options.value
48972
+ }]
48973
+ }
48974
+ });
48975
+ }
48976
+ const parseExpressionWidget = union(parseExpressionWidgetV1).or(pipeParsers(parseExpressionWidgetV0).then(migrateV0ToV1).parser).parser;
48832
48977
 
48833
- const parsePerseusRenderer = object({
48834
- content: string,
48978
+ const parsePerseusRenderer = defaulted(object({
48979
+ // TODO(benchristel): content is also defaulted to empty string in
48980
+ // renderer.tsx. See if we can remove one default or the other.
48981
+ content: defaulted(string, () => ""),
48835
48982
  // This module has an import cycle with parseWidgetsMap, because the
48836
48983
  // `group` widget can contain another renderer.
48837
48984
  // The anonymous function below ensures that we don't try to access
48838
48985
  // parseWidgetsMap before it's defined.
48839
- widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
48986
+ widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
48840
48987
  metadata: optional(array(string)),
48841
- images: record(string, object({
48842
- width: number,
48843
- height: number
48844
- }))
48845
- });
48988
+ images: parseImages
48989
+ }),
48990
+ // Default value
48991
+ () => ({
48992
+ content: "",
48993
+ widgets: {},
48994
+ images: {}
48995
+ }));
48846
48996
 
48847
48997
  const parseGradedGroupWidgetOptions = object({
48848
- title: string,
48998
+ title: defaulted(string, () => ""),
48849
48999
  hasHint: optional(nullable(boolean)),
48850
49000
  // This module has an import cycle with parsePerseusRenderer.
48851
49001
  // The anonymous function below ensures that we don't try to access
@@ -48939,7 +49089,7 @@ const parseIframeWidget = parseWidget(constant("iframe"), object({
48939
49089
  height: union(number).or(string).parser,
48940
49090
  allowFullScreen: boolean,
48941
49091
  allowTopNavigation: optional(boolean),
48942
- static: boolean
49092
+ static: defaulted(boolean, () => false)
48943
49093
  }));
48944
49094
 
48945
49095
  const parsePerseusImageBackground = object({
@@ -49318,13 +49468,13 @@ const parseMatcherWidget = parseWidget(constant("matcher"), object({
49318
49468
  padding: boolean
49319
49469
  }));
49320
49470
 
49321
- const parseMatrixWidget = parseWidget(constant("matrix"), object({
49322
- prefix: string,
49323
- suffix: string,
49471
+ const parseMatrixWidget = parseWidget(defaulted(constant("matrix"), () => "matrix"), object({
49472
+ prefix: optional(string),
49473
+ suffix: optional(string),
49324
49474
  answers: array(array(number)),
49325
- cursorPosition: array(number),
49475
+ cursorPosition: optional(array(number)),
49326
49476
  matrixBoardSize: array(number),
49327
- static: boolean
49477
+ static: optional(boolean)
49328
49478
  }));
49329
49479
 
49330
49480
  const parseMeasurerWidget = parseWidget(constant("measurer"), object({
@@ -49394,8 +49544,8 @@ const parseOrdererWidget = parseWidget(constant("orderer"), object({
49394
49544
  options: array(parseRenderer),
49395
49545
  correctOptions: array(parseRenderer),
49396
49546
  otherOptions: array(parseRenderer),
49397
- height: string,
49398
- layout: string
49547
+ height: enumeration("normal", "auto"),
49548
+ layout: enumeration("horizontal", "vertical")
49399
49549
  }));
49400
49550
 
49401
49551
  const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
@@ -49440,7 +49590,7 @@ const parsePythonProgramWidget = parseWidget(constant("python-program"), object(
49440
49590
 
49441
49591
  const parseRadioWidget = parseWidget(constant("radio"), object({
49442
49592
  choices: array(object({
49443
- content: string,
49593
+ content: defaulted(string, () => ""),
49444
49594
  clue: optional(string),
49445
49595
  correct: optional(boolean),
49446
49596
  isNoneOfTheAbove: optional(boolean),
@@ -49602,26 +49752,45 @@ const parseWidgetMapKeyComponents = pair(string, parseStringToPositiveInt);
49602
49752
  const parseHint = object({
49603
49753
  replace: optional(boolean),
49604
49754
  content: string,
49605
- widgets: parseWidgetsMap,
49755
+ widgets: defaulted(parseWidgetsMap, () => ({})),
49606
49756
  metadata: optional(array(string)),
49607
- images: record(string, object({
49608
- width: number,
49609
- height: number
49610
- }))
49757
+ images: parseImages
49611
49758
  });
49612
49759
 
49613
49760
  object({
49614
49761
  question: parsePerseusRenderer,
49615
- hints: array(parseHint),
49616
- answerArea: record(enumeration(...ItemExtras), boolean),
49617
- itemDataVersion: object({
49762
+ hints: defaulted(array(parseHint), () => []),
49763
+ answerArea: pipeParsers(defaulted(object({}), () => ({}))).then(migrateAnswerArea).then(record(enumeration(...ItemExtras), boolean)).parser,
49764
+ itemDataVersion: optional(object({
49618
49765
  major: number,
49619
49766
  minor: number
49620
- }),
49767
+ })),
49621
49768
  // Deprecated field
49622
49769
  answer: any
49623
49770
  });
49624
49771
 
49772
+ // Some answerAreas have extra fields, like:
49773
+ //
49774
+ // "answerArea": {
49775
+ // "type": "multiple",
49776
+ // "options": {
49777
+ // "content": "",
49778
+ // "images": {},
49779
+ // "widgets": {}
49780
+ // }
49781
+ // }
49782
+ //
49783
+ // The "type" and "options" fields don't seem to be used anywhere. This
49784
+ // migration function removes them.
49785
+ function migrateAnswerArea(rawValue, ctx) {
49786
+ const {
49787
+ type: _,
49788
+ options: __,
49789
+ ...rest
49790
+ } = rawValue;
49791
+ return ctx.success(rest);
49792
+ }
49793
+
49625
49794
  /**
49626
49795
  * Helper to parse PerseusItem JSON
49627
49796
  * Why not just use JSON.parse? We want: