@khanacademy/perseus-score 1.1.0 → 2.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 (32) hide show
  1. package/dist/es/index.js +424 -196
  2. package/dist/es/index.js.map +1 -1
  3. package/dist/index.d.ts +14 -0
  4. package/dist/index.js +437 -194
  5. package/dist/index.js.map +1 -1
  6. package/dist/score.d.ts +21 -0
  7. package/dist/util/score-noop.d.ts +11 -0
  8. package/dist/util/test-helpers.d.ts +14 -0
  9. package/dist/validate.d.ts +7 -0
  10. package/dist/validation.types.d.ts +136 -32
  11. package/dist/validation.typetest.d.ts +1 -0
  12. package/dist/widgets/categorizer/score-categorizer.d.ts +2 -2
  13. package/dist/widgets/cs-program/score-cs-program.d.ts +1 -1
  14. package/dist/widgets/expression/score-expression.d.ts +1 -1
  15. package/dist/widgets/grapher/score-grapher.d.ts +1 -1
  16. package/dist/widgets/group/score-group.d.ts +3 -0
  17. package/dist/widgets/group/validate-group.d.ts +3 -0
  18. package/dist/widgets/interactive-graph/score-interactive-graph.d.ts +1 -1
  19. package/dist/widgets/label-image/score-label-image.d.ts +2 -3
  20. package/dist/widgets/label-image/validate-label-image.d.ts +3 -0
  21. package/dist/widgets/matcher/score-matcher.d.ts +1 -1
  22. package/dist/widgets/matrix/score-matrix.d.ts +1 -1
  23. package/dist/widgets/matrix/validate-matrix.d.ts +2 -2
  24. package/dist/widgets/mock-widget/mock-widget-validation.types.d.ts +6 -0
  25. package/dist/widgets/mock-widget/score-mock-widget.d.ts +4 -0
  26. package/dist/widgets/mock-widget/validate-mock-widget.d.ts +4 -0
  27. package/dist/widgets/number-line/score-number-line.d.ts +2 -2
  28. package/dist/widgets/plotter/score-plotter.d.ts +2 -2
  29. package/dist/widgets/sorter/score-sorter.d.ts +1 -1
  30. package/dist/widgets/table/score-table.d.ts +1 -1
  31. package/dist/widgets/widget-registry.d.ts +4 -0
  32. package/package.json +4 -4
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,43 +12560,23 @@ function validateCategorizer(userInput, validationData) {
12545
12560
  return null;
12546
12561
  }
12547
12562
 
