@khanacademy/perseus-score 2.0.0 → 2.2.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/es/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as KAS from '@khanacademy/kas';
2
2
  import { KhanMath, number, geometry, angles, coefficients } from '@khanacademy/kmath';
3
- import { PerseusError, Errors, getDecimalSeparator, GrapherUtil, approximateDeepEqual, approximateEqual, deepClone, getMatrixSize } from '@khanacademy/perseus-core';
3
+ import { PerseusError, Errors, getDecimalSeparator, GrapherUtil, approximateDeepEqual, approximateEqual, deepClone, getMatrixSize, getWidgetIdsFromContent, getUpgradedWidgetOptions } from '@khanacademy/perseus-core';
4
4
  import { ErrorCodes as ErrorCodes$1 } from '@khanacademy/perseus-score';
5
5
 
6
6
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
@@ -12526,6 +12526,21 @@ const KhanAnswerTypes = {
12526
12526
  }
12527
12527
  };
12528
12528
 
12529
+ function scoreCategorizer(userInput, rubric) {
12530
+ let allCorrect = true;
12531
+ rubric.values.forEach((value, i) => {
12532
+ if (userInput.values[i] !== value) {
12533
+ allCorrect = false;
12534
+ }
12535
+ });
12536
+ return {
12537
+ type: "points",
12538
+ earned: allCorrect ? 1 : 0,
12539
+ total: 1,
12540
+ message: null
12541
+ };
12542
+ }
12543
+
12529
12544
  /**
12530
12545
  * Checks userInput from the categorizer widget to see if the user has selected
12531
12546
  * a category for each item.
@@ -12545,25 +12560,6 @@ function validateCategorizer(userInput, validationData) {
12545
12560
  return null;
12546
12561
  }
12547
12562
 
12548
- function scoreCategorizer(userInput, rubric) {
12549
- const validationError = validateCategorizer(userInput, rubric);
12550
- if (validationError) {
12551
- return validationError;
12552
- }
12553
- let allCorrect = true;
12554
- rubric.values.forEach((value, i) => {
12555
- if (userInput.values[i] !== value) {
12556
- allCorrect = false;
12557
- }
12558
- });
12559
- return {
12560
- type: "points",
12561
- earned: allCorrect ? 1 : 0,
12562
- total: 1,
12563
- message: null
12564
- };
12565
- }
12566
-
12567
12563
  function scoreCSProgram(userInput) {
12568
12564
  // The CS program can tell us whether it's correct or incorrect,
12569
12565
  // and pass an optional message
@@ -12589,25 +12585,7 @@ function scoreCSProgram(userInput) {
12589
12585
  };
12590
12586
  }
12591
12587
 
12592
- /**
12593
- * Checks if the user has selected an item from the dropdown before scoring.
12594
- * This is shown with a userInput value / index other than 0.
12595
- */
12596
- function validateDropdown(userInput) {
12597
- if (userInput.value === 0) {
12598
- return {
12599
- type: "invalid",
12600
- message: null
12601
- };
12602
- }
12603
- return null;
12604
- }
12605
-
12606
12588
  function scoreDropdown(userInput, rubric) {
12607
- const validationError = validateDropdown(userInput);
12608
- if (validationError) {
12609
- return validationError;
12610
- }
12611
12589
  const correct = rubric.choices[userInput.value - 1].correct;
12612
12590
  return {
12613
12591
  type: "points",
@@ -12618,15 +12596,11 @@ function scoreDropdown(userInput, rubric) {
12618
12596
  }
12619
12597
 
12620
12598
  /**
12621
- * Checks user input from the expression widget to see if it is scorable.
12622
- *
12623
- * Note: Most of the expression widget's validation requires the Rubric because
12624
- * of its use of KhanAnswerTypes as a core part of scoring.
12625
- *
12626
- * @see `scoreExpression()` for more details.
12599
+ * Checks if the user has selected an item from the dropdown before scoring.
12600
+ * This is shown with a userInput value / index other than 0.
12627
12601
  */
12628
- function validateExpression(userInput) {
12629
- if (userInput === "") {
12602
+ function validateDropdown(userInput) {
12603
+ if (userInput.value === 0) {
12630
12604
  return {
12631
12605
  type: "invalid",
12632
12606
  message: null
@@ -12654,10 +12628,6 @@ function validateExpression(userInput) {
12654
12628
  * - Otherwise, pass through the resulting points and message.
12655
12629
  */
12656
12630
  function scoreExpression(userInput, rubric, locale) {
12657
- const validationError = validateExpression(userInput);
12658
- if (validationError) {
12659
- return validationError;
12660
- }
12661
12631
  const options = _.clone(rubric);
12662
12632
  _.extend(options, {
12663
12633
  decimal_separator: getDecimalSeparator(locale)
@@ -12772,6 +12742,24 @@ function scoreExpression(userInput, rubric, locale) {
12772
12742
  };
12773
12743
  }
12774
12744
 
12745
+ /**
12746
+ * Checks user input from the expression widget to see if it is scorable.
12747
+ *
12748
+ * Note: Most of the expression widget's validation requires the Rubric because
12749
+ * of its use of KhanAnswerTypes as a core part of scoring.
12750
+ *
12751
+ * @see `scoreExpression()` for more details.
12752
+ */
12753
+ function validateExpression(userInput) {
12754
+ if (userInput === "") {
12755
+ return {
12756
+ type: "invalid",
12757
+ message: null
12758
+ };
12759
+ }
12760
+ return null;
12761
+ }
12762
+
12775
12763
  function getCoefficientsByType(data) {
12776
12764
  if (data.coords == null) {
12777
12765
  return undefined;
@@ -12859,7 +12847,8 @@ function scoreIframe(userInput) {
12859
12847
  const {
12860
12848
  collinear,
12861
12849
  canonicalSineCoefficients,
12862
- similar
12850
+ similar,
12851
+ clockwise
12863
12852
  } = geometry;
12864
12853
  const {
12865
12854
  getClockwiseAngle
@@ -13019,9 +13008,27 @@ function scoreInteractiveGraph(userInput, rubric) {
13019
13008
  };
13020
13009
  }
13021
13010
  } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
13022
- const guess = userInput.coords;
13011
+ const coords = userInput.coords;
13023
13012
  const correct = rubric.correct.coords;
13024
13013
  const allowReflexAngles = rubric.correct.allowReflexAngles;
13014
+
13015
+ // While the angle graph should always have 3 points, our types
13016
+ // technically allow for null values. We'll check for that here.
13017
+ // TODO: (LEMS-2857) We would like to update the type of coords
13018
+ // to be non-nullable, as the graph should always have 3 points.
13019
+ if (!coords) {
13020
+ return {
13021
+ type: "invalid",
13022
+ message: null
13023
+ };
13024
+ }
13025
+
13026
+ // We need to check both the direction of the angle and the
13027
+ // whether the graph allows for reflexive angles in order to
13028
+ // to determine if we need to reverse the coords for scoring.
13029
+ const areClockwise = clockwise([coords[0], coords[2], coords[1]]);
13030
+ const shouldReverseCoords = areClockwise && !allowReflexAngles;
13031
+ const guess = shouldReverseCoords ? coords.slice().reverse() : coords;
13025
13032
  let match;
13026
13033
  if (rubric.correct.match === "congruent") {
13027
13034
  const angles = _.map([guess, correct], function (coords) {
@@ -13035,13 +13042,7 @@ function scoreInteractiveGraph(userInput, rubric) {
13035
13042
  match = approximateEqual(...angles);
13036
13043
  } else {
13037
13044
  /* exact */
13038
- match =
13039
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
13040
- approximateDeepEqual(guess[1], correct[1]) &&
13041
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
13042
- collinear(correct[1], correct[0], guess[0]) &&
13043
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
13044
- collinear(correct[1], correct[2], guess[2]);
13045
+ match = approximateDeepEqual(guess[1], correct[1]) && collinear(correct[1], correct[0], guess[0]) && collinear(correct[1], correct[2], guess[2]);
13045
13046
  }
13046
13047
  if (match) {
13047
13048
  return {
@@ -13071,24 +13072,6 @@ function scoreInteractiveGraph(userInput, rubric) {
13071
13072
  };
13072
13073
  }
13073
13074
 
13074
- function validateLabelImage(userInput) {
13075
- let numAnswered = 0;
13076
- for (let i = 0; i < userInput.markers.length; i++) {
13077
- const userSelection = userInput.markers[i].selected;
13078
- if (userSelection && userSelection.length > 0) {
13079
- numAnswered++;
13080
- }
13081
- }
13082
- // We expect all question markers to be answered before grading.
13083
- if (numAnswered !== userInput.markers.length) {
13084
- return {
13085
- type: "invalid",
13086
- message: null
13087
- };
13088
- }
13089
- return null;
13090
- }
13091
-
13092
13075
  // Question state for marker as result of user selected answers.
13093
13076
 
13094
13077
  function scoreLabelImageMarker(userInput, rubric) {
@@ -13111,10 +13094,6 @@ function scoreLabelImageMarker(userInput, rubric) {
13111
13094
  return score;
13112
13095
  }
13113
13096
  function scoreLabelImage(userInput, rubric) {
13114
- const validationError = validateLabelImage(userInput);
13115
- if (validationError) {
13116
- return validationError;
13117
- }
13118
13097
  let numCorrect = 0;
13119
13098
  for (let i = 0; i < userInput.markers.length; i++) {
13120
13099
  const score = scoreLabelImageMarker(userInput.markers[i].selected, rubric.markers[i].answers);
@@ -13142,35 +13121,7 @@ function scoreMatcher(userInput, rubric) {
13142
13121
  };
13143
13122
  }
13144
13123
 
13145
- /**
13146
- * Checks user input from the matrix widget to see if it is scorable.
13147
- *
13148
- * Note: The matrix widget cannot do much validation without the Scoring
13149
- * Data because of its use of KhanAnswerTypes as a core part of scoring.
13150
- *
13151
- * @see `scoreMatrix()` for more details.
13152
- */
13153
- function validateMatrix(userInput) {
13154
- const supplied = userInput.answers;
13155
- const suppliedSize = getMatrixSize(supplied);
13156
- for (let row = 0; row < suppliedSize[0]; row++) {
13157
- for (let col = 0; col < suppliedSize[1]; col++) {
13158
- if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
13159
- return {
13160
- type: "invalid",
13161
- message: ErrorCodes.FILL_ALL_CELLS_ERROR
13162
- };
13163
- }
13164
- }
13165
- }
13166
- return null;
13167
- }
13168
-
13169
13124
  function scoreMatrix(userInput, rubric) {
13170
- const validationError = validateMatrix(userInput);
13171
- if (validationError != null) {
13172
- return validationError;
13173
- }
13174
13125
  const solution = rubric.answers;
13175
13126
  const supplied = userInput.answers;
13176
13127
  const solutionSize = getMatrixSize(solution);
@@ -13215,30 +13166,30 @@ function scoreMatrix(userInput, rubric) {
13215
13166
  }
13216
13167
 
13217
13168
  /**
13218
- * Checks user input is within the allowed range and not the same as the initial
13219
- * state.
13220
- * @param userInput
13221
- * @see 'scoreNumberLine' for the scoring logic.
13169
+ * Checks user input from the matrix widget to see if it is scorable.
13170
+ *
13171
+ * Note: The matrix widget cannot do much validation without the Scoring
13172
+ * Data because of its use of KhanAnswerTypes as a core part of scoring.
13173
+ *
13174
+ * @see `scoreMatrix()` for more details.
13222
13175
  */
13223
- function validateNumberLine(userInput) {
13224
- const divisionRange = userInput.divisionRange;
13225
- const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
13226
-
13227
- // TODO: I don't think isTickCrtl is a thing anymore
13228
- if (userInput.isTickCrtl && outsideAllowedRange) {
13229
- return {
13230
- type: "invalid",
13231
- message: "Number of divisions is outside the allowed range."
13232
- };
13176
+ function validateMatrix(userInput) {
13177
+ const supplied = userInput.answers;
13178
+ const suppliedSize = getMatrixSize(supplied);
13179
+ for (let row = 0; row < suppliedSize[0]; row++) {
13180
+ for (let col = 0; col < suppliedSize[1]; col++) {
13181
+ if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
13182
+ return {
13183
+ type: "invalid",
13184
+ message: ErrorCodes.FILL_ALL_CELLS_ERROR
13185
+ };
13186
+ }
13187
+ }
13233
13188
  }
13234
13189
  return null;
13235
13190
  }
13236
13191
 
13237
13192
  function scoreNumberLine(userInput, rubric) {
13238
- const validationError = validateNumberLine(userInput);
13239
- if (validationError) {
13240
- return validationError;
13241
- }
13242
13193
  const range = rubric.range;
13243
13194
  const start = rubric.initialX != null ? rubric.initialX : range[0];
13244
13195
  const startRel = rubric.isInequality ? "ge" : "eq";
@@ -13267,6 +13218,26 @@ function scoreNumberLine(userInput, rubric) {
13267
13218
  };
13268
13219
  }
13269
13220
 
13221
+ /**
13222
+ * Checks user input is within the allowed range and not the same as the initial
13223
+ * state.
13224
+ * @param userInput
13225
+ * @see 'scoreNumberLine' for the scoring logic.
13226
+ */
13227
+ function validateNumberLine(userInput) {
13228
+ const divisionRange = userInput.divisionRange;
13229
+ const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
13230
+
13231
+ // TODO: I don't think isTickCrtl is a thing anymore
13232
+ if (userInput.isTickCrtl && outsideAllowedRange) {
13233
+ return {
13234
+ type: "invalid",
13235
+ message: "Number of divisions is outside the allowed range."
13236
+ };
13237
+ }
13238
+ return null;
13239
+ }
13240
+
13270
13241
  function _extends() {
13271
13242
  return _extends = Object.assign ? Object.assign.bind() : function (n) {
13272
13243
  for (var e = 1; e < arguments.length; e++) {
@@ -13540,27 +13511,7 @@ function scoreNumericInput(userInput, rubric) {
13540
13511
  };
13541
13512
  }
13542
13513
 
13543
- /**
13544
- * Checks user input from the orderer widget to see if the user has started
13545
- * ordering the options, making the widget scorable.
13546
- * @param userInput
13547
- * @see `scoreOrderer` for more details.
13548
- */
13549
- function validateOrderer(userInput) {
13550
- if (userInput.current.length === 0) {
13551
- return {
13552
- type: "invalid",
13553
- message: null
13554
- };
13555
- }
13556
- return null;
13557
- }
13558
-
13559
13514
  function scoreOrderer(userInput, rubric) {
13560
- const validationError = validateOrderer(userInput);
13561
- if (validationError) {
13562
- return validationError;
13563
- }
13564
13515
  const correct = _.isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
13565
13516
  return {
13566
13517
  type: "points",
@@ -13571,13 +13522,13 @@ function scoreOrderer(userInput, rubric) {
13571
13522
  }
13572
13523
 
13573
13524
  /**
13574
- * Checks user input to confirm it is not the same as the starting values for the graph.
13575
- * This means the user has modified the graph, and the question can be scored.
13576
- *
13577
- * @see 'scorePlotter' for more details on scoring.
13525
+ * Checks user input from the orderer widget to see if the user has started
13526
+ * ordering the options, making the widget scorable.
13527
+ * @param userInput
13528
+ * @see `scoreOrderer` for more details.
13578
13529
  */
13579
- function validatePlotter(userInput, validationData) {
13580
- if (approximateDeepEqual(userInput, validationData.starting)) {
13530
+ function validateOrderer(userInput) {
13531
+ if (userInput.current.length === 0) {
13581
13532
  return {
13582
13533
  type: "invalid",
13583
13534
  message: null
@@ -13587,10 +13538,6 @@ function validatePlotter(userInput, validationData) {
13587
13538
  }
13588
13539
 
13589
13540
  function scorePlotter(userInput, rubric) {
13590
- const validationError = validatePlotter(userInput, rubric);
13591
- if (validationError) {
13592
- return validationError;
13593
- }
13594
13541
  return {
13595
13542
  type: "points",
13596
13543
  earned: approximateDeepEqual(userInput, rubric.correct) ? 1 : 0,
@@ -13600,18 +13547,13 @@ function scorePlotter(userInput, rubric) {
13600
13547
  }
13601
13548
 
13602
13549
  /**
13603
- * Checks if the user has selected at least one option. Additional validation
13604
- * is done in scoreRadio to check if the number of selected options is correct
13605
- * and if the user has selected both a correct option and the "none of the above"
13606
- * option.
13607
- * @param userInput
13608
- * @see `scoreRadio` for the additional validation logic and the scoring logic.
13550
+ * Checks user input to confirm it is not the same as the starting values for the graph.
13551
+ * This means the user has modified the graph, and the question can be scored.
13552
+ *
13553
+ * @see 'scorePlotter' for more details on scoring.
13609
13554
  */
13610
- function validateRadio(userInput) {
13611
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
13612
- return sum + (selected ? 1 : 0);
13613
- }, 0);
13614
- if (numSelected === 0) {
13555
+ function validatePlotter(userInput, validationData) {
13556
+ if (approximateDeepEqual(userInput, validationData.starting)) {
13615
13557
  return {
13616
13558
  type: "invalid",
13617
13559
  message: null
@@ -13621,10 +13563,6 @@ function validateRadio(userInput) {
13621
13563
  }
13622
13564
 
13623
13565
  function scoreRadio(userInput, rubric) {
13624
- const validationError = validateRadio(userInput);
13625
- if (validationError) {
13626
- return validationError;
13627
- }
13628
13566
  const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
13629
13567
  return sum + (selected ? 1 : 0);
13630
13568
  }, 0);
@@ -13665,6 +13603,37 @@ function scoreRadio(userInput, rubric) {
13665
13603
  };
13666
13604
  }
13667
13605
 
13606
+ /**
13607
+ * Checks if the user has selected at least one option. Additional validation
13608
+ * is done in scoreRadio to check if the number of selected options is correct
13609
+ * and if the user has selected both a correct option and the "none of the above"
13610
+ * option.
13611
+ * @param userInput
13612
+ * @see `scoreRadio` for the additional validation logic and the scoring logic.
13613
+ */
13614
+ function validateRadio(userInput) {
13615
+ const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
13616
+ return sum + (selected ? 1 : 0);
13617
+ }, 0);
13618
+ if (numSelected === 0) {
13619
+ return {
13620
+ type: "invalid",
13621
+ message: null
13622
+ };
13623
+ }
13624
+ return null;
13625
+ }
13626
+
13627
+ function scoreSorter(userInput, rubric) {
13628
+ const correct = approximateDeepEqual(userInput.options, rubric.correct);
13629
+ return {
13630
+ type: "points",
13631
+ earned: correct ? 1 : 0,
13632
+ total: 1,
13633
+ message: null
13634
+ };
13635
+ }
13636
+
13668
13637
  /**
13669
13638
  * Checks user input for the sorter widget to ensure that the user has made
13670
13639
  * changes before attempting to score the widget.
@@ -13688,20 +13657,6 @@ function validateSorter(userInput) {
13688
13657
  return null;
13689
13658
  }
13690
13659
 
13691
- function scoreSorter(userInput, rubric) {
13692
- const validationError = validateSorter(userInput);
13693
- if (validationError) {
13694
- return validationError;
13695
- }
13696
- const correct = approximateDeepEqual(userInput.options, rubric.correct);
13697
- return {
13698
- type: "points",
13699
- earned: correct ? 1 : 0,
13700
- total: 1,
13701
- message: null
13702
- };
13703
- }
13704
-
13705
13660
  /**
13706
13661
  * Filters the given table (modelled as a 2D array) to remove any rows that are
13707
13662
  * completely empty.
@@ -13845,5 +13800,282 @@ function scoreInputNumber(userInput, rubric) {
13845
13800
  };
13846
13801
  }
13847
13802
 
13848
- export { ErrorCodes, KhanAnswerTypes, inputNumberAnswerTypes, scoreCSProgram, scoreCategorizer, scoreDropdown, scoreExpression, scoreGrapher, scoreIframe, scoreInputNumber, scoreInteractiveGraph, scoreLabelImage, scoreLabelImageMarker, scoreMatcher, scoreMatrix, scoreNumberLine, scoreNumericInput, scoreOrderer, scorePlotter, scoreRadio, scoreSorter, scoreTable, validateCategorizer, validateDropdown, validateExpression, validateMatrix, validateNumberLine, validateOrderer, validatePlotter, validateRadio, validateSorter, validateTable };
13803
+ /**
13804
+ * Several widgets don't have "right"/"wrong" scoring logic,
13805
+ * so this just says to move on past those widgets
13806
+ *
13807
+ * TODO(LEMS-2543) widgets that use this probably shouldn't have any
13808
+ * scoring logic and the thing scoring an exercise
13809
+ * should just know to skip these
13810
+ */
13811
+ function scoreNoop(points = 0) {
13812
+ return {
13813
+ type: "points",
13814
+ earned: points,
13815
+ total: points,
13816
+ message: null
13817
+ };
13818
+ }
13819
+
13820
+ // The `group` widget is basically a widget hosting a full Perseus system in
13821
+ // it. As such, scoring a group means scoring all widgets it contains.
13822
+ function scoreGroup(userInput, rubric, locale) {
13823
+ const scores = scoreWidgetsFunctional(rubric.widgets, Object.keys(rubric.widgets), userInput, locale);
13824
+ return flattenScores(scores);
13825
+ }
13826
+
13827
+ /**
13828
+ * Checks the given user input to see if any answerable widgets have not been
13829
+ * "filled in" (ie. if they're empty). Another way to think about this
13830
+ * function is that its a check to see if we can score the provided input.
13831
+ */
13832
+ function emptyWidgetsFunctional(widgets,
13833
+ // This is a port of old code, I'm not sure why
13834
+ // we need widgetIds vs the keys of the widgets object
13835
+ widgetIds, userInputMap, locale) {
13836
+ return widgetIds.filter(id => {
13837
+ const widget = widgets[id];
13838
+ if (!widget || widget.static === true) {
13839
+ // Static widgets shouldn't count as empty
13840
+ return false;
13841
+ }
13842
+ const validator = getWidgetValidator(widget.type);
13843
+ const userInput = userInputMap[id];
13844
+ const validationData = widget.options;
13845
+ const score = validator == null ? void 0 : validator(userInput, validationData, locale);
13846
+ if (score) {
13847
+ return scoreIsEmpty(score);
13848
+ }
13849
+ });
13850
+ }
13851
+
13852
+ function validateGroup(userInput, validationData, locale) {
13853
+ const emptyWidgets = emptyWidgetsFunctional(validationData.widgets, Object.keys(validationData.widgets), userInput, locale);
13854
+ if (emptyWidgets.length === 0) {
13855
+ return null;
13856
+ }
13857
+ return {
13858
+ type: "invalid",
13859
+ message: null
13860
+ };
13861
+ }
13862
+
13863
+ function validateLabelImage(userInput) {
13864
+ let numAnswered = 0;
13865
+ for (let i = 0; i < userInput.markers.length; i++) {
13866
+ const userSelection = userInput.markers[i].selected;
13867
+ if (userSelection && userSelection.length > 0) {
13868
+ numAnswered++;
13869
+ }
13870
+ }
13871
+ // We expect all question markers to be answered before grading.
13872
+ if (numAnswered !== userInput.markers.length) {
13873
+ return {
13874
+ type: "invalid",
13875
+ message: null
13876
+ };
13877
+ }
13878
+ return null;
13879
+ }
13880
+
13881
+ function validateMockWidget(userInput) {
13882
+ if (userInput.currentValue == null || userInput.currentValue === "") {
13883
+ return {
13884
+ type: "invalid",
13885
+ message: ""
13886
+ };
13887
+ }
13888
+ return null;
13889
+ }
13890
+
13891
+ function scoreMockWidget(userInput, rubric) {
13892
+ const validationResult = validateMockWidget(userInput);
13893
+ if (validationResult != null) {
13894
+ return validationResult;
13895
+ }
13896
+ return {
13897
+ type: "points",
13898
+ earned: userInput.currentValue === rubric.value ? 1 : 0,
13899
+ total: 1,
13900
+ message: ""
13901
+ };
13902
+ }
13903
+
13904
+ const widgets = {};
13905
+ function registerWidget(type, scorer, validator) {
13906
+ widgets[type] = {
13907
+ scorer,
13908
+ validator
13909
+ };
13910
+ }
13911
+ const getWidgetValidator = name => {
13912
+ var _widgets$name$validat, _widgets$name;
13913
+ return (_widgets$name$validat = (_widgets$name = widgets[name]) == null ? void 0 : _widgets$name.validator) != null ? _widgets$name$validat : null;
13914
+ };
13915
+ const getWidgetScorer = name => {
13916
+ var _widgets$name$scorer, _widgets$name2;
13917
+ return (_widgets$name$scorer = (_widgets$name2 = widgets[name]) == null ? void 0 : _widgets$name2.scorer) != null ? _widgets$name$scorer : null;
13918
+ };
13919
+ registerWidget("categorizer", scoreCategorizer, validateCategorizer);
13920
+ registerWidget("cs-program", scoreCSProgram);
13921
+ registerWidget("dropdown", scoreDropdown, validateDropdown);
13922
+ registerWidget("expression", scoreExpression, validateExpression);
13923
+ registerWidget("grapher", scoreGrapher);
13924
+ registerWidget("group", scoreGroup, validateGroup);
13925
+ registerWidget("iframe", scoreIframe);
13926
+ registerWidget("input-number", scoreInputNumber);
13927
+ registerWidget("interactive-graph", scoreInteractiveGraph);
13928
+ registerWidget("label-image", scoreLabelImage, validateLabelImage);
13929
+ registerWidget("matcher", scoreMatcher);
13930
+ registerWidget("matrix", scoreMatrix, validateMatrix);
13931
+ registerWidget("mock-widget", scoreMockWidget, scoreMockWidget);
13932
+ registerWidget("number-line", scoreNumberLine, validateNumberLine);
13933
+ registerWidget("numeric-input", scoreNumericInput);
13934
+ registerWidget("orderer", scoreOrderer, validateOrderer);
13935
+ registerWidget("plotter", scorePlotter, validatePlotter);
13936
+ registerWidget("radio", scoreRadio, validateRadio);
13937
+ registerWidget("sorter", scoreSorter, validateSorter);
13938
+ registerWidget("table", scoreTable, validateTable);
13939
+ registerWidget("deprecated-standin", () => scoreNoop(1));
13940
+ registerWidget("measurer", () => scoreNoop(1));
13941
+ registerWidget("definition", scoreNoop);
13942
+ registerWidget("explanation", scoreNoop);
13943
+ registerWidget("image", scoreNoop);
13944
+ registerWidget("interaction", scoreNoop);
13945
+ registerWidget("molecule", scoreNoop);
13946
+ registerWidget("passage", scoreNoop);
13947
+ registerWidget("passage-ref", scoreNoop);
13948
+ registerWidget("passage-ref-target", scoreNoop);
13949
+ registerWidget("video", scoreNoop);
13950
+
13951
+ const noScore = {
13952
+ type: "points",
13953
+ earned: 0,
13954
+ total: 0,
13955
+ message: null
13956
+ };
13957
+
13958
+ /**
13959
+ * If a widget says that it is empty once it is graded.
13960
+ * Trying to encapsulate references to the score format.
13961
+ */
13962
+ function scoreIsEmpty(score) {
13963
+ // HACK(benkomalo): ugh. this isn't great; the Perseus score objects
13964
+ // overload the type "invalid" for what should probably be three
13965
+ // distinct cases:
13966
+ // - truly empty or not fully filled out
13967
+ // - invalid or malformed inputs
13968
+ // - "almost correct" like inputs where the widget wants to give
13969
+ // feedback (e.g. a fraction needs to be reduced, or `pi` should
13970
+ // be used instead of 3.14)
13971
+ //
13972
+ // Unfortunately the coercion happens all over the place, as these
13973
+ // Perseus style score objects are created *everywhere* (basically
13974
+ // in every widget), so it's hard to change now. We assume that
13975
+ // anything with a "message" is not truly empty, and one of the
13976
+ // latter two cases for now.
13977
+ return score.type === "invalid" && (!score.message || score.message.length === 0);
13978
+ }
13979
+
13980
+ /**
13981
+ * Combine two score objects.
13982
+ *
13983
+ * Given two score objects for two different widgets, combine them so that
13984
+ * if one is wrong, the total score is wrong, etc.
13985
+ */
13986
+ function combineScores(scoreA, scoreB) {
13987
+ let message;
13988
+ if (scoreA.type === "points" && scoreB.type === "points") {
13989
+ if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
13990
+ // TODO(alpert): Figure out how to combine messages usefully
13991
+ message = null;
13992
+ } else {
13993
+ message = scoreA.message || scoreB.message;
13994
+ }
13995
+ return {
13996
+ type: "points",
13997
+ earned: scoreA.earned + scoreB.earned,
13998
+ total: scoreA.total + scoreB.total,
13999
+ message: message
14000
+ };
14001
+ }
14002
+ if (scoreA.type === "points" && scoreB.type === "invalid") {
14003
+ return scoreB;
14004
+ }
14005
+ if (scoreA.type === "invalid" && scoreB.type === "points") {
14006
+ return scoreA;
14007
+ }
14008
+ if (scoreA.type === "invalid" && scoreB.type === "invalid") {
14009
+ if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
14010
+ // TODO(alpert): Figure out how to combine messages usefully
14011
+ message = null;
14012
+ } else {
14013
+ message = scoreA.message || scoreB.message;
14014
+ }
14015
+ return {
14016
+ type: "invalid",
14017
+ message: message
14018
+ };
14019
+ }
14020
+
14021
+ /**
14022
+ * The above checks cover all combinations of score type, so if we get here
14023
+ * then something is amiss with our inputs.
14024
+ */
14025
+ throw new PerseusError("PerseusScore with unknown type encountered", Errors.InvalidInput, {
14026
+ metadata: {
14027
+ scoreA: JSON.stringify(scoreA),
14028
+ scoreB: JSON.stringify(scoreB)
14029
+ }
14030
+ });
14031
+ }
14032
+ function flattenScores(widgetScoreMap) {
14033
+ return Object.values(widgetScoreMap).reduce(combineScores, noScore);
14034
+ }
14035
+
14036
+ // once scorePerseusItem is the only one calling scoreWidgetsFunctional
14037
+ function scorePerseusItem(perseusRenderData, userInputMap, locale) {
14038
+ // There seems to be a chance that PerseusRenderer.widgets might include
14039
+ // widget data for widgets that are not in PerseusRenderer.content,
14040
+ // so this checks that the widgets are being used before scoring them
14041
+ const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
14042
+ const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, locale);
14043
+ return flattenScores(scores);
14044
+ }
14045
+
14046
+ // TODO: combine scorePerseusItem with scoreWidgetsFunctional
14047
+ function scoreWidgetsFunctional(widgets,
14048
+ // This is a port of old code, I'm not sure why
14049
+ // we need widgetIds vs the keys of the widgets object
14050
+ widgetIds, userInputMap, locale) {
14051
+ const upgradedWidgets = getUpgradedWidgetOptions(widgets);
14052
+ const gradedWidgetIds = widgetIds.filter(id => {
14053
+ const props = upgradedWidgets[id];
14054
+ const widgetIsGraded = (props == null ? void 0 : props.graded) == null || props.graded;
14055
+ const widgetIsStatic = !!(props != null && props.static);
14056
+ // Ungraded widgets or widgets set to static shouldn't be graded.
14057
+ return widgetIsGraded && !widgetIsStatic;
14058
+ });
14059
+ const widgetScores = {};
14060
+ gradedWidgetIds.forEach(id => {
14061
+ var _validator;
14062
+ const widget = upgradedWidgets[id];
14063
+ if (!widget) {
14064
+ return;
14065
+ }
14066
+ const userInput = userInputMap[id];
14067
+ const validator = getWidgetValidator(widget.type);
14068
+ const scorer = getWidgetScorer(widget.type);
14069
+
14070
+ // We do validation (empty checks) first and then scoring. If
14071
+ // validation fails, it's result is itself a PerseusScore.
14072
+ const score = (_validator = validator == null ? void 0 : validator(userInput, widget.options, locale)) != null ? _validator : scorer == null ? void 0 : scorer(userInput, widget.options, locale);
14073
+ if (score != null) {
14074
+ widgetScores[id] = score;
14075
+ }
14076
+ });
14077
+ return widgetScores;
14078
+ }
14079
+
14080
+ 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 };
13849
14081
  //# sourceMappingURL=index.js.map