@khanacademy/perseus 45.1.0 → 46.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -31,6 +31,7 @@ export { default as MatrixWidget } from "./widgets/matrix";
31
31
  export { default as TableWidget } from "./widgets/table";
32
32
  export { default as PlotterWidget } from "./widgets/plotter";
33
33
  export { default as GrapherWidget } from "./widgets/grapher";
34
+ import { keScoreFromPerseusScore } from "./util/scoring";
34
35
  export declare const GrapherUtil: {
35
36
  allTypes: any;
36
37
  DEFAULT_GRAPHER_PROPS: any;
@@ -39,6 +40,9 @@ export declare const GrapherUtil: {
39
40
  getEquationString: (props: any) => string;
40
41
  typeToButton: (type: "linear" | "absolute_value" | "exponential" | "logarithm" | "quadratic" | "sinusoid" | "tangent") => any;
41
42
  };
43
+ export declare const ScoringUtil: {
44
+ keScoreFromPerseusScore: typeof keScoreFromPerseusScore;
45
+ };
42
46
  /**
43
47
  * Misc
44
48
  */
package/dist/index.js CHANGED
@@ -2915,12 +2915,6 @@ const rWidgetRule = /^\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]/;
2915
2915
  const rTypeFromWidgetId = /^([a-z-]+) ([0-9]+)$/;
2916
2916
  const rWidgetParts = new RegExp(rWidgetRule.source + "$");
2917
2917
  const snowman = "\u2603";
2918
- const noScore = {
2919
- type: "points",
2920
- earned: 0,
2921
- total: 0,
2922
- message: null
2923
- };
2924
2918
  const seededRNG$1 = function (seed) {
2925
2919
  let randomSeed = seed;
2926
2920
  return function () {
@@ -2998,97 +2992,6 @@ const split = "x".split(/(.)/g).length ? function (str, r) {
2998
2992
  return output;
2999
2993
  };
3000
2994
 
3001
- /**
3002
- * Combine two score objects.
3003
- *
3004
- * Given two score objects for two different widgets, combine them so that
3005
- * if one is wrong, the total score is wrong, etc.
3006
- */
3007
- function combineScores(scoreA, scoreB) {
3008
- let message;
3009
- if (scoreA.type === "points" && scoreB.type === "points") {
3010
- if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
3011
- // TODO(alpert): Figure out how to combine messages usefully
3012
- message = null;
3013
- } else {
3014
- message = scoreA.message || scoreB.message;
3015
- }
3016
- return {
3017
- type: "points",
3018
- earned: scoreA.earned + scoreB.earned,
3019
- total: scoreA.total + scoreB.total,
3020
- message: message
3021
- };
3022
- }
3023
- if (scoreA.type === "points" && scoreB.type === "invalid") {
3024
- return scoreB;
3025
- }
3026
- if (scoreA.type === "invalid" && scoreB.type === "points") {
3027
- return scoreA;
3028
- }
3029
- if (scoreA.type === "invalid" && scoreB.type === "invalid") {
3030
- if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
3031
- // TODO(alpert): Figure out how to combine messages usefully
3032
- message = null;
3033
- } else {
3034
- message = scoreA.message || scoreB.message;
3035
- }
3036
- return {
3037
- type: "invalid",
3038
- message: message
3039
- };
3040
- }
3041
-
3042
- /**
3043
- * The above checks cover all combinations of score type, so if we get here
3044
- * then something is amiss with our inputs.
3045
- */
3046
- throw new perseusCore.PerseusError("PerseusScore with unknown type encountered", perseusCore.Errors.InvalidInput, {
3047
- metadata: {
3048
- scoreA: JSON.stringify(scoreA),
3049
- scoreB: JSON.stringify(scoreB)
3050
- }
3051
- });
3052
- }
3053
- function flattenScores(widgetScoreMap) {
3054
- return Object.values(widgetScoreMap).reduce(combineScores, noScore);
3055
- }
3056
- function isCorrect(score) {
3057
- return score.type === "points" && score.earned >= score.total;
3058
- }
3059
- function keScoreFromPerseusScore(score,
3060
- // It's weird, but this is what we're passing it
3061
- guess, state) {
3062
- if (score.type === "points") {
3063
- return {
3064
- empty: false,
3065
- correct: isCorrect(score),
3066
- message: score.message,
3067
- guess: guess,
3068
- state: state
3069
- };
3070
- }
3071
- if (score.type === "invalid") {
3072
- return {
3073
- empty: true,
3074
- correct: false,
3075
- message: score.message,
3076
- suppressAlmostThere: score.suppressAlmostThere,
3077
- guess: guess,
3078
- state: state
3079
- };
3080
- }
3081
- throw new perseusCore.PerseusError(
3082
- // @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
3083
- "Invalid score type: " + score.type, perseusCore.Errors.InvalidInput, {
3084
- metadata: {
3085
- score: JSON.stringify(score),
3086
- guess: JSON.stringify(guess),
3087
- state: JSON.stringify(state)
3088
- }
3089
- });
3090
- }
3091
-
3092
2995
  /**
3093
2996
  * Return the first valid interpretation of 'text' as a number, in the form
3094
2997
  * {value: 2.3, exact: true}.
@@ -3386,28 +3289,6 @@ function strongEncodeURIComponent(str) {
3386
3289
  .replace(/\*/g, "%2A");
