@khanacademy/perseus-score 2.0.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.
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;
@@ -13071,24 +13059,6 @@ function scoreInteractiveGraph(userInput, rubric) {
13071
13059
  };
13072
13060
  }
13073
13061
 
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
13062
  // Question state for marker as result of user selected answers.
13093
13063
 
13094
13064
  function scoreLabelImageMarker(userInput, rubric) {
@@ -13111,10 +13081,6 @@ function scoreLabelImageMarker(userInput, rubric) {
13111
13081
  return score;
13112
13082
  }
13113
13083
  function scoreLabelImage(userInput, rubric) {
13114
- const validationError = validateLabelImage(userInput);
13115
- if (validationError) {
13116
- return validationError;
13117
- }
13118
13084
  let numCorrect = 0;
13119
13085
  for (let i = 0; i < userInput.markers.length; i++) {
13120
13086
  const score = scoreLabelImageMarker(userInput.markers[i].selected, rubric.markers[i].answers);
@@ -13142,35 +13108,7 @@ function scoreMatcher(userInput, rubric) {
13142
13108
  };
13143
13109
  }
13144
13110
 
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
13111
  function scoreMatrix(userInput, rubric) {
13170
- const validationError = validateMatrix(userInput);
13171
- if (validationError != null) {
13172
- return validationError;
13173
- }
13174
13112
  const solution = rubric.answers;
13175
13113
  const supplied = userInput.answers;
13176
13114
  const solutionSize = getMatrixSize(solution);
@@ -13215,30 +13153,30 @@ function scoreMatrix(userInput, rubric) {
13215
13153
  }
13216
13154
 
13217
13155
  /**
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.
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.
13222
13162
  */
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
- };
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
+ }
13233
13175
  }
13234
13176
  return null;
13235
13177
  }
13236
13178
 
13237
13179
  function scoreNumberLine(userInput, rubric) {
13238
- const validationError = validateNumberLine(userInput);
13239
- if (validationError) {
13240
- return validationError;
13241
- }
13242
13180
  const range = rubric.range;
13243
13181
  const start = rubric.initialX != null ? rubric.initialX : range[0];
13244
13182
  const startRel = rubric.isInequality ? "ge" : "eq";
@@ -13267,6 +13205,26 @@ function scoreNumberLine(userInput, rubric) {
13267
13205
  };
13268
13206
  }
13269
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
+
13270
13228
  function _extends() {
13271
13229
  return _extends = Object.assign ? Object.assign.bind() : function (n) {
13272
13230
  for (var e = 1; e < arguments.length; e++) {
@@ -13540,27 +13498,7 @@ function scoreNumericInput(userInput, rubric) {
13540
13498
  };
13541
13499
  }
13542
13500
 
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
13501
  function scoreOrderer(userInput, rubric) {
13560
- const validationError = validateOrderer(userInput);
13561
- if (validationError) {
13562
- return validationError;
13563
- }
13564
13502
  const correct = _.isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
13565
13503
  return {
13566
13504
  type: "points",
@@ -13571,13 +13509,13 @@ function scoreOrderer(userInput, rubric) {
13571
13509
  }
13572
13510
 
13573
13511
  /**
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.
13512
+ * Checks user input from the orderer widget to see if the user has started
13513
+ * ordering the options, making the widget scorable.
13514
+ * @param userInput
13515
+ * @see `scoreOrderer` for more details.
13578
13516
  */
13579
- function validatePlotter(userInput, validationData) {
13580
- if (approximateDeepEqual(userInput, validationData.starting)) {
13517
+ function validateOrderer(userInput) {
13518
+ if (userInput.current.length === 0) {
13581
13519
  return {
13582
13520
  type: "invalid",
13583
13521
  message: null
@@ -13587,10 +13525,6 @@ function validatePlotter(userInput, validationData) {
13587
13525
  }
13588
13526
 
13589
13527
  function scorePlotter(userInput, rubric) {
13590
- const validationError = validatePlotter(userInput, rubric);
13591
- if (validationError) {
13592
- return validationError;
13593
- }
13594
13528
  return {
13595
13529
  type: "points",
13596
13530
  earned: approximateDeepEqual(userInput, rubric.correct) ? 1 : 0,
@@ -13600,18 +13534,13 @@ function scorePlotter(userInput, rubric) {
13600
13534
  }
13601
13535
 
13602
13536
  /**
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.
13537
+ * Checks user input to confirm it is not the same as the starting values for the graph.
13538
+ * This means the user has modified the graph, and the question can be scored.
13539
+ *
13540
+ * @see 'scorePlotter' for more details on scoring.
13609
13541
  */
13610
- function validateRadio(userInput) {
13611
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
13612
- return sum + (selected ? 1 : 0);
13613
- }, 0);
13614
- if (numSelected === 0) {
13542
+ function validatePlotter(userInput, validationData) {
13543
+ if (approximateDeepEqual(userInput, validationData.starting)) {
13615
13544
  return {
13616
13545
  type: "invalid",
13617
13546
  message: null
@@ -13621,10 +13550,6 @@ function validateRadio(userInput) {
13621
13550
  }
13622
13551
 
13623
13552
  function scoreRadio(userInput, rubric) {
13624
- const validationError = validateRadio(userInput);
13625
- if (validationError) {
13626
- return validationError;
13627
- }
13628
13553
  const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
13629
13554
  return sum + (selected ? 1 : 0);
13630
13555
  }, 0);
@@ -13665,6 +13590,37 @@ function scoreRadio(userInput, rubric) {
13665
13590
  };
13666
13591
  }
13667
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
+
13668
13624
  /**
13669
13625
  * Checks user input for the sorter widget to ensure that the user has made
13670
13626
  * changes before attempting to score the widget.
@@ -13688,20 +13644,6 @@ function validateSorter(userInput) {
13688
13644
  return null;
13689
13645
  }
13690
13646
 
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
13647
  /**
13706
13648
  * Filters the given table (modelled as a 2D array) to remove any rows that are
13707
13649
  * completely empty.
@@ -13845,5 +13787,282 @@ function scoreInputNumber(userInput, rubric) {
13845
13787
  };
13846
13788
  }
13847
13789
 
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 };
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 };
13849
14068
  //# sourceMappingURL=index.js.map