@khanacademy/perseus 45.0.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.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,
@@ -3918,6 +3794,7 @@ Zoom.prototype.zoomImage = function () {
3918
3794
  this._zoomOriginal();
3919
3795
  }.bind(this);
3920
3796
  img.src = this._targetImage.src;
3797
+ img.alt = this._targetImage.alt;
3921
3798
  this.$zoomedImage = $zoomedImage;
3922
3799
  };
3923
3800
  Zoom.prototype._zoomOriginal = function () {
@@ -8993,7 +8870,10 @@ var Tex$1 = {
8993
8870
  // We use createElement instead of JSX here because we can't name this file tex.tsx;
8994
8871
  // that name is already taken.
8995
8872
  // eslint-disable-next-line import/no-deprecated
8996
- render( /*#__PURE__*/React__namespace.createElement(TeX, {
8873
+ render(
8874
+ /*#__PURE__*/
8875
+ // eslint-disable-next-line react/no-children-prop
8876
+ React__namespace.createElement(TeX, {
8997
8877
  children: text,
8998
8878
  onRender: callback
8999
8879
  }), $texHolder[0]);
@@ -14757,7 +14637,7 @@ const pluck = function (table, subKey) {
14757
14637
  * });
14758
14638
  * {a: 2, b: 3}
14759
14639
  */
14760
- const mapObject$2 = function (obj, lambda) {
14640
+ const mapObject = function (obj, lambda) {
14761
14641
  const result = {};
14762
14642
  Object.keys(obj).forEach(key => {
14763
14643
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
@@ -14766,27 +14646,6 @@ const mapObject$2 = function (obj, lambda) {
14766
14646
  return result;
14767
14647
  };
14768
14648
 
14769
- /**
14770
- * Maps an array to an object
14771
- *
14772
- * > mapObjectFromArray(['a', 'b'], function(elem) {
14773
- * return elem + elem;
14774
- * });
14775
- * {a: 'aa', b: 'bb'}
14776
- */
14777
- const mapObjectFromArray = function (arr, lambda) {
14778
- const result = {};
14779
- arr.forEach(elem => {
14780
- result[elem] = lambda(elem);
14781
- });
14782
- return result;
14783
- };
14784
- var objective_ = {
14785
- pluck: pluck,
14786
- mapObject: mapObject$2,
14787
- mapObjectFromArray: mapObjectFromArray
14788
- };
14789
-
14790
14649
  /**
14791
14650
  * MovableLine
14792
14651
  */
@@ -14851,7 +14710,7 @@ ___default["default"].extend(MovableLine$4.prototype, {
14851
14710
  // - are objects, not primitives (and need a deeper copy)
14852
14711
  // - they don't need getters created for them
14853
14712
  // TODO(jack): Consider "default" once we es3ify perseus
14854
- objective_.pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
14713
+ pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
14855
14714
  },
14856
14715
  /**
14857
14716
  * Resets the object to its state as if it were constructed with
@@ -15358,7 +15217,7 @@ class MovablePoint$5 {
15358
15217
  // - are objects, not primitives (and need a deeper copy)
15359
15218
  // - they don't need getters created for them
15360
15219
  // TODO(jack): Consider "default" once we es3ify perseus
15361
- objective_.pluck(MovablePointOptions, "standard")
15220
+ pluck(MovablePointOptions, "standard")
15362
15221
  // We only update props here, because we want things on state to
15363
15222
  // be persistent, and updated appropriately in modify()
15364
15223
  ),
@@ -16259,7 +16118,7 @@ ___default["default"].extend(MovablePolygon.prototype, {
16259
16118
  // - are objects, not primitives (and need a deeper copy)
16260
16119
  // - they don't need getters created for them
16261
16120
  // TODO(jack): Consider "default" once we es3ify perseus
16262
- objective_.pluck(MovablePolygonOptions, "standard")
16121
+ pluck(MovablePolygonOptions, "standard")
16263
16122
 
16264
16123
  // We only update props here, because we want things on state to
16265
16124
  // be persistent, and updated appropriately in modify()
@@ -18919,6 +18778,126 @@ class QuestionParagraph extends React__namespace.Component {
18919
18778
  }
18920
18779
  }
18921
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
+
18922
18901
  // These are all the current widgets that require user-input and are supported for automatic scoring
18923
18902
  const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
18924
18903
 
@@ -19481,12 +19460,9 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19481
19460
  isLintable: isLintable
19482
19461
  });
19483
19462
 
19484
- const {
19485
- mapObject: mapObject$1
19486
- } = objective_;
19487
19463
  function getUpgradedWidgetOptions(oldWidgetOptions) {
19488
19464
  // @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
19489
- return mapObject$1(oldWidgetOptions, (widgetInfo, widgetId) => {
19465
+ return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
19490
19466
  if (!widgetInfo.type || !widgetInfo.alignment) {
19491
19467
  const newValues = {};
19492
19468
  if (!widgetInfo.type) {
@@ -19517,17 +19493,10 @@ widgetIds, userInputMap, strings, locale) {
19517
19493
  // Static widgets shouldn't count as empty
19518
19494
  return false;
19519
19495
  }
19520
- let score = null;
19521
- const userInput = userInputMap[id];
19522
19496
  const scorer = getWidgetScorer(widget.type);
19523
- if (widget.type === "group") {
19524
- const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19525
- score = Util.flattenScores(scores);
19526
- } else if (scorer) {
19527
- score = scorer(userInput, widget.options, strings, locale);
19528
- }
19497
+ const score = scorer?.(userInputMap[id], widget.options, strings, locale);
19529
19498
  if (score) {
19530
- return Util.scoreIsEmpty(score);
19499
+ return scoreIsEmpty(score);
19531
19500
  }
19532
19501
  });
19533
19502
  }
@@ -19543,7 +19512,7 @@ strings, locale) {
19543
19512
  // so this checks that the widgets are being used before scoring them
19544
19513
  const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
19545
19514
  const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
19546
- return Util.flattenScores(scores);
19515
+ return flattenScores(scores);
19547
19516
  }
19548
19517
  function scoreWidgetsFunctional(widgets,
19549
19518
  // This is a port of old code, I'm not sure why
@@ -19565,11 +19534,9 @@ widgetIds, userInputMap, strings, locale) {
19565
19534
  }
19566
19535
  const userInput = userInputMap[id];
19567
19536
  const scorer = getWidgetScorer(widget.type);
19568
- if (widget.type === "group") {
19569
- const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
19570
- widgetScores[id] = Util.flattenScores(scores);
19571
- } else if (scorer) {
19572
- widgetScores[id] = scorer(userInput, widget.options, strings, locale);
19537
+ const score = scorer?.(userInput, widget.options, strings, locale);
19538
+ if (score != null) {
19539
+ widgetScores[id] = score;
19573
19540
  }
19574
19541
  });
19575
19542
  return widgetScores;
@@ -19876,9 +19843,6 @@ class WidgetContainer extends React__namespace.Component {
19876
19843
  }
19877
19844
  }
19878
19845
 
19879
- const {
19880
- mapObject
19881
- } = objective_;
19882
19846
  const rContainsNonWhitespace = /\S/;
19883
19847
  const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
19884
19848
  const noopOnRender = () => {};
@@ -20995,6 +20959,13 @@ class Renderer extends React__namespace.Component {
20995
20959
  }, this);
20996
20960
  return state;
20997
20961
  };
20962
+
20963
+ /**
20964
+ * Returns an array of widget ids that are empty (meaning widgets where the
20965
+ * learner has not interacted with the widget yet or has not filled in all
20966
+ * fields). For example, the `interactive-graph` widget is considered
20967
+ * empty if the graph is in the starting state.
20968
+ */
20998
20969
  emptyWidgets() {
20999
20970
  return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
21000
20971
  }
@@ -21048,7 +21019,7 @@ class Renderer extends React__namespace.Component {
21048
21019
  }, 0);
21049
21020
  });
21050
21021
  };
21051
- setInputValue = (path, newValue, focus) => {
21022
+ setInputValue = (path, newValue, cb) => {
21052
21023
  // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
21053
21024
  const widgetId = ___default["default"].first(path);
21054
21025
  // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
@@ -21056,7 +21027,7 @@ class Renderer extends React__namespace.Component {
21056
21027
  const widget = this.getWidgetInstance(widgetId);
21057
21028
 
21058
21029
  // Widget handles parsing of the interWidgetPath.
21059
- widget?.setInputValue?.(interWidgetPath, newValue, focus);
21030
+ widget?.setInputValue?.(interWidgetPath, newValue, cb);
21060
21031
  };
21061
21032
 
21062
21033
  /**
@@ -21127,7 +21098,7 @@ class Renderer extends React__namespace.Component {
21127
21098
  */
21128
21099
  score() {
21129
21100
  const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
21130
- const combinedScore = Util.flattenScores(scores);
21101
+ const combinedScore = flattenScores(scores);
21131
21102
  return combinedScore;
21132
21103
  }
21133
21104
 
@@ -21139,28 +21110,6 @@ class Renderer extends React__namespace.Component {
21139
21110
  const totalScore = this.score();
21140
21111
  return [totalGuess, totalScore];
21141
21112
  };
21142
- examples = () => {
21143
- const widgetIds = this.widgetIds;
21144
- const examples = widgetIds.map(widgetId => {
21145
- const widget = this.getWidgetInstance(widgetId);
21146
- return widget != null && widget.examples ? widget.examples() : null;
21147
- }).filter(Boolean);
21148
-
21149
- // no widgets with examples
21150
- if (!examples.length) {
21151
- return null;
21152
- }
21153
- const allEqual = ___default["default"].all(examples, function (example) {
21154
- return ___default["default"].isEqual(examples[0], example);
21155
- });
21156
-
21157
- // some widgets have different examples
21158
- // TODO(alex): handle this better
21159
- if (!allEqual) {
21160
- return null;
21161
- }
21162
- return examples[0];
21163
- };
21164
21113
 
21165
21114
  // TranslationLinter callback
21166
21115
  handletranslationLintErrors = lintErrors => {
@@ -21354,6 +21303,7 @@ class TextInput extends React__namespace.Component {
21354
21303
  value: formattedValue,
21355
21304
  type: "text",
21356
21305
  "aria-label": labelText,
21306
+ "aria-describedby": this.props["aria-describedby"],
21357
21307
  onChange: value => this.props.onChange(value),
21358
21308
  placeholder: placeholder,
21359
21309
  onFocus: onFocus,
@@ -21612,6 +21562,7 @@ class Tooltip extends React__namespace.Component {
21612
21562
  }, /*#__PURE__*/React__namespace.createElement("div", {
21613
21563
  ref: this.tooltipContainerRef,
21614
21564
  className: "tooltipContainer",
21565
+ role: "tooltip",
21615
21566
  style: {
21616
21567
  position: "absolute",
21617
21568
  // height must start out undefined, not null, so that
@@ -21688,9 +21639,15 @@ class InputWithExamples extends React__namespace.Component {
21688
21639
  };
21689
21640
  _renderInput = () => {
21690
21641
  const id = this._getUniqueId();
21642
+ const ariaId = `aria-for-${id}`;
21643
+ // Generate text from a known set of format options that will read well in a screen reader
21644
+ const examplesAria = this.props.examples.length === 7 ? "" : `${this.props.examples[0]}
21645
+ ${this.props.examples.slice(1).join(", or\n")}`
21646
+ // @ts-expect-error TS2550: Property replaceAll does not exist on type string.
21647
+ .replaceAll("*", "").replaceAll("$", "").replaceAll("\\ \\text{pi}", " pi").replaceAll("\\ ", " and ");
21691
21648
  const inputProps = {
21692
21649
  id: id,
21693
- "aria-describedby": id,
21650
+ "aria-describedby": ariaId,
21694
21651
  ref: "input",
21695
21652
  className: this._getInputClassName(),
21696
21653
  labelText: this.props.labelText,
@@ -21706,7 +21663,12 @@ class InputWithExamples extends React__namespace.Component {
21706
21663
  autoCorrect: "off",
21707
21664
  spellCheck: "false"
21708
21665
  };
21709
- return /*#__PURE__*/React__namespace.createElement(TextInput, inputProps);
21666
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(TextInput, inputProps), /*#__PURE__*/React__namespace.createElement("span", {
21667
+ id: ariaId,
21668
+ style: {
21669
+ display: "none"
21670
+ }
21671
+ }, examplesAria));
21710
21672
  };
21711
21673
  _handleFocus = () => {
21712
21674
  this.props.onFocus();
@@ -21747,8 +21709,12 @@ class InputWithExamples extends React__namespace.Component {
21747
21709
  };
21748
21710
  render() {
21749
21711
  const input = this._renderInput();
21750
- const examplesContent = ___default["default"].map(this.props.examples, example => {
21751
- return "- " + example;
21712
+ const examplesContent = this.props.examples.length <= 2 ? this.props.examples.join(" ") // A single item (with or without leading text) is not a "list"
21713
+ : this.props.examples // 2 or more items should display as a list
21714
+ .map((example, index) => {
21715
+ // If the first example is bold, then it is most likely a heading/leading text.
21716
+ // So, it shouldn't be part of the list.
21717
+ return index === 0 && example.startsWith("**") ? `${example}\n` : `- ${example}`;
21752
21718
  }).join("\n");
21753
21719
  const showExamples = this.props.shouldShowExamples && this.state.showExamples;
21754
21720
  return /*#__PURE__*/React__namespace.createElement(Tooltip, {
@@ -22188,7 +22154,7 @@ class InputNumber extends React__namespace.Component {
22188
22154
  getPromptJSON() {
22189
22155
  return getPromptJSON$s(this.props, this.getUserInput());
22190
22156
  }
22191
- examples = () => {
22157
+ examples() {
22192
22158
  const {
22193
22159
  strings
22194
22160
  } = this.context;
@@ -22196,7 +22162,7 @@ class InputNumber extends React__namespace.Component {
22196
22162
  const forms = answerTypes[type].forms.split(/\s*,\s*/);
22197
22163
  const examples = ___default["default"].map(forms, form => formExamples$1[form](this.props, strings));
22198
22164
  return [strings.yourAnswer].concat(examples);
22199
- };
22165
+ }
22200
22166
  render() {
22201
22167
  if (this.props.apiOptions.customKeypad) {
22202
22168
  // TODO(charlie): Support "Review Mode".
@@ -22402,7 +22368,7 @@ function scoreNumericInput(userInput, rubric, strings) {
22402
22368
  // We may have received TeX; try to parse it before grading.
22403
22369
  // If `currentValue` is not TeX, this should be a no-op.
22404
22370
  const currentValue = ParseTex(userInput.currentValue);
22405
- const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => Math.abs(answer.value) <= 1);
22371
+ const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value !== undefined && Math.abs(answer.value) <= 1);
22406
22372
 
22407
22373
  // The coefficient is an attribute of the widget
22408
22374
  let localValue = currentValue;
@@ -22478,11 +22444,15 @@ class NumericInput extends React__namespace.Component {
22478
22444
  state = {
22479
22445
  // keeps track of the other set of values when switching
22480
22446
  // between 0 and finite solutions
22481
- previousValues: [""]
22447
+ previousValues: [""],
22448
+ isFocused: false
22482
22449
  };
22483
22450
 
22484
- // TODO(Nicole, Jeremy): This is maybe never used and should be removed
22485
- examples = () => {
22451
+ /**
22452
+ * Generates a string that demonstrates how to input the various supported
22453
+ * answer forms.
22454
+ */
22455
+ examples() {
22486
22456
  // if the set of specified forms are empty, allow all forms
22487
22457
  const forms = this.props.answerForms?.length !== 0 ? this.props.answerForms : Object.keys(formExamples).map(name => {
22488
22458
  return {
@@ -22497,7 +22467,7 @@ class NumericInput extends React__namespace.Component {
22497
22467
  // versions of the same format
22498
22468
  examples = ___default["default"].uniq(examples);
22499
22469
  return [this.context.strings.yourAnswer].concat(examples);
22500
- };
22470
+ }
22501
22471
  shouldShowExamples = () => {
22502
22472
  const noFormsAccepted = this.props.answerForms?.length === 0;
22503
22473
  // To check if all answer forms are accepted, we must first
@@ -22543,9 +22513,21 @@ class NumericInput extends React__namespace.Component {
22543
22513
  };
22544
22514
  _handleFocus = () => {
22545
22515
  this.props.onFocus([]);
22516
+ this.setState(currentState => {
22517
+ return {
22518
+ ...currentState,
22519
+ isFocused: true
22520
+ };
22521
+ });
22546
22522
  };
22547
22523
  _handleBlur = () => {
22548
22524
  this.props.onBlur([]);
22525
+ this.setState(currentState => {
22526
+ return {
22527
+ ...currentState,
22528
+ isFocused: false
22529
+ };
22530
+ });
22549
22531
  };
22550
22532
  render() {
22551
22533
  let labelText = this.props.labelText;
@@ -22576,13 +22558,20 @@ class NumericInput extends React__namespace.Component {
22576
22558
  // component.
22577
22559
  const styles = aphrodite.StyleSheet.create({
22578
22560
  input: {
22561
+ borderRadius: "3px",
22562
+ borderWidth: this.state.isFocused ? "2px" : "1px",
22563
+ display: "inline-block",
22564
+ fontFamily: `Symbola, "Times New Roman", serif`,
22565
+ fontSize: "18px",
22566
+ height: "32px",
22567
+ lineHeight: "18px",
22568
+ padding: this.state.isFocused ? "4px" : "4px 5px",
22569
+ // account for added focus border thickness
22579
22570
  textAlign: this.props.rightAlign ? "right" : "left",
22580
- width: this.props.size === "small" ? 40 : 80,
22581
- padding: 0,
22582
- height: "auto"
22571
+ width: this.props.size === "small" ? 40 : 80
22583
22572
  }
22584
22573
  });
22585
- return /*#__PURE__*/React__namespace.createElement("div", null, /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
22574
+ return /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
22586
22575
  ref: ref => this.inputRef = ref,
22587
22576
  value: this.props.currentValue,
22588
22577
  onChange: this.handleChange,
@@ -22594,7 +22583,7 @@ class NumericInput extends React__namespace.Component {
22594
22583
  id: this.props.widgetId,
22595
22584
  disabled: this.props.apiOptions.readOnly,
22596
22585
  style: styles.input
22597
- }));
22586
+ });
22598
22587
  }
22599
22588
  }
22600
22589
 
@@ -27522,8 +27511,8 @@ var Definition$1 = {
27522
27511
  scorer: () => scoreNoop()
27523
27512
  };
27524
27513
 
27525
- // The props are type `any` on purpose so that this can receive props
27526
- // from any deprecated widget
27514
+ // There are no required props, but this component can receive props from any
27515
+ // deprecated widget
27527
27516
  class DeprecatedStandin extends React__default["default"].Component {
27528
27517
  static contextType = PerseusI18nContext;
27529
27518
  // this just helps with TS weak typing when a Widget
@@ -29646,6 +29635,13 @@ const getPromptJSON$f = rendererJSON => {
29646
29635
  };
29647
29636
  };
29648
29637
 
29638
+ // The `group` widget is basically a widget hosting a full Perseus system in
29639
+ // it. As such, scoring a group means scoring all widgets it contains.
29640
+ function scoreGroup(userInput, options, strings, locale) {
29641
+ const scores = scoreWidgetsFunctional(options.widgets, Object.keys(options.widgets), userInput, strings, locale);
29642
+ return flattenScores(scores);
29643
+ }
29644
+
29649
29645
  // exports has no 'transform'
29650
29646
 
29651
29647
  class Group extends React__namespace.Component {
@@ -29797,6 +29793,9 @@ var Group$1 = {
29797
29793
  displayName: "Group (SAT only)",
29798
29794
  widget: Group,
29799
29795
  traverseChildWidgets: traverseChildWidgets,
29796
+ // TODO(LEMS-2656): remove TS suppression
29797
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
29798
+ scorer: scoreGroup,
29800
29799
  hidden: true,
29801
29800
  isLintable: true
29802
29801
  };
@@ -32540,6 +32539,61 @@ const LegacyGrid = _ref => {
32540
32539
 
32541
32540
  // Range is replaced within this file with Interval, but it is used elsewhere
32542
32541
  // and exported from the package, so we need to keep it around.
32542
+ /**
32543
+ * Our core set of Perseus widgets.
32544
+ *
32545
+ * This interface is the basis for "registering" all Perseus widget types.
32546
+ * There should be one key/value pair for each supported widget. If you create
32547
+ * a new widget, an entry should be added to this interface. Note that this
32548
+ * only registers the widget options type, you'll also need to register the
32549
+ * widget so that it's available at runtime (@see
32550
+ * {@link file://./widgets.ts#registerWidget}).
32551
+ *
32552
+ * Importantly, the key should be the name that is used in widget IDs. For most
32553
+ * widgets that is the same as the widget option's `type` field. In cases where
32554
+ * a widget has been deprecated and replaced with the deprecated-standin
32555
+ * widget, it should be the original widget type!
32556
+ *
32557
+ * If you define the widget outside of this package, you can still add the new
32558
+ * widget to this interface by writing the following in that package that
32559
+ * contains the widget. TypeScript will merge that definition of the
32560
+ * `PerseusWidgets` with the one defined below.
32561
+ *
32562
+ * ```typescript
32563
+ * declare module "@khanacademy/perseus" {
32564
+ * interface PerseusWidgetTypes {
32565
+ * // A new widget
32566
+ * "new-awesomeness": MyAwesomeNewWidget;
32567
+ *
32568
+ * // A deprecated widget
32569
+ * "super-old-widget": AutoCorrectWidget;
32570
+ * }
32571
+ * }
32572
+ *
32573
+ * // The new widget's options definition
32574
+ * type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>;
32575
+ *
32576
+ * // The deprecated widget's options definition
32577
+ * type SuperOldWidget = WidgetOptions<'super-old-widget', object>;
32578
+ * ```
32579
+ *
32580
+ * This interface can be extended through the magic of TypeScript "Declaration
32581
+ * merging". Specifically, we augment this module and extend this interface.
32582
+ *
32583
+ * @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
32584
+ */
32585
+ /**
32586
+ * A map of widget IDs to widget options. This is most often used as the type
32587
+ * for a set of widgets defined in a `PerseusItem` but can also be useful to
32588
+ * represent a function parameter where only `widgets` from a `PerseusItem` are
32589
+ * needed. Today Widget IDs are made up of the widget type and an incrementing
32590
+ * integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid
32591
+ * reading/parsing the widget id to derive any information from it, except in
32592
+ * the case of this map.
32593
+ *
32594
+ * @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
32595
+ * by augmenting the PerseusWidgetTypes with new widget types!
32596
+ */
32543
32597
  /**
32544
32598
  * A "PerseusItem" is a classic Perseus item. It is rendered by the
32545
32599
  * `ServerItemRenderer` and the layout is pre-set.
@@ -32555,6 +32609,9 @@ const LegacyGrid = _ref => {
32555
32609
  * A "MultiItem" is an advanced Perseus item. It is rendered by the
32556
32610
  * `MultiRenderer` and you can control the layout of individual parts of the
32557
32611
  * item.
32612
+ *
32613
+ * @deprecated MultiItem support is slated for removal in a future Perseus
32614
+ * release.
32558
32615
  */
32559
32616
  const ItemExtras = [
32560
32617
  // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
@@ -32576,6 +32633,11 @@ const ItemExtras = [
32576
32633
  // The user might benefit from using a statistics Z Table like https://www.ztable.net/
32577
32634
  "zTable"];
32578
32635
 
32636
+ /**
32637
+ * The type representing the common structure of all widget's options. The
32638
+ * `Options` generic type represents the widget-specific option data.
32639
+ */
32640
+
32579
32641
  // prettier-ignore
32580
32642
 
32581
32643
  // prettier-ignore
@@ -32644,7 +32706,9 @@ const ItemExtras = [
32644
32706
 
32645
32707
  //prettier-ignore
32646
32708
 
32647
- // A background image applied to various widgets.
32709
+ /**
32710
+ * A background image applied to various widgets.
32711
+ */
32648
32712
 
32649
32713
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
32650
32714
 
@@ -37288,8 +37352,6 @@ const makeInvalidTypeError = (functionName, graphType) => {
37288
37352
  });
37289
37353
  };
37290
37354
 
37291
- // There's no transform function in exports
37292
-
37293
37355
  // TODO: there's another, very similar getSinusoidCoefficients function
37294
37356
  // they should probably be merged
37295
37357
  function getSinusoidCoefficients(coords) {
@@ -42407,23 +42469,37 @@ const getPromptJSON$6 = (renderProps, userInput) => {
42407
42469
  };
42408
42470
  };
42409
42471
 
42410
- function scoreNumberLine(state, rubric) {
42411
- const range = rubric.range;
42412
- const divisionRange = state.divisionRange;
42413
- const start = rubric.initialX != null ? rubric.initialX : range[0];
42414
- const startRel = rubric.isInequality ? "ge" : "eq";
42415
- const correctRel = rubric.correctRel || "eq";
42416
- const correctPos = kmath.number.equal(state.numLinePosition, rubric.correctX || 0);
42417
- const outsideAllowedRange = state.numDivisions > divisionRange[1] || state.numDivisions < divisionRange[0];
42472
+ /**
42473
+ * Checks user input is within the allowed range and not the same as the initial
42474
+ * state.
42475
+ * @param userInput
42476
+ * @see 'scoreNumberLine' for the scoring logic.
42477
+ */
42478
+ function validateNumberLine(userInput) {
42479
+ const divisionRange = userInput.divisionRange;
42480
+ const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
42418
42481
 
42419
42482
  // TODO: I don't think isTickCrtl is a thing anymore
42420
- if (state.isTickCrtl && outsideAllowedRange) {
42483
+ if (userInput.isTickCrtl && outsideAllowedRange) {
42421
42484
  return {
42422
42485
  type: "invalid",
42423
42486
  message: "Number of divisions is outside the allowed range."
42424
42487
  };
42425
42488
  }
42426
- if (correctPos && correctRel === state.rel) {
42489
+ return null;
42490
+ }
42491
+
42492
+ function scoreNumberLine(userInput, scoringData) {
42493
+ const validationError = validateNumberLine(userInput);
42494
+ if (validationError) {
42495
+ return validationError;
42496
+ }
42497
+ const range = scoringData.range;
42498
+ const start = scoringData.initialX != null ? scoringData.initialX : range[0];
42499
+ const startRel = scoringData.isInequality ? "ge" : "eq";
42500
+ const correctRel = scoringData.correctRel || "eq";
42501
+ const correctPos = kmath.number.equal(userInput.numLinePosition, scoringData.correctX || 0);
42502
+ if (correctPos && correctRel === userInput.rel) {
42427
42503
  return {
42428
42504
  type: "points",
42429
42505
  earned: 1,
@@ -42431,7 +42507,7 @@ function scoreNumberLine(state, rubric) {
42431
42507
  message: null
42432
42508
  };
42433
42509
  }
42434
- if (state.numLinePosition === start && state.rel === startRel) {
42510
+ if (userInput.numLinePosition === start && userInput.rel === startRel) {
42435
42511
  // We're where we started.
42436
42512
  return {
42437
42513
  type: "invalid",
@@ -45380,7 +45456,7 @@ const init = function () {
45380
45456
 
45381
45457
  // This file is processed by a Rollup plugin (replace) to inject the production
45382
45458
  const libName = "@khanacademy/perseus";
45383
- const libVersion = "45.0.0";
45459
+ const libVersion = "46.0.0";
45384
45460
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45385
45461
 
45386
45462
  const apiVersion = {
@@ -45968,14 +46044,10 @@ class ServerItemRenderer extends React__namespace.Component {
45968
46044
  });
45969
46045
  }
45970
46046
  componentDidUpdate(prevProps, prevState) {
45971
- if (this.props.apiOptions.answerableCallback) {
46047
+ const answerableCallback = this.props.apiOptions.answerableCallback;
46048
+ if (answerableCallback != null) {
45972
46049
  const isAnswerable = this.questionRenderer.emptyWidgets().length === 0;
45973
- const {
45974
- answerableCallback
45975
- } = this.props.apiOptions;
45976
- if (answerableCallback) {
45977
- answerableCallback(isAnswerable);
45978
- }
46050
+ answerableCallback(isAnswerable);
45979
46051
  }
45980
46052
  if (!this._fullyRendered) {
45981
46053
  const assetsLoaded = Object.values(this.state.assetStatuses).every(Boolean);
@@ -46157,7 +46229,7 @@ class ServerItemRenderer extends React__namespace.Component {
46157
46229
  // TODO(alex): Check whether we rely on the format here for
46158
46230
  // analyzing ProblemLogs. If not, remove this layer.
46159
46231
  const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
46160
- const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
46232
+ const keScore = keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
46161
46233
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
46162
46234
  this.setState({
46163
46235
  questionCompleted: keScore.correct,
@@ -46346,7 +46418,7 @@ const traverseRenderer$1 = function (rendererOptions, contentCallback, deepWidge
46346
46418
  newContent = modifiedContent;
46347
46419
  }
46348
46420
  }
46349
- const newWidgets = objective_.mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
46421
+ const newWidgets = mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
46350
46422
  // Widgets without info or a type are empty widgets, and
46351
46423
  // should always be renderable. It's also annoying to write
46352
46424
  // checks for this everywhere, so we just filter them out once and
@@ -47253,7 +47325,7 @@ class MultiRenderer extends React__namespace.Component {
47253
47325
  if (ref.getSerializedState) {
47254
47326
  state = ref.getSerializedState();
47255
47327
  }
47256
- return Util.keScoreFromPerseusScore(score, guess, state);
47328
+ return keScoreFromPerseusScore(score, guess, state);
47257
47329
  }
47258
47330
 
47259
47331
  /**
@@ -47282,8 +47354,8 @@ class MultiRenderer extends React__namespace.Component {
47282
47354
  scores.push(data.ref.score());
47283
47355
  return data.ref?.getUserInput();
47284
47356
  });
47285
- const combinedScore = scores.reduce(Util.combineScores);
47286
- return Util.keScoreFromPerseusScore(combinedScore, guess, state);
47357
+ const combinedScore = scores.reduce(combineScores);
47358
+ return keScoreFromPerseusScore(combinedScore, guess, state);
47287
47359
  }
47288
47360
 
47289
47361
  /**
@@ -48956,7 +49028,7 @@ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
48956
49028
  const parseDefinitionWidget = parseWidget(constant("definition"), object({
48957
49029
  togglePrompt: string,
48958
49030
  definition: string,
48959
- static: boolean
49031
+ static: defaulted(boolean, () => false)
48960
49032
  }));
48961
49033
 
48962
49034
  const parseDropdownWidget = parseWidget(constant("dropdown"), object({
@@ -49159,15 +49231,26 @@ const parseIframeWidget = parseWidget(constant("iframe"), object({
49159
49231
  static: defaulted(boolean, () => false)
49160
49232
  }));
49161
49233
 
49234
+ const stringToNumber = (rawValue, ctx) => {
49235
+ if (typeof rawValue === "number") {
49236
+ return ctx.success(rawValue);
49237
+ }
49238
+ const parsedNumber = +rawValue;
49239
+ if (rawValue === "" || isNaN(parsedNumber)) {
49240
+ return ctx.failure("a number or numeric string", rawValue);
49241
+ }
49242
+ return ctx.success(parsedNumber);
49243
+ };
49244
+
49245
+ const numericToNumber = pipeParsers(union(number).or(string).parser).then(stringToNumber).parser;
49162
49246
  const parsePerseusImageBackground = object({
49163
49247
  url: optional(nullable(string)),
49164
- width: optional(number),
49165
- height: optional(number),
49166
- top: optional(number),
49167
- left: optional(number),
49168
- bottom: optional(number),
49169
- // TODO(benchristel): convert scale to a number
49170
- scale: optional(union(number).or(string).parser)
49248
+ width: optional(numericToNumber),
49249
+ height: optional(numericToNumber),
49250
+ top: optional(numericToNumber),
49251
+ left: optional(numericToNumber),
49252
+ bottom: optional(numericToNumber),
49253
+ scale: optional(numericToNumber)
49171
49254
  });
49172
49255
 
49173
49256
  const pairOfNumbers$2 = pair(number, number);
@@ -49186,6 +49269,12 @@ const parseImageWidget = parseWidget(constant("image"), object({
49186
49269
  box: optional(pairOfNumbers$2)
49187
49270
  }));
49188
49271
 
49272
+ const booleanToString = (rawValue, ctx) => {
49273
+ if (typeof rawValue === "boolean") {
49274
+ return ctx.success(String(rawValue));
49275
+ }
49276
+ return ctx.failure("boolean", rawValue);
49277
+ };
49189
49278
  const parseInputNumberWidget = parseWidget(constant("input-number"), object({
49190
49279
  answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
49191
49280
  inexact: optional(boolean),
@@ -49193,7 +49282,11 @@ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
49193
49282
  rightAlign: optional(boolean),
49194
49283
  simplify: enumeration("required", "optional", "enforced"),
49195
49284
  size: enumeration("normal", "small"),
49196
- value: union(number).or(string).parser,
49285
+ // TODO(benchristel): there are some content items where value is a
49286
+ // boolean, even though that makes no sense. We should figure out if
49287
+ // those content items are actually published anywhere, and consider
49288
+ // updating them.
49289
+ value: union(number).or(string).or(booleanToString).parser,
49197
49290
  customKeypad: optional(boolean)
49198
49291
  }));
49199
49292
 
@@ -49491,18 +49584,27 @@ const parseLockedFunctionType = object({
49491
49584
  const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
49492
49585
  const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
49493
49586
  step: pairOfNumbers,
49494
- gridStep: pairOfNumbers,
49495
- snapStep: pairOfNumbers,
49587
+ // TODO(benchristel): rather than making gridStep and snapStep
49588
+ // optional, we should duplicate the defaulting logic from the
49589
+ // InteractiveGraph component. See parse-perseus-json/README.md for
49590
+ // why.
49591
+ gridStep: optional(pairOfNumbers),
49592
+ snapStep: optional(pairOfNumbers),
49496
49593
  backgroundImage: optional(parsePerseusImageBackground),
49497
49594
  markings: enumeration("graph", "grid", "none"),
49498
- labels: array(string),
49595
+ labels: optional(array(string)),
49499
49596
  showProtractor: boolean,
49500
49597
  showRuler: optional(boolean),
49501
49598
  showTooltips: optional(boolean),
49502
49599
  rulerLabel: optional(string),
49503
49600
  rulerTicks: optional(number),
49504
49601
  range: pair(pairOfNumbers, pairOfNumbers),
49505
- graph: parsePerseusGraphType,
49602
+ // NOTE(benchristel): I copied the default graph from
49603
+ // interactive-graph.tsx. See the parse-perseus-json/README.md for
49604
+ // an explanation of why we want to duplicate the default here.
49605
+ graph: defaulted(parsePerseusGraphType, () => ({
49606
+ type: "linear"
49607
+ })),
49506
49608
  correct: parsePerseusGraphType,
49507
49609
  // TODO(benchristel): default lockedFigures to empty array
49508
49610
  lockedFigures: optional(array(parseLockedFigure)),
@@ -49570,7 +49672,10 @@ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
49570
49672
  isTickCtrl: optional(nullable(boolean)),
49571
49673
  divisionRange: array(number),
49572
49674
  numDivisions: optional(nullable(number)),
49573
- snapDivisions: number,
49675
+ // NOTE(benchristel): I copied the default snapDivisions from
49676
+ // number-line.tsx. See the parse-perseus-json/README.md for
49677
+ // an explanation of why we want to duplicate the default here.
49678
+ snapDivisions: defaulted(number, () => 2),
49574
49679
  tickStep: optional(nullable(number)),
49575
49680
  correctRel: optional(nullable(string)),
49576
49681
  correctX: number,
@@ -49583,18 +49688,18 @@ const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "d
49583
49688
  const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
49584
49689
  answers: array(object({
49585
49690
  message: string,
49586
- value: number,
49691
+ value: optional(number),
49587
49692
  status: string,
49588
49693
  answerForms: optional(array(parseMathFormat)),
49589
49694
  strict: boolean,
49590
49695
  maxError: optional(nullable(number)),
49591
49696
  simplify: optional(nullable(string))
49592
49697
  })),
49593
- labelText: string,
49698
+ labelText: optional(string),
49594
49699
  size: string,
49595
49700
  coefficient: boolean,
49596
49701
  rightAlign: optional(boolean),
49597
- static: boolean,
49702
+ static: defaulted(boolean, () => false),
49598
49703
  answerForms: optional(array(object({
49599
49704
  name: parseMathFormat,
49600
49705
  simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
@@ -49607,12 +49712,18 @@ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
49607
49712
  function parseRenderer(rawValue, ctx) {
49608
49713
  return parsePerseusRenderer(rawValue, ctx);
49609
49714
  }
49715
+ const largeToAuto = (height, ctx) => {
49716
+ if (height === "large") {
49717
+ return ctx.success("auto");
49718
+ }
49719
+ return ctx.success(height);
49720
+ };
49610
49721
  const parseOrdererWidget = parseWidget(constant("orderer"), object({
49611
- options: array(parseRenderer),
49722
+ options: defaulted(array(parseRenderer), () => []),
49612
49723
  correctOptions: array(parseRenderer),
49613
49724
  otherOptions: array(parseRenderer),
49614
- height: enumeration("normal", "auto"),
49615
- layout: enumeration("horizontal", "vertical")
49725
+ height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
49726
+ layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
49616
49727
  }));
49617
49728
 
49618
49729
  const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
@@ -49626,7 +49737,7 @@ const parsePassageWidget = parseWidget(constant("passage"), object({
49626
49737
  passageText: string,
49627
49738
  passageTitle: string,
49628
49739
  showLineNumbers: boolean,
49629
- static: boolean
49740
+ static: defaulted(boolean, () => false)
49630
49741
  }));
49631
49742
 
49632
49743
  const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
@@ -49647,7 +49758,10 @@ const parsePlotterWidget = parseWidget(constant("plotter"), object({
49647
49758
  picUrl: optional(nullable(string)),
49648
49759
  picSize: optional(nullable(number)),
49649
49760
  picBoxHeight: optional(nullable(number)),
49650
- plotDimensions: array(number)
49761
+ // NOTE(benchristel): I copied the default plotDimensions from
49762
+ // plotter.tsx. See the parse-perseus-json/README.md for an explanation
49763
+ // of why we want to duplicate the defaults here.
49764
+ plotDimensions: defaulted(array(number), () => [380, 300])
49651
49765
  }));
49652
49766
 
49653
49767
  const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
@@ -49800,6 +49914,10 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
49800
49914
  return parseAndAssign(`table ${id}`, parseTableWidget);
49801
49915
  case "video":
49802
49916
  return parseAndAssign(`video ${id}`, parseVideoWidget);
49917
+ case "sequence":
49918
+ // sequence is a deprecated widget type, and the corresponding
49919
+ // widget component no longer exists.
49920
+ return parseAndAssign(`sequence ${id}`, parseDeprecatedWidget);
49803
49921
  default:
49804
49922
  if (getWidget(type)) {
49805
49923
  // @ts-expect-error - 'type' is not a valid widget type
@@ -49808,6 +49926,11 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
49808
49926
  return ctx.failure("a valid widget type", type);
49809
49927
  }
49810
49928
  };
49929
+ const parseDeprecatedWidget = parseWidget(
49930
+ // Ignore the incoming widget type and hardcode "deprecated-standin"
49931
+ (_, ctx) => ctx.success("deprecated-standin"),
49932
+ // Allow any widget options
49933
+ object({}));
49811
49934
  const parseStringToPositiveInt = (rawValue, ctx) => {
49812
49935
  if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
49813
49936
  return ctx.failure("numeric string", rawValue);
@@ -50089,6 +50212,9 @@ const GrapherUtil = {
50089
50212
  getEquationString,
50090
50213
  typeToButton
50091
50214
  };
50215
+ const ScoringUtil = {
50216
+ keScoreFromPerseusScore
50217
+ };
50092
50218
 
50093
50219
  exports.ApiOptions = ApiOptions;
50094
50220
  exports.ArticleRenderer = ArticleRenderer;
@@ -50122,6 +50248,7 @@ exports.PlotterWidget = Plotter$1;
50122
50248
  exports.QUESTION_WIDGETS = QUESTION_WIDGETS;
50123
50249
  exports.Radio = Radio;
50124
50250
  exports.Renderer = Renderer;
50251
+ exports.ScoringUtil = ScoringUtil;
50125
50252
  exports.ServerItemRenderer = ref;
50126
50253
  exports.TableWidget = Table$1;
50127
50254
  exports.Util = Util;