3387
3290
  }
3388
3291
 
3389
- /**
3390
- * If a widget says that it is empty once it is graded.
3391
- * Trying to encapsulate references to the score format.
3392
- */
3393
- function scoreIsEmpty(score) {
3394
- // HACK(benkomalo): ugh. this isn't great; the Perseus score objects
3395
- // overload the type "invalid" for what should probably be three
3396
- // distinct cases:
3397
- // - truly empty or not fully filled out
3398
- // - invalid or malformed inputs
3399
- // - "almost correct" like inputs where the widget wants to give
3400
- // feedback (e.g. a fraction needs to be reduced, or `pi` should
3401
- // be used instead of 3.14)
3402
- //
3403
- // Unfortunately the coercion happens all over the place, as these
3404
- // Perseus style score objects are created *everywhere* (basically
3405
- // in every widget), so it's hard to change now. We assume that
3406
- // anything with a "message" is not truly empty, and one of the
3407
- // latter two cases for now.
3408
- return score.type === "invalid" && (!score.message || score.message.length === 0);
3409
- }
3410
-
3411
3292
  /*
3412
3293
  * The touchHandlers are used to track the current state of the touch
3413
3294
  * event, such as whether or not the user is currently pressed down (either
@@ -3580,13 +3461,9 @@ const Util = {
3580
3461
  rTypeFromWidgetId,
3581
3462
  rWidgetParts,
3582
3463
  snowman,
3583
- noScore,
3584
3464
  seededRNG: seededRNG$1,
3585
3465
  shuffle: shuffle$3,
3586
3466
  split,
3587
- combineScores,
3588
- flattenScores,
3589
- keScoreFromPerseusScore,
3590
3467
  firstNumericalParse: firstNumericalParse$1,
3591
3468
  stringArrayOfSize: stringArrayOfSize$1,
3592
3469
  gridDimensionConfig,
@@ -3602,7 +3479,6 @@ const Util = {
3602
3479
  parseQueryString,
3603
3480
  updateQueryString: updateQueryString$2,
3604
3481
  strongEncodeURIComponent,
3605
- scoreIsEmpty,
3606
3482
  touchHandlers,
3607
3483
  resetTouchHandlers,
3608
3484
  extractPointerLocation,
@@ -18902,6 +18778,126 @@ class QuestionParagraph extends React__namespace.Component {
18902
18778
  }
18903
18779
  }
18904
18780
 
18781
+ const noScore = {
18782
+ type: "points",
18783
+ earned: 0,
18784
+ total: 0,
18785
+ message: null
18786
+ };
18787
+
18788
+ /**
18789
+ * If a widget says that it is empty once it is graded.
18790
+ * Trying to encapsulate references to the score format.
18791
+ */
18792
+ function scoreIsEmpty(score) {
18793
+ // HACK(benkomalo): ugh. this isn't great; the Perseus score objects
18794
+ // overload the type "invalid" for what should probably be three
18795
+ // distinct cases:
18796
+ // - truly empty or not fully filled out
18797
+ // - invalid or malformed inputs
18798
+ // - "almost correct" like inputs where the widget wants to give
18799
+ // feedback (e.g. a fraction needs to be reduced, or `pi` should
18800
+ // be used instead of 3.14)
18801
+ //
18802
+ // Unfortunately the coercion happens all over the place, as these
18803
+ // Perseus style score objects are created *everywhere* (basically
18804
+ // in every widget), so it's hard to change now. We assume that
18805
+ // anything with a "message" is not truly empty, and one of the
18806
+ // latter two cases for now.
18807
+ return score.type === "invalid" && (!score.message || score.message.length === 0);
18808
+ }
18809
+
18810
+ /**
18811
+ * Combine two score objects.
18812
+ *
18813
+ * Given two score objects for two different widgets, combine them so that
18814
+ * if one is wrong, the total score is wrong, etc.
18815
+ */
18816
+ function combineScores(scoreA, scoreB) {
18817
+ let message;
18818
+ if (scoreA.type === "points" && scoreB.type === "points") {
18819
+ if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
18820
+ // TODO(alpert): Figure out how to combine messages usefully
18821
+ message = null;
18822
+ } else {
18823
+ message = scoreA.message || scoreB.message;
18824
+ }
18825
+ return {
18826
+ type: "points",
18827
+ earned: scoreA.earned + scoreB.earned,
18828
+ total: scoreA.total + scoreB.total,
18829
+ message: message
18830
+ };
18831
+ }
18832
+ if (scoreA.type === "points" && scoreB.type === "invalid") {
18833
+ return scoreB;
18834
+ }
18835
+ if (scoreA.type === "invalid" && scoreB.type === "points") {
18836
+ return scoreA;
18837
+ }
18838
+ if (scoreA.type === "invalid" && scoreB.type === "invalid") {
18839
+ if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
18840
+ // TODO(alpert): Figure out how to combine messages usefully
18841
+ message = null;
18842
+ } else {
18843
+ message = scoreA.message || scoreB.message;
18844
+ }
18845
+ return {
18846
+ type: "invalid",
18847
+ message: message
18848
+ };
18849
+ }
18850
+
18851
+ /**
18852
+ * The above checks cover all combinations of score type, so if we get here
18853
+ * then something is amiss with our inputs.
18854
+ */
18855
+ throw new perseusCore.PerseusError("PerseusScore with unknown type encountered", perseusCore.Errors.InvalidInput, {
18856
+ metadata: {
18857
+ scoreA: JSON.stringify(scoreA),
18858
+ scoreB: JSON.stringify(scoreB)
18859
+ }
18860
+ });
18861
+ }
18862
+ function flattenScores(widgetScoreMap) {
18863
+ return Object.values(widgetScoreMap).reduce(combineScores, noScore);
18864
+ }
18865
+ function isCorrect(score) {
18866
+ return score.type === "points" && score.earned >= score.total;
18867
+ }
18868
+ function keScoreFromPerseusScore(score,
18869
+ // It's weird, but this is what we're passing it
18870
+ guess, state) {
18871
+ if (score.type === "points") {
18872
+ return {
18873
+ empty: false,
18874
+ correct: isCorrect(score),
18875
+ message: score.message,
18876
+ guess: guess,
18877
+ state: state
18878
+ };
18879
+ }
18880
+ if (score.type === "invalid") {
18881
+ return {
18882
+ empty: true,
18883
+ correct: false,
18884
+ message: score.message,
18885
+ suppressAlmostThere: score.suppressAlmostThere,
18886
+ guess: guess,
18887
+ state: state
18888
+ };
18889
+ }
18890
+ throw new perseusCore.PerseusError(
18891
+ // @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
18892
+ "Invalid score type: " + score.type, perseusCore.Errors.InvalidInput, {
18893
+ metadata: {
18894
+ score: JSON.stringify(score),
18895
+ guess: JSON.stringify(guess),
18896
+ state: JSON.stringify(state)
18897
+ }
18898
+ });
18899
+ }
18900
+
18905
18901
  // These are all the current widgets that require user-input and are supported for automatic scoring