12548
- function scoreCategorizer(userInput, scoringData) {
12549
- const validationError = validateCategorizer(userInput, scoringData);
12550
- if (validationError) {
12551
- return validationError;
12552
- }
12553
- let allCorrect = true;
12554
- scoringData.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
- // TODO: merge this with scoreIframe, it's the same code
12568
- function scoreCSProgram(state) {
12563
+ function scoreCSProgram(userInput) {
12569
12564
  // The CS program can tell us whether it's correct or incorrect,
12570
12565
  // and pass an optional message
12571
- if (state.status === "correct") {
12566
+ if (userInput.status === "correct") {
12572
12567
  return {
12573
12568
  type: "points",
12574
12569
  earned: 1,
12575
12570
  total: 1,
12576
- message: state.message || null
12571
+ message: userInput.message || null
12577
12572
  };
12578
12573
  }
12579
- if (state.status === "incorrect") {
12574
+ if (userInput.status === "incorrect") {
12580
12575
  return {
12581
12576
  type: "points",
12582
12577
  earned: 0,
12583
12578
  total: 1,
12584
- message: state.message || null
12579
+ message: userInput.message || null
12585
12580
  };
12586
12581
  }
12587
12582
  return {
@@ -12590,25 +12585,7 @@ function scoreCSProgram(state) {
12590
12585
  };
12591
12586
  }
12592
12587
 
12593
- /**
12594
- * Checks if the user has selected an item from the dropdown before scoring.
12595
- * This is shown with a userInput value / index other than 0.
12596
- */
12597
- function validateDropdown(userInput) {
12598
- if (userInput.value === 0) {
12599
- return {
12600
- type: "invalid",
12601
- message: null
12602
- };
12603
- }
12604
- return null;
12605
- }
12606
-
12607
12588
  function scoreDropdown(userInput, rubric) {
12608
- const validationError = validateDropdown(userInput);
12609
- if (validationError) {
12610
- return validationError;
12611
- }
12612
12589
  const correct = rubric.choices[userInput.value - 1].correct;
12613
12590
  return {
12614
12591
  type: "points",
@@ -12619,15 +12596,11 @@ function scoreDropdown(userInput, rubric) {
12619
12596
  }
12620
12597
 
12621
12598
  /**
12622
- * Checks user input from the expression widget to see if it is scorable.
12623
- *
12624
- * Note: Most of the expression widget's validation requires the Rubric because
12625
- * of its use of KhanAnswerTypes as a core part of scoring.
12626
- *
12627
- * @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.
12628
12601
  */
12629
- function validateExpression(userInput) {
12630
- if (userInput === "") {
12602
+ function validateDropdown(userInput) {
12603
+ if (userInput.value === 0) {
12631
12604
  return {
12632
12605
  type: "invalid",
12633
12606
  message: null
@@ -12654,13 +12627,7 @@ function validateExpression(userInput) {
12654
12627
  * show the user an error. TODO(joel) - what error?
12655
12628
  * - Otherwise, pass through the resulting points and message.
12656
12629
  */
12657
- function scoreExpression(userInput, rubric,
12658
- // TODO: remove strings as a param for scorers
12659
- strings, locale) {
12660
- const validationError = validateExpression(userInput);
12661
- if (validationError) {
12662
- return validationError;
12663
- }
12630
+ function scoreExpression(userInput, rubric, locale) {
12664
12631
  const options = _.clone(rubric);
12665
12632
  _.extend(options, {
12666
12633
  decimal_separator: getDecimalSeparator(locale)
@@ -12676,7 +12643,11 @@ strings, locale) {
12676
12643
  // in the function variables list for the expression.
12677
12644
  if (!expression.parsed) {
12678
12645
  /* c8 ignore next */
12679
- throw new PerseusError("Unable to parse solution answer for expression", Errors.InvalidInput);
12646
+ throw new PerseusError("Unable to parse solution answer for expression", Errors.InvalidInput, {
12647
+ metadata: {
12648
+ rubric: JSON.stringify(rubric)
12649
+ }
12650
+ });
12680
12651
  }
12681
12652
  return KhanAnswerTypes.expression.createValidatorFunctional(expression.expr, _({}).extend(options, {
12682
12653
  simplify: answer.simplify,
@@ -12771,6 +12742,24 @@ strings, locale) {
12771
12742
  };
12772
12743
  }
12773
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
+
12774
12763
  function getCoefficientsByType(data) {
12775
12764
  if (data.coords == null) {
12776
12765
  return undefined;
@@ -13072,47 +13061,33 @@ function scoreInteractiveGraph(userInput, rubric) {
13072
13061
 
13073
13062
  // Question state for marker as result of user selected answers.
13074
13063
 
13075
- function scoreLabelImageMarker(marker) {
13064
+ function scoreLabelImageMarker(userInput, rubric) {
13076
13065
  const score = {
13077
13066
  hasAnswers: false,
13078
13067
  isCorrect: false
13079
13068
  };
13080
- if (marker.selected && marker.selected.length > 0) {
13069
+ if (userInput && userInput.length > 0) {
13081
13070
  score.hasAnswers = true;
13082
13071
  }
13083
- if (marker.answers.length > 0) {
13084
- if (marker.selected && marker.selected.length === marker.answers.length) {
13072
+ if (rubric.length > 0) {
13073
+ if (userInput && userInput.length === rubric.length) {
13085
13074
  // All correct answers are selected by the user.
13086
- score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
13075
+ score.isCorrect = userInput.every(choice => rubric.includes(choice));
13087
13076
  }
13088
- } else if (!marker.selected || marker.selected.length === 0) {
13077
+ } else if (!userInput || userInput.length === 0) {
13089
13078
  // Correct as no answers should be selected by the user.
13090
13079
  score.isCorrect = true;
13091
13080
  }
13092
13081
  return score;
13093
13082
  }
13094
-
13095
- // TODO(LEMS-2440): May need to pull answers out of PerseusLabelImageWidgetOptions[markers] for the rubric
13096
13083
  function scoreLabelImage(userInput, rubric) {
13097
- let numAnswered = 0;
13098
13084
  let numCorrect = 0;
13099
- for (const marker of userInput.markers) {
13100
- const score = scoreLabelImageMarker(marker);
13101
- if (score.hasAnswers) {
13102
- numAnswered++;
13103
- }
13085
+ for (let i = 0; i < userInput.markers.length; i++) {
13086
+ const score = scoreLabelImageMarker(userInput.markers[i].selected, rubric.markers[i].answers);
13104
13087
  if (score.isCorrect) {
13105
13088
  numCorrect++;
13106
13089
  }
13107
13090
  }
13108
-
13109
- // We expect all question markers to be answered before grading.
13110
- if (numAnswered !== userInput.markers.length) {
13111
- return {
13112
- type: "invalid",
13113
- message: null
13114
- };
13115
- }
13116
13091
  return {
13117
13092
  type: "points",
13118
13093
  // Markers with no expected answers are graded as correct if user
@@ -13123,8 +13098,8 @@ function scoreLabelImage(userInput, rubric) {
13123
13098
  };
13124
13099
  }
13125
13100
 
13126
- function scoreMatcher(state, rubric) {
13127
- const correct = _.isEqual(state.left, rubric.left) && _.isEqual(state.right, rubric.right);
13101
+ function scoreMatcher(userInput, rubric) {
13102
+ const correct = _.isEqual(userInput.left, rubric.left) && _.isEqual(userInput.right, rubric.right);
13128
13103
  return {
13129
13104
  type: "points",
13130
13105
  earned: correct ? 1 : 0,
@@ -13133,35 +13108,7 @@ function scoreMatcher(state, rubric) {
13133
13108
  };
13134
13109
  }
13135
13110
 
13136
- /**
13137
- * Checks user input from the matrix widget to see if it is scorable.
13138
- *
13139
- * Note: The matrix widget cannot do much validation without the Scoring
13140
- * Data because of its use of KhanAnswerTypes as a core part of scoring.
13141
- *
13142
- * @see `scoreMatrix()` for more details.
13143
- */
13144
- function validateMatrix(userInput, validationData) {
13145
- const supplied = userInput.answers;
13146
- const suppliedSize = getMatrixSize(supplied);
13147
- for (let row = 0; row < suppliedSize[0]; row++) {
13148
- for (let col = 0; col < suppliedSize[1]; col++) {
13149
- if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
13150
- return {
13151
- type: "invalid",
13152
- message: ErrorCodes.FILL_ALL_CELLS_ERROR
13153
- };
13154
- }
13155
- }
13156
- }
13157
- return null;
13158
- }
13159
-
13160
13111
  function scoreMatrix(userInput, rubric) {
13161
- const validationResult = validateMatrix(userInput);
13162
- if (validationResult != null) {
13163
- return validationResult;
13164
- }
13165
13112
  const solution = rubric.answers;
13166
13113
  const supplied = userInput.answers;
13167
13114
  const solutionSize = getMatrixSize(solution);
@@ -13206,35 +13153,35 @@ function scoreMatrix(userInput, rubric) {
13206
13153
  }
13207
13154
 
13208
13155
  /**
13209
- * Checks user input is within the allowed range and not the same as the initial
13210
- * state.
13211
- * @param userInput
13212
- * @see 'scoreNumberLine' for the scoring logic.
13156
+ * Checks user input from the matrix widget to see if it is scorable.
13157
+ *
13158
+ * Note: The matrix widget cannot do much validation without the Scoring
13159
+ * Data because of its use of KhanAnswerTypes as a core part of scoring.
13160
+ *
13161
+ * @see `scoreMatrix()` for more details.
13213
13162
  */
13214
- function validateNumberLine(userInput) {
13215
- const divisionRange = userInput.divisionRange;
13216
- const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
13217
-
13218
- // TODO: I don't think isTickCrtl is a thing anymore
13219
- if (userInput.isTickCrtl && outsideAllowedRange) {
13220
- return {
13221
- type: "invalid",
13222
- message: "Number of divisions is outside the allowed range."
13223
- };
13163
+ function validateMatrix(userInput) {
13164
+ const supplied = userInput.answers;
13165
+ const suppliedSize = getMatrixSize(supplied);
13166
+ for (let row = 0; row < suppliedSize[0]; row++) {
13167
+ for (let col = 0; col < suppliedSize[1]; col++) {
13168
+ if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
13169
+ return {
13170
+ type: "invalid",
13171
+ message: ErrorCodes.FILL_ALL_CELLS_ERROR
13172
+ };
13173
+ }
13174
+ }
13224
13175
  }
13225
13176
  return null;
13226
13177
  }
13227
13178
 
13228
- function scoreNumberLine(userInput, scoringData) {
13229
- const validationError = validateNumberLine(userInput);
13230
- if (validationError) {
13231
- return validationError;
13232
- }
13233
- const range = scoringData.range;
13234
- const start = scoringData.initialX != null ? scoringData.initialX : range[0];
13235
- const startRel = scoringData.isInequality ? "ge" : "eq";
13236
- const correctRel = scoringData.correctRel || "eq";
13237
- const correctPos = number.equal(userInput.numLinePosition, scoringData.correctX || 0);
13179
+ function scoreNumberLine(userInput, rubric) {
13180
+ const range = rubric.range;
13181
+ const start = rubric.initialX != null ? rubric.initialX : range[0];
13182
+ const startRel = rubric.isInequality ? "ge" : "eq";
13183
+ const correctRel = rubric.correctRel || "eq";
13184
+ const correctPos = number.equal(userInput.numLinePosition, rubric.correctX || 0);
13238
13185
  if (correctPos && correctRel === userInput.rel) {
13239
13186
  return {
13240
13187
  type: "points",
@@ -13258,6 +13205,26 @@ function scoreNumberLine(userInput, scoringData) {
13258
13205
  };
13259
13206
  }
13260
13207
 
13208
+ /**
13209
+ * Checks user input is within the allowed range and not the same as the initial
13210
+ * state.
13211
+ * @param userInput
13212
+ * @see 'scoreNumberLine' for the scoring logic.
13213
+ */
13214
+ function validateNumberLine(userInput) {
13215
+ const divisionRange = userInput.divisionRange;
13216
+ const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
13217
+
13218
+ // TODO: I don't think isTickCrtl is a thing anymore
13219
+ if (userInput.isTickCrtl && outsideAllowedRange) {
13220
+ return {
13221
+ type: "invalid",
13222
+ message: "Number of divisions is outside the allowed range."
13223
+ };
13224
+ }
13225
+ return null;
13226
+ }
13227
+
13261
13228
  function _extends() {
13262
13229
  return _extends = Object.assign ? Object.assign.bind() : function (n) {
13263
13230
  for (var e = 1; e < arguments.length; e++) {
@@ -13531,6 +13498,16 @@ function scoreNumericInput(userInput, rubric) {
13531
13498
  };
13532
13499
  }
13533
13500
 
13501
+ function scoreOrderer(userInput, rubric) {
13502
+ const correct = _.isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
13503
+ return {
13504
+ type: "points",
13505
+ earned: correct ? 1 : 0,
13506
+ total: 1,
13507
+ message: null
13508
+ };
13509
+ }
13510
+
13534
13511
  /**
13535
13512
  * Checks user input from the orderer widget to see if the user has started
13536
13513
  * ordering the options, making the widget scorable.
@@ -13547,15 +13524,10 @@ function validateOrderer(userInput) {
13547
13524
  return null;
13548
13525
  }
13549
13526
 
13550
- function scoreOrderer(userInput, rubric) {
13551
- const validateError = validateOrderer(userInput);
13552
- if (validateError) {
13553
- return validateError;
13554
- }
13555
- const correct = _.isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
13527
+ function scorePlotter(userInput, rubric) {
13556
13528
  return {
13557
13529
  type: "points",
13558
- earned: correct ? 1 : 0,
13530
+ earned: approximateDeepEqual(userInput, rubric.correct) ? 1 : 0,
13559
13531
  total: 1,
13560
13532
  message: null
13561
13533
  };
@@ -13577,45 +13549,7 @@ function validatePlotter(userInput, validationData) {
13577
13549
  return null;
13578
13550
  }
13579
13551
 
13580
- function scorePlotter(userInput, scoringData) {
13581
- const validationError = validatePlotter(userInput, scoringData);
13582
- if (validationError) {
13583
- return validationError;
13584
- }
13585
- return {
13586
- type: "points",
13587
- earned: approximateDeepEqual(userInput, scoringData.correct) ? 1 : 0,
13588
- total: 1,
13589
- message: null
13590
- };
13591
- }
13592
-
13593
- /**
13594
- * Checks if the user has selected at least one option. Additional validation
13595
- * is done in scoreRadio to check if the number of selected options is correct
13596
- * and if the user has selected both a correct option and the "none of the above"
13597
- * option.
13598
- * @param userInput
13599
- * @see `scoreRadio` for the additional validation logic and the scoring logic.
13600
- */
13601
- function validateRadio(userInput) {
13602
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
13603
- return sum + (selected ? 1 : 0);
13604
- }, 0);
13605
- if (numSelected === 0) {
13606
- return {
13607
- type: "invalid",
13608
- message: null
13609
- };
13610
- }
13611
- return null;
13612
- }
13613
-
13614
13552
  function scoreRadio(userInput, rubric) {
13615
- const validationError = validateRadio(userInput);
13616
- if (validationError) {
13617
- return validationError;
13618
- }
13619
13553
  const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
13620
13554
  return sum + (selected ? 1 : 0);
13621
13555
  }, 0);
@@ -13656,6 +13590,37 @@ function scoreRadio(userInput, rubric) {
13656
13590
  };
13657
13591
  }
13658
13592
 
13593
+ /**
13594
+ * Checks if the user has selected at least one option. Additional validation
13595
+ * is done in scoreRadio to check if the number of selected options is correct
13596
+ * and if the user has selected both a correct option and the "none of the above"
13597
+ * option.
13598
+ * @param userInput
13599
+ * @see `scoreRadio` for the additional validation logic and the scoring logic.
13600
+ */
13601
+ function validateRadio(userInput) {
13602
+ const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
13603
+ return sum + (selected ? 1 : 0);
13604
+ }, 0);
13605
+ if (numSelected === 0) {
13606
+ return {
13607
+ type: "invalid",
13608
+ message: null
13609
+ };
13610
+ }
13611
+ return null;
13612
+ }
13613
+
13614
+ function scoreSorter(userInput, rubric) {
13615
+ const correct = approximateDeepEqual(userInput.options, rubric.correct);
13616
+ return {
13617
+ type: "points",
13618
+ earned: correct ? 1 : 0,
13619
+ total: 1,
13620
+ message: null
13621
+ };
13622
+ }
13623
+
13659
13624
  /**
13660
13625
  * Checks user input for the sorter widget to ensure that the user has made
13661
13626
  * changes before attempting to score the widget.
@@ -13679,20 +13644,6 @@ function validateSorter(userInput) {
13679
13644
  return null;
13680
13645
  }
13681
13646
 
13682
- function scoreSorter(userInput, rubric) {
13683
- const validationError = validateSorter(userInput);
13684
- if (validationError) {
13685
- return validationError;
13686
- }
13687
- const correct = approximateDeepEqual(userInput.options, rubric.correct);
13688
- return {
13689
- type: "points",
13690
- earned: correct ? 1 : 0,
13691
- total: 1,
13692
- message: null
13693
- };
13694
- }
13695
-
13696
13647
  /**
13697
13648
  * Filters the given table (modelled as a 2D array) to remove any rows that are
13698
13649
  * completely empty.
@@ -13836,5 +13787,282 @@ function scoreInputNumber(userInput, rubric) {
13836
13787
  };
13837
13788
  }
13838
13789
 
13839
- export { ErrorCodes, KhanAnswerTypes, inputNumberAnswerTypes, scoreCSProgram, scoreCategorizer, scoreDropdown, scoreExpression, scoreGrapher, scoreIframe, scoreInputNumber, scoreInteractiveGraph, scoreLabelImage, scoreLabelImageMarker, scoreMatcher, scoreMatrix, scoreNumberLine, scoreNumericInput, scoreOrderer, scorePlotter, scoreRadio, scoreSorter, scoreTable };
13790
+ /**
13791
+ * Several widgets don't have "right"/"wrong" scoring logic,
13792
+ * so this just says to move on past those widgets
13793
+ *
13794
+ * TODO(LEMS-2543) widgets that use this probably shouldn't have any
13795
+ * scoring logic and the thing scoring an exercise
13796
+ * should just know to skip these
13797
+ */
13798
+ function scoreNoop(points = 0) {
13799
+ return {
13800
+ type: "points",
13801
+ earned: points,
13802
+ total: points,
13803
+ message: null
13804
+ };
13805
+ }
13806
+
13807
+ // The `group` widget is basically a widget hosting a full Perseus system in
13808
+ // it. As such, scoring a group means scoring all widgets it contains.
13809
+ function scoreGroup(userInput, rubric, locale) {
13810
+ const scores = scoreWidgetsFunctional(rubric.widgets, Object.keys(rubric.widgets), userInput, locale);
13811
+ return flattenScores(scores);
13812
+ }
13813
+
13814
+ /**
13815
+ * Checks the given user input to see if any answerable widgets have not been
13816
+ * "filled in" (ie. if they're empty). Another way to think about this
13817
+ * function is that its a check to see if we can score the provided input.
13818
+ */
13819
+ function emptyWidgetsFunctional(widgets,
13820
+ // This is a port of old code, I'm not sure why
13821
+ // we need widgetIds vs the keys of the widgets object
13822
+ widgetIds, userInputMap, locale) {
13823
+ return widgetIds.filter(id => {
13824
+ const widget = widgets[id];
13825
+ if (!widget || widget.static === true) {
13826
+ // Static widgets shouldn't count as empty
13827
+ return false;
13828
+ }
13829
+ const validator = getWidgetValidator(widget.type);
13830
+ const userInput = userInputMap[id];
13831
+ const validationData = widget.options;
13832
+ const score = validator == null ? void 0 : validator(userInput, validationData, locale);
13833
+ if (score) {
13834
+ return scoreIsEmpty(score);
13835
+ }
13836
+ });
13837
+ }
13838
+
13839
+ function validateGroup(userInput, validationData, locale) {
13840
+ const emptyWidgets = emptyWidgetsFunctional(validationData.widgets, Object.keys(validationData.widgets), userInput, locale);
13841
+ if (emptyWidgets.length === 0) {
13842
+ return null;
13843
+ }
13844
+ return {
13845
+ type: "invalid",
13846
+ message: null
13847
+ };
13848
+ }
13849
+
13850
+ function validateLabelImage(userInput) {
13851
+ let numAnswered = 0;
13852
+ for (let i = 0; i < userInput.markers.length; i++) {
13853
+ const userSelection = userInput.markers[i].selected;
13854
+ if (userSelection && userSelection.length > 0) {
13855
+ numAnswered++;
13856
+ }
13857
+ }
13858
+ // We expect all question markers to be answered before grading.
13859
+ if (numAnswered !== userInput.markers.length) {
13860
+ return {
13861
+ type: "invalid",
13862
+ message: null
13863
+ };
13864
+ }
13865
+ return null;
13866
+ }
13867
+
13868
+ function validateMockWidget(userInput) {
13869
+ if (userInput.currentValue == null || userInput.currentValue === "") {
13870
+ return {
13871
+ type: "invalid",
13872
+ message: ""
13873
+ };
13874
+ }
13875
+ return null;
13876
+ }
13877
+
13878
+ function scoreMockWidget(userInput, rubric) {
13879
+ const validationResult = validateMockWidget(userInput);
13880
+ if (validationResult != null) {
13881
+ return validationResult;
13882
+ }
13883
+ return {
13884
+ type: "points",
13885
+ earned: userInput.currentValue === rubric.value ? 1 : 0,
13886
+ total: 1,
13887
+ message: ""
13888
+ };
13889
+ }
13890
+
13891
+ const widgets = {};
13892
+ function registerWidget(type, scorer, validator) {
13893
+ widgets[type] = {
13894
+ scorer,
13895
+ validator
13896
+ };
13897
+ }
13898
+ const getWidgetValidator = name => {
13899
+ var _widgets$name$validat, _widgets$name;
13900
+ return (_widgets$name$validat = (_widgets$name = widgets[name]) == null ? void 0 : _widgets$name.validator) != null ? _widgets$name$validat : null;
13901
+ };
13902
+ const getWidgetScorer = name => {
13903
+ var _widgets$name$scorer, _widgets$name2;
13904
+ return (_widgets$name$scorer = (_widgets$name2 = widgets[name]) == null ? void 0 : _widgets$name2.scorer) != null ? _widgets$name$scorer : null;
13905
+ };
13906
+ registerWidget("categorizer", scoreCategorizer, validateCategorizer);
13907
+ registerWidget("cs-program", scoreCSProgram);
13908
+ registerWidget("dropdown", scoreDropdown, validateDropdown);
13909
+ registerWidget("expression", scoreExpression, validateExpression);
13910
+ registerWidget("grapher", scoreGrapher);
13911
+ registerWidget("group", scoreGroup, validateGroup);
13912
+ registerWidget("iframe", scoreIframe);
13913
+ registerWidget("input-number", scoreInputNumber);
13914
+ registerWidget("interactive-graph", scoreInteractiveGraph);
13915
+ registerWidget("label-image", scoreLabelImage, validateLabelImage);
13916
+ registerWidget("matcher", scoreMatcher);
13917
+ registerWidget("matrix", scoreMatrix, validateMatrix);
13918
+ registerWidget("mock-widget", scoreMockWidget, scoreMockWidget);
13919
+ registerWidget("number-line", scoreNumberLine, validateNumberLine);
13920
+ registerWidget("numeric-input", scoreNumericInput);
13921
+ registerWidget("orderer", scoreOrderer, validateOrderer);
13922
+ registerWidget("plotter", scorePlotter, validatePlotter);
13923
+ registerWidget("radio", scoreRadio, validateRadio);
13924
+ registerWidget("sorter", scoreSorter, validateSorter);
13925
+ registerWidget("table", scoreTable, validateTable);
13926
+ registerWidget("deprecated-standin", () => scoreNoop(1));
13927
+ registerWidget("measurer", () => scoreNoop(1));
13928
+ registerWidget("definition", scoreNoop);
13929
+ registerWidget("explanation", scoreNoop);
13930
+ registerWidget("image", scoreNoop);
13931
+ registerWidget("interaction", scoreNoop);
13932
+ registerWidget("molecule", scoreNoop);
13933
+ registerWidget("passage", scoreNoop);
13934
+ registerWidget("passage-ref", scoreNoop);
13935
+ registerWidget("passage-ref-target", scoreNoop);
13936
+ registerWidget("video", scoreNoop);
13937
+
13938
+ const noScore = {
13939
+ type: "points",
13940
+ earned: 0,
13941
+ total: 0,
13942
+ message: null
13943
+ };
13944
+
13945
+ /**
13946
+ * If a widget says that it is empty once it is graded.
13947
+ * Trying to encapsulate references to the score format.
13948
+ */
13949
+ function scoreIsEmpty(score) {
13950
+ // HACK(benkomalo): ugh. this isn't great; the Perseus score objects
13951
+ // overload the type "invalid" for what should probably be three
13952
+ // distinct cases:
13953
+ // - truly empty or not fully filled out
13954
+ // - invalid or malformed inputs
13955
+ // - "almost correct" like inputs where the widget wants to give
13956
+ // feedback (e.g. a fraction needs to be reduced, or `pi` should
13957
+ // be used instead of 3.14)
13958
+ //
13959
+ // Unfortunately the coercion happens all over the place, as these
13960
+ // Perseus style score objects are created *everywhere* (basically
13961
+ // in every widget), so it's hard to change now. We assume that
13962
+ // anything with a "message" is not truly empty, and one of the
13963
+ // latter two cases for now.
13964
+ return score.type === "invalid" && (!score.message || score.message.length === 0);
13965
+ }
13966
+
13967
+ /**
13968
+ * Combine two score objects.
13969
+ *
13970
+ * Given two score objects for two different widgets, combine them so that
13971
+ * if one is wrong, the total score is wrong, etc.
13972
+ */
13973
+ function combineScores(scoreA, scoreB) {
13974
+ let message;
13975
+ if (scoreA.type === "points" && scoreB.type === "points") {
13976
+ if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
13977
+ // TODO(alpert): Figure out how to combine messages usefully
13978
+ message = null;
13979
+ } else {
13980
+ message = scoreA.message || scoreB.message;
13981
+ }
13982
+ return {
13983
+ type: "points",
13984
+ earned: scoreA.earned + scoreB.earned,
13985
+ total: scoreA.total + scoreB.total,
13986
+ message: message
13987
+ };
13988
+ }
13989
+ if (scoreA.type === "points" && scoreB.type === "invalid") {
13990
+ return scoreB;
13991
+ }
13992
+ if (scoreA.type === "invalid" && scoreB.type === "points") {
13993
+ return scoreA;
13994
+ }
13995
+ if (scoreA.type === "invalid" && scoreB.type === "invalid") {
13996
+ if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
13997
+ // TODO(alpert): Figure out how to combine messages usefully
13998
+ message = null;
13999
+ } else {
14000
+ message = scoreA.message || scoreB.message;
14001
+ }
14002
+ return {
14003
+ type: "invalid",
14004
+ message: message
14005
+ };
14006
+ }
14007
+
14008
+ /**
14009
+ * The above checks cover all combinations of score type, so if we get here
14010
+ * then something is amiss with our inputs.
14011
+ */
14012
+ throw new PerseusError("PerseusScore with unknown type encountered", Errors.InvalidInput, {
14013
+ metadata: {
14014
+ scoreA: JSON.stringify(scoreA),
14015
+ scoreB: JSON.stringify(scoreB)
14016
+ }
14017
+ });
14018
+ }
14019
+ function flattenScores(widgetScoreMap) {
14020
+ return Object.values(widgetScoreMap).reduce(combineScores, noScore);
14021
+ }
14022
+
14023
+ // once scorePerseusItem is the only one calling scoreWidgetsFunctional
14024
+ function scorePerseusItem(perseusRenderData, userInputMap, locale) {
14025
+ // There seems to be a chance that PerseusRenderer.widgets might include
14026
+ // widget data for widgets that are not in PerseusRenderer.content,
14027
+ // so this checks that the widgets are being used before scoring them
14028
+ const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
14029
+ const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, locale);
14030
+ return flattenScores(scores);
14031
+ }
14032
+
14033
+ // TODO: combine scorePerseusItem with scoreWidgetsFunctional
14034
+ function scoreWidgetsFunctional(widgets,
14035
+ // This is a port of old code, I'm not sure why
14036
+ // we need widgetIds vs the keys of the widgets object
14037
+ widgetIds, userInputMap, locale) {
14038
+ const upgradedWidgets = getUpgradedWidgetOptions(widgets);
14039
+ const gradedWidgetIds = widgetIds.filter(id => {
14040
+ const props = upgradedWidgets[id];
14041
+ const widgetIsGraded = (props == null ? void 0 : props.graded) == null || props.graded;
14042
+ const widgetIsStatic = !!(props != null && props.static);
14043
+ // Ungraded widgets or widgets set to static shouldn't be graded.
14044
+ return widgetIsGraded && !widgetIsStatic;
14045
+ });
14046
+ const widgetScores = {};
14047
+ gradedWidgetIds.forEach(id => {
14048
+ var _validator;
14049
+ const widget = upgradedWidgets[id];
14050
+ if (!widget) {
14051
+ return;
14052
+ }
14053
+ const userInput = userInputMap[id];
14054
+ const validator = getWidgetValidator(widget.type);
14055
+ const scorer = getWidgetScorer(widget.type);
14056
+
14057
+ // We do validation (empty checks) first and then scoring. If
14058
+ // validation fails, it's result is itself a PerseusScore.
14059
+ const score = (_validator = validator == null ? void 0 : validator(userInput, widget.options, locale)) != null ? _validator : scorer == null ? void 0 : scorer(userInput, widget.options, locale);
14060
+ if (score != null) {
14061
+ widgetScores[id] = score;
14062
+ }
14063
+ });
14064
+ return widgetScores;
14065
+ }
14066
+
14067
+ 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 };
13840
14068
  //# sourceMappingURL=index.js.map