18906
18902
  const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
18907
18903
 
@@ -19500,7 +19496,7 @@ widgetIds, userInputMap, strings, locale) {
19500
19496
  const scorer = getWidgetScorer(widget.type);
19501
19497
  const score = scorer?.(userInputMap[id], widget.options, strings, locale);
19502
19498
  if (score) {
19503
- return Util.scoreIsEmpty(score);
19499
+ return scoreIsEmpty(score);
19504
19500
  }
19505
19501
  });
19506
19502
  }
@@ -19516,7 +19512,7 @@ strings, locale) {
19516
19512
  // so this checks that the widgets are being used before scoring them
19517
19513
  const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
19518
19514
  const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
19519
- return Util.flattenScores(scores);
19515
+ return flattenScores(scores);
19520
19516
  }
19521
19517
  function scoreWidgetsFunctional(widgets,
19522
19518
  // This is a port of old code, I'm not sure why
@@ -21102,7 +21098,7 @@ class Renderer extends React__namespace.Component {
21102
21098
  */
21103
21099
  score() {
21104
21100
  const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
21105
- const combinedScore = Util.flattenScores(scores);
21101
+ const combinedScore = flattenScores(scores);
21106
21102
  return combinedScore;
21107
21103
  }
21108
21104
 
@@ -21114,28 +21110,6 @@ class Renderer extends React__namespace.Component {
21114
21110
  const totalScore = this.score();
21115
21111
  return [totalGuess, totalScore];
21116
21112
  };
21117
- examples = () => {
21118
- const widgetIds = this.widgetIds;
21119
- const examples = widgetIds.map(widgetId => {
21120
- const widget = this.getWidgetInstance(widgetId);
21121
- return widget != null && widget.examples ? widget.examples() : null;
21122
- }).filter(Boolean);
21123
-
21124
- // no widgets with examples
21125
- if (!examples.length) {
21126
- return null;
21127
- }
21128
- const allEqual = ___default["default"].all(examples, function (example) {
21129
- return ___default["default"].isEqual(examples[0], example);
21130
- });
21131
-
21132
- // some widgets have different examples
21133
- // TODO(alex): handle this better
21134
- if (!allEqual) {
21135
- return null;
21136
- }
21137
- return examples[0];
21138
- };
21139
21113
 
21140
21114
  // TranslationLinter callback
21141
21115
  handletranslationLintErrors = lintErrors => {
@@ -22180,7 +22154,7 @@ class InputNumber extends React__namespace.Component {
22180
22154
  getPromptJSON() {
22181
22155
  return getPromptJSON$s(this.props, this.getUserInput());
22182
22156
  }
22183
- examples = () => {
22157
+ examples() {
22184
22158
  const {
22185
22159
  strings
22186
22160
  } = this.context;
@@ -22188,7 +22162,7 @@ class InputNumber extends React__namespace.Component {
22188
22162
  const forms = answerTypes[type].forms.split(/\s*,\s*/);
22189
22163
  const examples = ___default["default"].map(forms, form => formExamples$1[form](this.props, strings));
22190
22164
  return [strings.yourAnswer].concat(examples);
22191
- };
22165
+ }
22192
22166
  render() {
22193
22167
  if (this.props.apiOptions.customKeypad) {
22194
22168
  // TODO(charlie): Support "Review Mode".
@@ -22474,8 +22448,11 @@ class NumericInput extends React__namespace.Component {
22474
22448
  isFocused: false
22475
22449
  };
22476
22450
 
22477
- // TODO(Nicole, Jeremy): This is maybe never used and should be removed
22478
- examples = () => {
22451
+ /**
22452
+ * Generates a string that demonstrates how to input the various supported
22453
+ * answer forms.
22454
+ */
22455
+ examples() {
22479
22456
  // if the set of specified forms are empty, allow all forms
22480
22457
  const forms = this.props.answerForms?.length !== 0 ? this.props.answerForms : Object.keys(formExamples).map(name => {
22481
22458
  return {
@@ -22490,7 +22467,7 @@ class NumericInput extends React__namespace.Component {
22490
22467
  // versions of the same format
22491
22468
  examples = ___default["default"].uniq(examples);
22492
22469
  return [this.context.strings.yourAnswer].concat(examples);
22493
- };
22470
+ }
22494
22471
  shouldShowExamples = () => {
22495
22472
  const noFormsAccepted = this.props.answerForms?.length === 0;
22496
22473
  // To check if all answer forms are accepted, we must first
@@ -29662,7 +29639,7 @@ const getPromptJSON$f = rendererJSON => {
29662
29639
  // it. As such, scoring a group means scoring all widgets it contains.
29663
29640
  function scoreGroup(userInput, options, strings, locale) {
29664
29641
  const scores = scoreWidgetsFunctional(options.widgets, Object.keys(options.widgets), userInput, strings, locale);
29665
- return Util.flattenScores(scores);
29642
+ return flattenScores(scores);
29666
29643
  }
29667
29644
 
29668
29645
  // exports has no 'transform'
@@ -45479,7 +45456,7 @@ const init = function () {
45479
45456
 
45480
45457
  // This file is processed by a Rollup plugin (replace) to inject the production
45481
45458
  const libName = "@khanacademy/perseus";
45482
- const libVersion = "45.1.0";
45459
+ const libVersion = "46.0.0";
45483
45460
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45484
45461
 
45485
45462
  const apiVersion = {
@@ -46252,7 +46229,7 @@ class ServerItemRenderer extends React__namespace.Component {
46252
46229
  // TODO(alex): Check whether we rely on the format here for
46253
46230
  // analyzing ProblemLogs. If not, remove this layer.
46254
46231
  const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
46255
- const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
46232
+ const keScore = keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
46256
46233
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
46257
46234
  this.setState({
46258
46235
  questionCompleted: keScore.correct,
@@ -47348,7 +47325,7 @@ class MultiRenderer extends React__namespace.Component {
47348
47325
  if (ref.getSerializedState) {
47349
47326
  state = ref.getSerializedState();
47350
47327
  }
47351
- return Util.keScoreFromPerseusScore(score, guess, state);
47328
+ return keScoreFromPerseusScore(score, guess, state);
47352
47329
  }
47353
47330
 
47354
47331
  /**
@@ -47377,8 +47354,8 @@ class MultiRenderer extends React__namespace.Component {
47377
47354
  scores.push(data.ref.score());
47378
47355
  return data.ref?.getUserInput();
47379
47356
  });
47380
- const combinedScore = scores.reduce(Util.combineScores);
47381
- return Util.keScoreFromPerseusScore(combinedScore, guess, state);
47357
+ const combinedScore = scores.reduce(combineScores);
47358
+ return keScoreFromPerseusScore(combinedScore, guess, state);
47382
47359
  }
47383
47360
 
47384
47361
  /**
@@ -50235,6 +50212,9 @@ const GrapherUtil = {
50235
50212
  getEquationString,
50236
50213
  typeToButton
50237
50214
  };
50215
+ const ScoringUtil = {
50216
+ keScoreFromPerseusScore
50217
+ };
50238
50218
 
50239
50219
  exports.ApiOptions = ApiOptions;
50240
50220
  exports.ArticleRenderer = ArticleRenderer;
@@ -50268,6 +50248,7 @@ exports.PlotterWidget = Plotter$1;
50268
50248
  exports.QUESTION_WIDGETS = QUESTION_WIDGETS;
50269
50249
  exports.Radio = Radio;
50270
50250
  exports.Renderer = Renderer;
50251
+ exports.ScoringUtil = ScoringUtil;
50271
50252
  exports.ServerItemRenderer = ref;
50272
50253
  exports.TableWidget = Table$1;
50273
50254
  exports.Util = Util;