@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/es/index.js CHANGED
@@ -2793,12 +2793,6 @@ const rWidgetRule = /^\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]/;
2793
2793
  const rTypeFromWidgetId = /^([a-z-]+) ([0-9]+)$/;
2794
2794
  const rWidgetParts = new RegExp(rWidgetRule.source + "$");
2795
2795
  const snowman = "\u2603";
2796
- const noScore = {
2797
- type: "points",
2798
- earned: 0,
2799
- total: 0,
2800
- message: null
2801
- };
2802
2796
  const seededRNG$1 = function seededRNG(seed) {
2803
2797
  let randomSeed = seed;
2804
2798
  return function () {
@@ -2875,97 +2869,6 @@ const split = "x".split(/(.)/g).length ? function (str, r) {
2875
2869
  return output;
2876
2870
  };
2877
2871
 
2878
- /**
2879
- * Combine two score objects.
2880
- *
2881
- * Given two score objects for two different widgets, combine them so that
2882
- * if one is wrong, the total score is wrong, etc.
2883
- */
2884
- function combineScores(scoreA, scoreB) {
2885
- let message;
2886
- if (scoreA.type === "points" && scoreB.type === "points") {
2887
- if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
2888
- // TODO(alpert): Figure out how to combine messages usefully
2889
- message = null;
2890
- } else {
2891
- message = scoreA.message || scoreB.message;
2892
- }
2893
- return {
2894
- type: "points",
2895
- earned: scoreA.earned + scoreB.earned,
2896
- total: scoreA.total + scoreB.total,
2897
- message: message
2898
- };
2899
- }
2900
- if (scoreA.type === "points" && scoreB.type === "invalid") {
2901
- return scoreB;
2902
- }
2903
- if (scoreA.type === "invalid" && scoreB.type === "points") {
2904
- return scoreA;
2905
- }
2906
- if (scoreA.type === "invalid" && scoreB.type === "invalid") {
2907
- if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
2908
- // TODO(alpert): Figure out how to combine messages usefully
2909
- message = null;
2910
- } else {
2911
- message = scoreA.message || scoreB.message;
2912
- }
2913
- return {
2914
- type: "invalid",
2915
- message: message
2916
- };
2917
- }
2918
-
2919
- /**
2920
- * The above checks cover all combinations of score type, so if we get here
2921
- * then something is amiss with our inputs.
2922
- */
2923
- throw new PerseusError("PerseusScore with unknown type encountered", Errors.InvalidInput, {
2924
- metadata: {
2925
- scoreA: JSON.stringify(scoreA),
2926
- scoreB: JSON.stringify(scoreB)
2927
- }
2928
- });
2929
- }
2930
- function flattenScores(widgetScoreMap) {
2931
- return Object.values(widgetScoreMap).reduce(combineScores, noScore);
2932
- }
2933
- function isCorrect(score) {
2934
- return score.type === "points" && score.earned >= score.total;
2935
- }
2936
- function keScoreFromPerseusScore(score,
2937
- // It's weird, but this is what we're passing it
2938
- guess, state) {
2939
- if (score.type === "points") {
2940
- return {
2941
- empty: false,
2942
- correct: isCorrect(score),
2943
- message: score.message,
2944
- guess: guess,
2945
- state: state
2946
- };
2947
- }
2948
- if (score.type === "invalid") {
2949
- return {
2950
- empty: true,
2951
- correct: false,
2952
- message: score.message,
2953
- suppressAlmostThere: score.suppressAlmostThere,
2954
- guess: guess,
2955
- state: state
2956
- };
2957
- }
2958
- throw new PerseusError(
2959
- // @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
2960
- "Invalid score type: " + score.type, Errors.InvalidInput, {
2961
- metadata: {
2962
- score: JSON.stringify(score),
2963
- guess: JSON.stringify(guess),
2964
- state: JSON.stringify(state)
2965
- }
2966
- });
2967
- }
2968
-
2969
2872
  /**
2970
2873
  * Return the first valid interpretation of 'text' as a number, in the form
2971
2874
  * {value: 2.3, exact: true}.
@@ -3263,28 +3166,6 @@ function strongEncodeURIComponent(str) {
3263
3166
  .replace(/\*/g, "%2A");
3264
3167
  }
3265
3168
 
3266
- /**
3267
- * If a widget says that it is empty once it is graded.
3268
- * Trying to encapsulate references to the score format.
3269
- */
3270
- function scoreIsEmpty(score) {
3271
- // HACK(benkomalo): ugh. this isn't great; the Perseus score objects
3272
- // overload the type "invalid" for what should probably be three
3273
- // distinct cases:
3274
- // - truly empty or not fully filled out
3275
- // - invalid or malformed inputs
3276
- // - "almost correct" like inputs where the widget wants to give
3277
- // feedback (e.g. a fraction needs to be reduced, or `pi` should
3278
- // be used instead of 3.14)
3279
- //
3280
- // Unfortunately the coercion happens all over the place, as these
3281
- // Perseus style score objects are created *everywhere* (basically
3282
- // in every widget), so it's hard to change now. We assume that
3283
- // anything with a "message" is not truly empty, and one of the
3284
- // latter two cases for now.
3285
- return score.type === "invalid" && (!score.message || score.message.length === 0);
3286
- }
3287
-
3288
3169
  /*
3289
3170
  * The touchHandlers are used to track the current state of the touch
3290
3171
  * event, such as whether or not the user is currently pressed down (either
@@ -3459,13 +3340,9 @@ const Util = {
3459
3340
  rTypeFromWidgetId,
3460
3341
  rWidgetParts,
3461
3342
  snowman,
3462
- noScore,
3463
3343
  seededRNG: seededRNG$1,
3464
3344
  shuffle: shuffle$3,
3465
3345
  split,
3466
- combineScores,
3467
- flattenScores,
3468
- keScoreFromPerseusScore,
3469
3346
  firstNumericalParse: firstNumericalParse$1,
3470
3347
  stringArrayOfSize: stringArrayOfSize$1,
3471
3348
  gridDimensionConfig,
@@ -3481,7 +3358,6 @@ const Util = {
3481
3358
  parseQueryString,
3482
3359
  updateQueryString: updateQueryString$2,
3483
3360
  strongEncodeURIComponent,
3484
- scoreIsEmpty,
3485
3361
  touchHandlers,
3486
3362
  resetTouchHandlers,
3487
3363
  extractPointerLocation,
@@ -3797,6 +3673,7 @@ Zoom.prototype.zoomImage = function () {
3797
3673
  this._zoomOriginal();
3798
3674
  }.bind(this);
3799
3675
  img.src = this._targetImage.src;
3676
+ img.alt = this._targetImage.alt;
3800
3677
  this.$zoomedImage = $zoomedImage;
3801
3678
  };
3802
3679
  Zoom.prototype._zoomOriginal = function () {
@@ -8872,7 +8749,10 @@ var Tex$1 = {
8872
8749
  // We use createElement instead of JSX here because we can't name this file tex.tsx;
8873
8750
  // that name is already taken.
8874
8751
  // eslint-disable-next-line import/no-deprecated
8875
- render( /*#__PURE__*/React.createElement(TeX, {
8752
+ render(
8753
+ /*#__PURE__*/
8754
+ // eslint-disable-next-line react/no-children-prop
8755
+ React.createElement(TeX, {
8876
8756
  children: text,
8877
8757
  onRender: callback
8878
8758
  }), $texHolder[0]);
@@ -14609,7 +14489,7 @@ const pluck = function pluck(table, subKey) {
14609
14489
  * });
14610
14490
  * {a: 2, b: 3}
14611
14491
  */
14612
- const mapObject$2 = function mapObject(obj, lambda) {
14492
+ const mapObject = function mapObject(obj, lambda) {
14613
14493
  const result = {};
14614
14494
  Object.keys(obj).forEach(key => {
14615
14495
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
@@ -14618,27 +14498,6 @@ const mapObject$2 = function mapObject(obj, lambda) {
14618
14498
  return result;
14619
14499
  };
14620
14500
 
14621
- /**
14622
- * Maps an array to an object
14623
- *
14624
- * > mapObjectFromArray(['a', 'b'], function(elem) {
14625
- * return elem + elem;
14626
- * });
14627
- * {a: 'aa', b: 'bb'}
14628
- */
14629
- const mapObjectFromArray = function mapObjectFromArray(arr, lambda) {
14630
- const result = {};
14631
- arr.forEach(elem => {
14632
- result[elem] = lambda(elem);
14633
- });
14634
- return result;
14635
- };
14636
- var objective_ = {
14637
- pluck: pluck,
14638
- mapObject: mapObject$2,
14639
- mapObjectFromArray: mapObjectFromArray
14640
- };
14641
-
14642
14501
  /**
14643
14502
  * MovableLine
14644
14503
  */
@@ -14703,7 +14562,7 @@ _.extend(MovableLine$4.prototype, {
14703
14562
  // - are objects, not primitives (and need a deeper copy)
14704
14563
  // - they don't need getters created for them
14705
14564
  // TODO(jack): Consider "default" once we es3ify perseus
14706
- objective_.pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
14565
+ pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
14707
14566
  },
14708
14567
  /**
14709
14568
  * Resets the object to its state as if it were constructed with
@@ -15203,7 +15062,7 @@ class MovablePoint$5 {
15203
15062
  // - are objects, not primitives (and need a deeper copy)
15204
15063
  // - they don't need getters created for them
15205
15064
  // TODO(jack): Consider "default" once we es3ify perseus
15206
- objective_.pluck(MovablePointOptions, "standard")
15065
+ pluck(MovablePointOptions, "standard")
15207
15066
  // We only update props here, because we want things on state to
15208
15067
  // be persistent, and updated appropriately in modify()
15209
15068
  ), DEFAULT_PROPS$1);
@@ -16094,7 +15953,7 @@ _.extend(MovablePolygon.prototype, {
16094
15953
  // - are objects, not primitives (and need a deeper copy)
16095
15954
  // - they don't need getters created for them
16096
15955
  // TODO(jack): Consider "default" once we es3ify perseus
16097
- objective_.pluck(MovablePolygonOptions, "standard")
15956
+ pluck(MovablePolygonOptions, "standard")
16098
15957
 
16099
15958
  // We only update props here, because we want things on state to
16100
15959
  // be persistent, and updated appropriately in modify()
@@ -18719,6 +18578,126 @@ class QuestionParagraph extends React.Component {
18719
18578
  }
18720
18579
  }
18721
18580
 
18581
+ const noScore = {
18582
+ type: "points",
18583
+ earned: 0,
18584
+ total: 0,
18585
+ message: null
18586
+ };
18587
+
18588
+ /**
18589
+ * If a widget says that it is empty once it is graded.
18590
+ * Trying to encapsulate references to the score format.
18591
+ */
18592
+ function scoreIsEmpty(score) {
18593
+ // HACK(benkomalo): ugh. this isn't great; the Perseus score objects
18594
+ // overload the type "invalid" for what should probably be three
18595
+ // distinct cases:
18596
+ // - truly empty or not fully filled out
18597
+ // - invalid or malformed inputs
18598
+ // - "almost correct" like inputs where the widget wants to give
18599
+ // feedback (e.g. a fraction needs to be reduced, or `pi` should
18600
+ // be used instead of 3.14)
18601
+ //
18602
+ // Unfortunately the coercion happens all over the place, as these
18603
+ // Perseus style score objects are created *everywhere* (basically
18604
+ // in every widget), so it's hard to change now. We assume that
18605
+ // anything with a "message" is not truly empty, and one of the
18606
+ // latter two cases for now.
18607
+ return score.type === "invalid" && (!score.message || score.message.length === 0);
18608
+ }
18609
+
18610
+ /**
18611
+ * Combine two score objects.
18612
+ *
18613
+ * Given two score objects for two different widgets, combine them so that
18614
+ * if one is wrong, the total score is wrong, etc.
18615
+ */
18616
+ function combineScores(scoreA, scoreB) {
18617
+ let message;
18618
+ if (scoreA.type === "points" && scoreB.type === "points") {
18619
+ if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
18620
+ // TODO(alpert): Figure out how to combine messages usefully
18621
+ message = null;
18622
+ } else {
18623
+ message = scoreA.message || scoreB.message;
18624
+ }
18625
+ return {
18626
+ type: "points",
18627
+ earned: scoreA.earned + scoreB.earned,
18628
+ total: scoreA.total + scoreB.total,
18629
+ message: message
18630
+ };
18631
+ }
18632
+ if (scoreA.type === "points" && scoreB.type === "invalid") {
18633
+ return scoreB;
18634
+ }
18635
+ if (scoreA.type === "invalid" && scoreB.type === "points") {
18636
+ return scoreA;
18637
+ }
18638
+ if (scoreA.type === "invalid" && scoreB.type === "invalid") {
18639
+ if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
18640
+ // TODO(alpert): Figure out how to combine messages usefully
18641
+ message = null;
18642
+ } else {
18643
+ message = scoreA.message || scoreB.message;
18644
+ }
18645
+ return {
18646
+ type: "invalid",
18647
+ message: message
18648
+ };
18649
+ }
18650
+
18651
+ /**
18652
+ * The above checks cover all combinations of score type, so if we get here
18653
+ * then something is amiss with our inputs.
18654
+ */
18655
+ throw new PerseusError("PerseusScore with unknown type encountered", Errors.InvalidInput, {
18656
+ metadata: {
18657
+ scoreA: JSON.stringify(scoreA),
18658
+ scoreB: JSON.stringify(scoreB)
18659
+ }
18660
+ });
18661
+ }
18662
+ function flattenScores(widgetScoreMap) {
18663
+ return Object.values(widgetScoreMap).reduce(combineScores, noScore);
18664
+ }
18665
+ function isCorrect(score) {
18666
+ return score.type === "points" && score.earned >= score.total;
18667
+ }
18668
+ function keScoreFromPerseusScore(score,
18669
+ // It's weird, but this is what we're passing it
18670
+ guess, state) {
18671
+ if (score.type === "points") {
18672
+ return {
18673
+ empty: false,
18674
+ correct: isCorrect(score),
18675
+ message: score.message,
18676
+ guess: guess,
18677
+ state: state
18678
+ };
18679
+ }
18680
+ if (score.type === "invalid") {
18681
+ return {
18682
+ empty: true,
18683
+ correct: false,
18684
+ message: score.message,
18685
+ suppressAlmostThere: score.suppressAlmostThere,
18686
+ guess: guess,
18687
+ state: state
18688
+ };
18689
+ }
18690
+ throw new PerseusError(
18691
+ // @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
18692
+ "Invalid score type: " + score.type, Errors.InvalidInput, {
18693
+ metadata: {
18694
+ score: JSON.stringify(score),
18695
+ guess: JSON.stringify(guess),
18696
+ state: JSON.stringify(state)
18697
+ }
18698
+ });
18699
+ }
18700
+
18722
18701
  // These are all the current widgets that require user-input and are supported for automatic scoring
18723
18702
  const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
18724
18703
 
@@ -19284,12 +19263,9 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19284
19263
  isLintable: isLintable
19285
19264
  });
19286
19265
 
19287
- const {
19288
- mapObject: mapObject$1
19289
- } = objective_;
19290
19266
  function getUpgradedWidgetOptions(oldWidgetOptions) {
19291
19267
  // @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; }'.
19292
- return mapObject$1(oldWidgetOptions, (widgetInfo, widgetId) => {
19268
+ return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
19293
19269
  if (!widgetInfo.type || !widgetInfo.alignment) {
19294
19270
  const newValues = {};
19295
19271
  if (!widgetInfo.type) {
@@ -19317,17 +19293,10 @@ widgetIds, userInputMap, strings, locale) {
19317
19293
  // Static widgets shouldn't count as empty
19318
19294
  return false;
19319
19295
  }
19320
- let score = null;
19321
- const userInput = userInputMap[id];
19322
19296
  const scorer = getWidgetScorer(widget.type);
19323
- if (widget.type === "group") {
19324
- const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19325
- score = Util.flattenScores(scores);
19326
- } else if (scorer) {
19327
- score = scorer(userInput, widget.options, strings, locale);
19328
- }
19297
+ const score = scorer == null ? void 0 : scorer(userInputMap[id], widget.options, strings, locale);
19329
19298
  if (score) {
19330
- return Util.scoreIsEmpty(score);
19299
+ return scoreIsEmpty(score);
19331
19300
  }
19332
19301
  });
19333
19302
  }
@@ -19343,7 +19312,7 @@ strings, locale) {
19343
19312
  // so this checks that the widgets are being used before scoring them
19344
19313
  const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
19345
19314
  const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
19346
- return Util.flattenScores(scores);
19315
+ return flattenScores(scores);
19347
19316
  }
19348
19317
  function scoreWidgetsFunctional(widgets,
19349
19318
  // This is a port of old code, I'm not sure why
@@ -19365,11 +19334,9 @@ widgetIds, userInputMap, strings, locale) {
19365
19334
  }
19366
19335
  const userInput = userInputMap[id];
19367
19336
  const scorer = getWidgetScorer(widget.type);
19368
- if (widget.type === "group") {
19369
- const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
19370
- widgetScores[id] = Util.flattenScores(scores);
19371
- } else if (scorer) {
19372
- widgetScores[id] = scorer(userInput, widget.options, strings, locale);
19337
+ const score = scorer == null ? void 0 : scorer(userInput, widget.options, strings, locale);
19338
+ if (score != null) {
19339
+ widgetScores[id] = score;
19373
19340
  }
19374
19341
  });
19375
19342
  return widgetScores;
@@ -19675,9 +19642,6 @@ WidgetContainer.defaultProps = {
19675
19642
  linterContext: linterContextDefault
19676
19643
  };
19677
19644
 
19678
- const {
19679
- mapObject
19680
- } = objective_;
19681
19645
  const rContainsNonWhitespace = /\S/;
19682
19646
  const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
19683
19647
  const noopOnRender = () => {};
@@ -20603,7 +20567,7 @@ class Renderer extends React.Component {
20603
20567
  }, 0);
20604
20568
  });
20605
20569
  };
20606
- this.setInputValue = (path, newValue, focus) => {
20570
+ this.setInputValue = (path, newValue, cb) => {
20607
20571
  // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
20608
20572
  const widgetId = _.first(path);
20609
20573
  // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
@@ -20611,7 +20575,7 @@ class Renderer extends React.Component {
20611
20575
  const widget = this.getWidgetInstance(widgetId);
20612
20576
 
20613
20577
  // Widget handles parsing of the interWidgetPath.
20614
- widget == null || widget.setInputValue == null || widget.setInputValue(interWidgetPath, newValue, focus);
20578
+ widget == null || widget.setInputValue == null || widget.setInputValue(interWidgetPath, newValue, cb);
20615
20579
  };
20616
20580
  /**
20617
20581
  * Returns an array of all widget IDs in the order they occur in
@@ -20628,28 +20592,6 @@ class Renderer extends React.Component {
20628
20592
  const totalScore = this.score();
20629
20593
  return [totalGuess, totalScore];
20630
20594
  };
20631
- this.examples = () => {
20632
- const widgetIds = this.widgetIds;
20633
- const examples = widgetIds.map(widgetId => {
20634
- const widget = this.getWidgetInstance(widgetId);
20635
- return widget != null && widget.examples ? widget.examples() : null;
20636
- }).filter(Boolean);
20637
-
20638
- // no widgets with examples
20639
- if (!examples.length) {
20640
- return null;
20641
- }
20642
- const allEqual = _.all(examples, function (example) {
20643
- return _.isEqual(examples[0], example);
20644
- });
20645
-
20646
- // some widgets have different examples
20647
- // TODO(alex): handle this better
20648
- if (!allEqual) {
20649
- return null;
20650
- }
20651
- return examples[0];
20652
- };
20653
20595
  // TranslationLinter callback
20654
20596
  this.handletranslationLintErrors = lintErrors => {
20655
20597
  if (!this._isMounted) {
@@ -20832,6 +20774,12 @@ class Renderer extends React.Component {
20832
20774
  isLastUsedWidget: widgetId === this.state.lastUsedWidgetId
20833
20775
  });
20834
20776
  }
20777
+ /**
20778
+ * Returns an array of widget ids that are empty (meaning widgets where the
20779
+ * learner has not interacted with the widget yet or has not filled in all
20780
+ * fields). For example, the `interactive-graph` widget is considered
20781
+ * empty if the graph is in the starting state.
20782
+ */
20835
20783
  emptyWidgets() {
20836
20784
  return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20837
20785
  }
@@ -20894,7 +20842,7 @@ class Renderer extends React.Component {
20894
20842
  */
20895
20843
  score() {
20896
20844
  const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20897
- const combinedScore = Util.flattenScores(scores);
20845
+ const combinedScore = flattenScores(scores);
20898
20846
  return combinedScore;
20899
20847
  }
20900
20848
  render() {
@@ -21096,6 +21044,7 @@ class TextInput extends React.Component {
21096
21044
  value: formattedValue,
21097
21045
  type: "text",
21098
21046
  "aria-label": labelText,
21047
+ "aria-describedby": this.props["aria-describedby"],
21099
21048
  onChange: value => this.props.onChange(value),
21100
21049
  placeholder: placeholder,
21101
21050
  onFocus: onFocus,
@@ -21362,6 +21311,7 @@ class Tooltip extends React.Component {
21362
21311
  }, /*#__PURE__*/React.createElement("div", {
21363
21312
  ref: this.tooltipContainerRef,
21364
21313
  className: "tooltipContainer",
21314
+ role: "tooltip",
21365
21315
  style: {
21366
21316
  position: "absolute",
21367
21317
  // height must start out undefined, not null, so that
@@ -21442,9 +21392,15 @@ class InputWithExamples extends React.Component {
21442
21392
  };
21443
21393
  this._renderInput = () => {
21444
21394
  const id = this._getUniqueId();
21395
+ const ariaId = `aria-for-${id}`;
21396
+ // Generate text from a known set of format options that will read well in a screen reader
21397
+ const examplesAria = this.props.examples.length === 7 ? "" : `${this.props.examples[0]}
21398
+ ${this.props.examples.slice(1).join(", or\n")}`
21399
+ // @ts-expect-error TS2550: Property replaceAll does not exist on type string.
21400
+ .replaceAll("*", "").replaceAll("$", "").replaceAll("\\ \\text{pi}", " pi").replaceAll("\\ ", " and ");
21445
21401
  const inputProps = {
21446
21402
  id: id,
21447
- "aria-describedby": id,
21403
+ "aria-describedby": ariaId,
21448
21404
  ref: "input",
21449
21405
  className: this._getInputClassName(),
21450
21406
  labelText: this.props.labelText,
@@ -21460,7 +21416,12 @@ class InputWithExamples extends React.Component {
21460
21416
  autoCorrect: "off",
21461
21417
  spellCheck: "false"
21462
21418
  };
21463
- return /*#__PURE__*/React.createElement(TextInput, inputProps);
21419
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(TextInput, inputProps), /*#__PURE__*/React.createElement("span", {
21420
+ id: ariaId,
21421
+ style: {
21422
+ display: "none"
21423
+ }
21424
+ }, examplesAria));
21464
21425
  };
21465
21426
  this._handleFocus = () => {
21466
21427
  this.props.onFocus();
@@ -21502,8 +21463,12 @@ class InputWithExamples extends React.Component {
21502
21463
  }
21503
21464
  render() {
21504
21465
  const input = this._renderInput();
21505
- const examplesContent = _.map(this.props.examples, example => {
21506
- return "- " + example;
21466
+ const examplesContent = this.props.examples.length <= 2 ? this.props.examples.join(" ") // A single item (with or without leading text) is not a "list"
21467
+ : this.props.examples // 2 or more items should display as a list
21468
+ .map((example, index) => {
21469
+ // If the first example is bold, then it is most likely a heading/leading text.
21470
+ // So, it shouldn't be part of the list.
21471
+ return index === 0 && example.startsWith("**") ? `${example}\n` : `- ${example}`;
21507
21472
  }).join("\n");
21508
21473
  const showExamples = this.props.shouldShowExamples && this.state.showExamples;
21509
21474
  return /*#__PURE__*/React.createElement(Tooltip, {
@@ -21938,15 +21903,6 @@ class InputNumber extends React.Component {
21938
21903
  currentValue: newValue
21939
21904
  }, cb);
21940
21905
  };
21941
- this.examples = () => {
21942
- const {
21943
- strings
21944
- } = this.context;
21945
- const type = this.props.answerType;
21946
- const forms = answerTypes[type].forms.split(/\s*,\s*/);
21947
- const examples = _.map(forms, form => formExamples$1[form](this.props, strings));
21948
- return [strings.yourAnswer].concat(examples);
21949
- };
21950
21906
  }
21951
21907
  static getUserInputFromProps(props) {
21952
21908
  return {
@@ -21959,6 +21915,15 @@ class InputNumber extends React.Component {
21959
21915
  getPromptJSON() {
21960
21916
  return getPromptJSON$s(this.props, this.getUserInput());
21961
21917
  }
21918
+ examples() {
21919
+ const {
21920
+ strings
21921
+ } = this.context;
21922
+ const type = this.props.answerType;
21923
+ const forms = answerTypes[type].forms.split(/\s*,\s*/);
21924
+ const examples = _.map(forms, form => formExamples$1[form](this.props, strings));
21925
+ return [strings.yourAnswer].concat(examples);
21926
+ }
21962
21927
  render() {
21963
21928
  if (this.props.apiOptions.customKeypad) {
21964
21929
  // TODO(charlie): Support "Review Mode".
@@ -22177,7 +22142,7 @@ function scoreNumericInput(userInput, rubric, strings) {
22177
22142
  // We may have received TeX; try to parse it before grading.
22178
22143
  // If `currentValue` is not TeX, this should be a no-op.
22179
22144
  const currentValue = ParseTex(userInput.currentValue);
22180
- const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => Math.abs(answer.value) <= 1);
22145
+ const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value !== undefined && Math.abs(answer.value) <= 1);
22181
22146
 
22182
22147
  // The coefficient is an attribute of the widget
22183
22148
  let localValue = currentValue;
@@ -22238,33 +22203,16 @@ class NumericInput extends React.Component {
22238
22203
  this.state = {
22239
22204
  // keeps track of the other set of values when switching
22240
22205
  // between 0 and finite solutions
22241
- previousValues: [""]
22242
- };
22243
- // TODO(Nicole, Jeremy): This is maybe never used and should be removed
22244
- this.examples = () => {
22245
- var _this$props$answerFor;
22246
- // if the set of specified forms are empty, allow all forms
22247
- const forms = ((_this$props$answerFor = this.props.answerForms) == null ? void 0 : _this$props$answerFor.length) !== 0 ? this.props.answerForms : Object.keys(formExamples).map(name => {
22248
- return {
22249
- name: name,
22250
- simplify: "required"
22251
- };
22252
- });
22253
- let examples = _.map(forms, form => {
22254
- return formExamples[form.name](form, this.context.strings);
22255
- });
22256
- // Ensure no duplicate tooltip text from simplified and unsimplified
22257
- // versions of the same format
22258
- examples = _.uniq(examples);
22259
- return [this.context.strings.yourAnswer].concat(examples);
22206
+ previousValues: [""],
22207
+ isFocused: false
22260
22208
  };
22261
22209
  this.shouldShowExamples = () => {
22262
- var _this$props$answerFor2, _this$props$answerFor3;
22263
- const noFormsAccepted = ((_this$props$answerFor2 = this.props.answerForms) == null ? void 0 : _this$props$answerFor2.length) === 0;
22210
+ var _this$props$answerFor, _this$props$answerFor2;
22211
+ const noFormsAccepted = ((_this$props$answerFor = this.props.answerForms) == null ? void 0 : _this$props$answerFor.length) === 0;
22264
22212
  // To check if all answer forms are accepted, we must first
22265
22213
  // find the *names* of all accepted forms, and see if they are
22266
22214
  // all present, ignoring duplicates
22267
- const answerFormNames = _.uniq((_this$props$answerFor3 = this.props.answerForms) == null ? void 0 : _this$props$answerFor3.map(form => form.name));
22215
+ const answerFormNames = _.uniq((_this$props$answerFor2 = this.props.answerForms) == null ? void 0 : _this$props$answerFor2.map(form => form.name));
22268
22216
  const allFormsAccepted = answerFormNames.length >= Object.keys(formExamples).length;
22269
22217
  return !noFormsAccepted && !allFormsAccepted;
22270
22218
  };
@@ -22301,9 +22249,19 @@ class NumericInput extends React.Component {
22301
22249
  };
22302
22250
  this._handleFocus = () => {
22303
22251
  this.props.onFocus([]);
22252
+ this.setState(currentState => {
22253
+ return _extends({}, currentState, {
22254
+ isFocused: true
22255
+ });
22256
+ });
22304
22257
  };
22305
22258
  this._handleBlur = () => {
22306
22259
  this.props.onBlur([]);
22260
+ this.setState(currentState => {
22261
+ return _extends({}, currentState, {
22262
+ isFocused: false
22263
+ });
22264
+ });
22307
22265
  };
22308
22266
  }
22309
22267
  static getUserInputFromProps(props) {
@@ -22311,6 +22269,27 @@ class NumericInput extends React.Component {
22311
22269
  currentValue: props.currentValue
22312
22270
  };
22313
22271
  }
22272
+ /**
22273
+ * Generates a string that demonstrates how to input the various supported
22274
+ * answer forms.
22275
+ */
22276
+ examples() {
22277
+ var _this$props$answerFor3;
22278
+ // if the set of specified forms are empty, allow all forms
22279
+ const forms = ((_this$props$answerFor3 = this.props.answerForms) == null ? void 0 : _this$props$answerFor3.length) !== 0 ? this.props.answerForms : Object.keys(formExamples).map(name => {
22280
+ return {
22281
+ name: name,
22282
+ simplify: "required"
22283
+ };
22284
+ });
22285
+ let examples = _.map(forms, form => {
22286
+ return formExamples[form.name](form, this.context.strings);
22287
+ });
22288
+ // Ensure no duplicate tooltip text from simplified and unsimplified
22289
+ // versions of the same format
22290
+ examples = _.uniq(examples);
22291
+ return [this.context.strings.yourAnswer].concat(examples);
22292
+ }
22314
22293
  getUserInput() {
22315
22294
  return NumericInput.getUserInputFromProps(this.props);
22316
22295
  }
@@ -22346,13 +22325,20 @@ class NumericInput extends React.Component {
22346
22325
  // component.
22347
22326
  const styles = StyleSheet.create({
22348
22327
  input: {
22328
+ borderRadius: "3px",
22329
+ borderWidth: this.state.isFocused ? "2px" : "1px",
22330
+ display: "inline-block",
22331
+ fontFamily: `Symbola, "Times New Roman", serif`,
22332
+ fontSize: "18px",
22333
+ height: "32px",
22334
+ lineHeight: "18px",
22335
+ padding: this.state.isFocused ? "4px" : "4px 5px",
22336
+ // account for added focus border thickness
22349
22337
  textAlign: this.props.rightAlign ? "right" : "left",
22350
- width: this.props.size === "small" ? 40 : 80,
22351
- padding: 0,
22352
- height: "auto"
22338
+ width: this.props.size === "small" ? 40 : 80
22353
22339
  }
22354
22340
  });
22355
- return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(InputWithExamples, {
22341
+ return /*#__PURE__*/React.createElement(InputWithExamples, {
22356
22342
  ref: ref => this.inputRef = ref,
22357
22343
  value: this.props.currentValue,
22358
22344
  onChange: this.handleChange,
@@ -22364,7 +22350,7 @@ class NumericInput extends React.Component {
22364
22350
  id: this.props.widgetId,
22365
22351
  disabled: this.props.apiOptions.readOnly,
22366
22352
  style: styles.input
22367
- }));
22353
+ });
22368
22354
  }
22369
22355
  }
22370
22356
 
@@ -27260,8 +27246,8 @@ var Definition$1 = {
27260
27246
  scorer: () => scoreNoop()
27261
27247
  };
27262
27248
 
27263
- // The props are type `any` on purpose so that this can receive props
27264
- // from any deprecated widget
27249
+ // There are no required props, but this component can receive props from any
27250
+ // deprecated widget
27265
27251
  class DeprecatedStandin extends React__default.Component {
27266
27252
  constructor(...args) {
27267
27253
  super(...args);
@@ -29371,6 +29357,13 @@ const getPromptJSON$f = rendererJSON => {
29371
29357
  });
29372
29358
  };
29373
29359
 
29360
+ // The `group` widget is basically a widget hosting a full Perseus system in
29361
+ // it. As such, scoring a group means scoring all widgets it contains.
29362
+ function scoreGroup(userInput, options, strings, locale) {
29363
+ const scores = scoreWidgetsFunctional(options.widgets, Object.keys(options.widgets), userInput, strings, locale);
29364
+ return flattenScores(scores);
29365
+ }
29366
+
29374
29367
  // exports has no 'transform'
29375
29368
 
29376
29369
  class Group extends React.Component {
@@ -29525,6 +29518,9 @@ var Group$1 = {
29525
29518
  displayName: "Group (SAT only)",
29526
29519
  widget: Group,
29527
29520
  traverseChildWidgets: traverseChildWidgets,
29521
+ // TODO(LEMS-2656): remove TS suppression
29522
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
29523
+ scorer: scoreGroup,
29528
29524
  hidden: true,
29529
29525
  isLintable: true
29530
29526
  };
@@ -32234,6 +32230,61 @@ const LegacyGrid = ({
32234
32230
 
32235
32231
  // Range is replaced within this file with Interval, but it is used elsewhere
32236
32232
  // and exported from the package, so we need to keep it around.
32233
+ /**
32234
+ * Our core set of Perseus widgets.
32235
+ *
32236
+ * This interface is the basis for "registering" all Perseus widget types.
32237
+ * There should be one key/value pair for each supported widget. If you create
32238
+ * a new widget, an entry should be added to this interface. Note that this
32239
+ * only registers the widget options type, you'll also need to register the
32240
+ * widget so that it's available at runtime (@see
32241
+ * {@link file://./widgets.ts#registerWidget}).
32242
+ *
32243
+ * Importantly, the key should be the name that is used in widget IDs. For most
32244
+ * widgets that is the same as the widget option's `type` field. In cases where
32245
+ * a widget has been deprecated and replaced with the deprecated-standin
32246
+ * widget, it should be the original widget type!
32247
+ *
32248
+ * If you define the widget outside of this package, you can still add the new
32249
+ * widget to this interface by writing the following in that package that
32250
+ * contains the widget. TypeScript will merge that definition of the
32251
+ * `PerseusWidgets` with the one defined below.
32252
+ *
32253
+ * ```typescript
32254
+ * declare module "@khanacademy/perseus" {
32255
+ * interface PerseusWidgetTypes {
32256
+ * // A new widget
32257
+ * "new-awesomeness": MyAwesomeNewWidget;
32258
+ *
32259
+ * // A deprecated widget
32260
+ * "super-old-widget": AutoCorrectWidget;
32261
+ * }
32262
+ * }
32263
+ *
32264
+ * // The new widget's options definition
32265
+ * type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>;
32266
+ *
32267
+ * // The deprecated widget's options definition
32268
+ * type SuperOldWidget = WidgetOptions<'super-old-widget', object>;
32269
+ * ```
32270
+ *
32271
+ * This interface can be extended through the magic of TypeScript "Declaration
32272
+ * merging". Specifically, we augment this module and extend this interface.
32273
+ *
32274
+ * @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
32275
+ */
32276
+ /**
32277
+ * A map of widget IDs to widget options. This is most often used as the type
32278
+ * for a set of widgets defined in a `PerseusItem` but can also be useful to
32279
+ * represent a function parameter where only `widgets` from a `PerseusItem` are
32280
+ * needed. Today Widget IDs are made up of the widget type and an incrementing
32281
+ * integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid
32282
+ * reading/parsing the widget id to derive any information from it, except in
32283
+ * the case of this map.
32284
+ *
32285
+ * @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
32286
+ * by augmenting the PerseusWidgetTypes with new widget types!
32287
+ */
32237
32288
  /**
32238
32289
  * A "PerseusItem" is a classic Perseus item. It is rendered by the
32239
32290
  * `ServerItemRenderer` and the layout is pre-set.
@@ -32249,6 +32300,9 @@ const LegacyGrid = ({
32249
32300
  * A "MultiItem" is an advanced Perseus item. It is rendered by the
32250
32301
  * `MultiRenderer` and you can control the layout of individual parts of the
32251
32302
  * item.
32303
+ *
32304
+ * @deprecated MultiItem support is slated for removal in a future Perseus
32305
+ * release.
32252
32306
  */
32253
32307
  const ItemExtras = [
32254
32308
  // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
@@ -32270,6 +32324,11 @@ const ItemExtras = [
32270
32324
  // The user might benefit from using a statistics Z Table like https://www.ztable.net/
32271
32325
  "zTable"];
32272
32326
 
32327
+ /**
32328
+ * The type representing the common structure of all widget's options. The
32329
+ * `Options` generic type represents the widget-specific option data.
32330
+ */
32331
+
32273
32332
  // prettier-ignore
32274
32333
 
32275
32334
  // prettier-ignore
@@ -32338,7 +32397,9 @@ const ItemExtras = [
32338
32397
 
32339
32398
  //prettier-ignore
32340
32399
 
32341
- // A background image applied to various widgets.
32400
+ /**
32401
+ * A background image applied to various widgets.
32402
+ */
32342
32403
 
32343
32404
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
32344
32405
 
@@ -36906,8 +36967,6 @@ const makeInvalidTypeError = (functionName, graphType) => {
36906
36967
  });
36907
36968
  };
36908
36969
 
36909
- // There's no transform function in exports
36910
-
36911
36970
  // TODO: there's another, very similar getSinusoidCoefficients function
36912
36971
  // they should probably be merged
36913
36972
  function getSinusoidCoefficients(coords) {
@@ -42043,23 +42102,37 @@ const getPromptJSON$6 = (renderProps, userInput) => {
42043
42102
  };
42044
42103
  };
42045
42104
 
42046
- function scoreNumberLine(state, rubric) {
42047
- const range = rubric.range;
42048
- const divisionRange = state.divisionRange;
42049
- const start = rubric.initialX != null ? rubric.initialX : range[0];
42050
- const startRel = rubric.isInequality ? "ge" : "eq";
42051
- const correctRel = rubric.correctRel || "eq";
42052
- const correctPos = number$1.equal(state.numLinePosition, rubric.correctX || 0);
42053
- const outsideAllowedRange = state.numDivisions > divisionRange[1] || state.numDivisions < divisionRange[0];
42105
+ /**
42106
+ * Checks user input is within the allowed range and not the same as the initial
42107
+ * state.
42108
+ * @param userInput
42109
+ * @see 'scoreNumberLine' for the scoring logic.
42110
+ */
42111
+ function validateNumberLine(userInput) {
42112
+ const divisionRange = userInput.divisionRange;
42113
+ const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
42054
42114
 
42055
42115
  // TODO: I don't think isTickCrtl is a thing anymore
42056
- if (state.isTickCrtl && outsideAllowedRange) {
42116
+ if (userInput.isTickCrtl && outsideAllowedRange) {
42057
42117
  return {
42058
42118
  type: "invalid",
42059
42119
  message: "Number of divisions is outside the allowed range."
42060
42120
  };
42061
42121
  }
42062
- if (correctPos && correctRel === state.rel) {
42122
+ return null;
42123
+ }
42124
+
42125
+ function scoreNumberLine(userInput, scoringData) {
42126
+ const validationError = validateNumberLine(userInput);
42127
+ if (validationError) {
42128
+ return validationError;
42129
+ }
42130
+ const range = scoringData.range;
42131
+ const start = scoringData.initialX != null ? scoringData.initialX : range[0];
42132
+ const startRel = scoringData.isInequality ? "ge" : "eq";
42133
+ const correctRel = scoringData.correctRel || "eq";
42134
+ const correctPos = number$1.equal(userInput.numLinePosition, scoringData.correctX || 0);
42135
+ if (correctPos && correctRel === userInput.rel) {
42063
42136
  return {
42064
42137
  type: "points",
42065
42138
  earned: 1,
@@ -42067,7 +42140,7 @@ function scoreNumberLine(state, rubric) {
42067
42140
  message: null
42068
42141
  };
42069
42142
  }
42070
- if (state.numLinePosition === start && state.rel === startRel) {
42143
+ if (userInput.numLinePosition === start && userInput.rel === startRel) {
42071
42144
  // We're where we started.
42072
42145
  return {
42073
42146
  type: "invalid",
@@ -45013,7 +45086,7 @@ const init = function init() {
45013
45086
 
45014
45087
  // This file is processed by a Rollup plugin (replace) to inject the production
45015
45088
  const libName = "@khanacademy/perseus";
45016
- const libVersion = "45.0.0";
45089
+ const libVersion = "46.0.0";
45017
45090
  addLibraryVersionToPerseusDebug(libName, libVersion);
45018
45091
 
45019
45092
  const apiVersion = {
@@ -45621,14 +45694,10 @@ class ServerItemRenderer extends React.Component {
45621
45694
  });
45622
45695
  }
45623
45696
  componentDidUpdate(prevProps, prevState) {
45624
- if (this.props.apiOptions.answerableCallback) {
45697
+ const answerableCallback = this.props.apiOptions.answerableCallback;
45698
+ if (answerableCallback != null) {
45625
45699
  const isAnswerable = this.questionRenderer.emptyWidgets().length === 0;
45626
- const {
45627
- answerableCallback
45628
- } = this.props.apiOptions;
45629
- if (answerableCallback) {
45630
- answerableCallback(isAnswerable);
45631
- }
45700
+ answerableCallback(isAnswerable);
45632
45701
  }
45633
45702
  if (!this._fullyRendered) {
45634
45703
  const assetsLoaded = Object.values(this.state.assetStatuses).every(Boolean);
@@ -45792,7 +45861,7 @@ class ServerItemRenderer extends React.Component {
45792
45861
  // TODO(alex): Check whether we rely on the format here for
45793
45862
  // analyzing ProblemLogs. If not, remove this layer.
45794
45863
  const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
45795
- const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
45864
+ const keScore = keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
45796
45865
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45797
45866
  this.setState({
45798
45867
  questionCompleted: keScore.correct,
@@ -45971,7 +46040,7 @@ const traverseRenderer$1 = function traverseRenderer(rendererOptions, contentCal
45971
46040
  newContent = modifiedContent;
45972
46041
  }
45973
46042
  }
45974
- const newWidgets = objective_.mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
46043
+ const newWidgets = mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
45975
46044
  // Widgets without info or a type are empty widgets, and
45976
46045
  // should always be renderable. It's also annoying to write
45977
46046
  // checks for this everywhere, so we just filter them out once and
@@ -46871,7 +46940,7 @@ class MultiRenderer extends React.Component {
46871
46940
  if (ref.getSerializedState) {
46872
46941
  state = ref.getSerializedState();
46873
46942
  }
46874
- return Util.keScoreFromPerseusScore(score, guess, state);
46943
+ return keScoreFromPerseusScore(score, guess, state);
46875
46944
  }
46876
46945
 
46877
46946
  /**
@@ -46901,8 +46970,8 @@ class MultiRenderer extends React.Component {
46901
46970
  scores.push(data.ref.score());
46902
46971
  return (_data$ref = data.ref) == null ? void 0 : _data$ref.getUserInput();
46903
46972
  });
46904
- const combinedScore = scores.reduce(Util.combineScores);
46905
- return Util.keScoreFromPerseusScore(combinedScore, guess, state);
46973
+ const combinedScore = scores.reduce(combineScores);
46974
+ return keScoreFromPerseusScore(combinedScore, guess, state);
46906
46975
  }
46907
46976
 
46908
46977
  /**
@@ -48570,7 +48639,7 @@ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
48570
48639
  const parseDefinitionWidget = parseWidget(constant("definition"), object({
48571
48640
  togglePrompt: string,
48572
48641
  definition: string,
48573
- static: boolean
48642
+ static: defaulted(boolean, () => false)
48574
48643
  }));
48575
48644
 
48576
48645
  const parseDropdownWidget = parseWidget(constant("dropdown"), object({
@@ -48772,15 +48841,26 @@ const parseIframeWidget = parseWidget(constant("iframe"), object({
48772
48841
  static: defaulted(boolean, () => false)
48773
48842
  }));
48774
48843
 
48844
+ const stringToNumber = (rawValue, ctx) => {
48845
+ if (typeof rawValue === "number") {
48846
+ return ctx.success(rawValue);
48847
+ }
48848
+ const parsedNumber = +rawValue;
48849
+ if (rawValue === "" || isNaN(parsedNumber)) {
48850
+ return ctx.failure("a number or numeric string", rawValue);
48851
+ }
48852
+ return ctx.success(parsedNumber);
48853
+ };
48854
+
48855
+ const numericToNumber = pipeParsers(union(number).or(string).parser).then(stringToNumber).parser;
48775
48856
  const parsePerseusImageBackground = object({
48776
48857
  url: optional(nullable(string)),
48777
- width: optional(number),
48778
- height: optional(number),
48779
- top: optional(number),
48780
- left: optional(number),
48781
- bottom: optional(number),
48782
- // TODO(benchristel): convert scale to a number
48783
- scale: optional(union(number).or(string).parser)
48858
+ width: optional(numericToNumber),
48859
+ height: optional(numericToNumber),
48860
+ top: optional(numericToNumber),
48861
+ left: optional(numericToNumber),
48862
+ bottom: optional(numericToNumber),
48863
+ scale: optional(numericToNumber)
48784
48864
  });
48785
48865
 
48786
48866
  const pairOfNumbers$2 = pair(number, number);
@@ -48799,6 +48879,12 @@ const parseImageWidget = parseWidget(constant("image"), object({
48799
48879
  box: optional(pairOfNumbers$2)
48800
48880
  }));
48801
48881
 
48882
+ const booleanToString = (rawValue, ctx) => {
48883
+ if (typeof rawValue === "boolean") {
48884
+ return ctx.success(String(rawValue));
48885
+ }
48886
+ return ctx.failure("boolean", rawValue);
48887
+ };
48802
48888
  const parseInputNumberWidget = parseWidget(constant("input-number"), object({
48803
48889
  answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
48804
48890
  inexact: optional(boolean),
@@ -48806,7 +48892,11 @@ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
48806
48892
  rightAlign: optional(boolean),
48807
48893
  simplify: enumeration("required", "optional", "enforced"),
48808
48894
  size: enumeration("normal", "small"),
48809
- value: union(number).or(string).parser,
48895
+ // TODO(benchristel): there are some content items where value is a
48896
+ // boolean, even though that makes no sense. We should figure out if
48897
+ // those content items are actually published anywhere, and consider
48898
+ // updating them.
48899
+ value: union(number).or(string).or(booleanToString).parser,
48810
48900
  customKeypad: optional(boolean)
48811
48901
  }));
48812
48902
 
@@ -49104,18 +49194,27 @@ const parseLockedFunctionType = object({
49104
49194
  const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
49105
49195
  const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
49106
49196
  step: pairOfNumbers,
49107
- gridStep: pairOfNumbers,
49108
- snapStep: pairOfNumbers,
49197
+ // TODO(benchristel): rather than making gridStep and snapStep
49198
+ // optional, we should duplicate the defaulting logic from the
49199
+ // InteractiveGraph component. See parse-perseus-json/README.md for
49200
+ // why.
49201
+ gridStep: optional(pairOfNumbers),
49202
+ snapStep: optional(pairOfNumbers),
49109
49203
  backgroundImage: optional(parsePerseusImageBackground),
49110
49204
  markings: enumeration("graph", "grid", "none"),
49111
- labels: array(string),
49205
+ labels: optional(array(string)),
49112
49206
  showProtractor: boolean,
49113
49207
  showRuler: optional(boolean),
49114
49208
  showTooltips: optional(boolean),
49115
49209
  rulerLabel: optional(string),
49116
49210
  rulerTicks: optional(number),
49117
49211
  range: pair(pairOfNumbers, pairOfNumbers),
49118
- graph: parsePerseusGraphType,
49212
+ // NOTE(benchristel): I copied the default graph from
49213
+ // interactive-graph.tsx. See the parse-perseus-json/README.md for
49214
+ // an explanation of why we want to duplicate the default here.
49215
+ graph: defaulted(parsePerseusGraphType, () => ({
49216
+ type: "linear"
49217
+ })),
49119
49218
  correct: parsePerseusGraphType,
49120
49219
  // TODO(benchristel): default lockedFigures to empty array
49121
49220
  lockedFigures: optional(array(parseLockedFigure)),
@@ -49183,7 +49282,10 @@ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
49183
49282
  isTickCtrl: optional(nullable(boolean)),
49184
49283
  divisionRange: array(number),
49185
49284
  numDivisions: optional(nullable(number)),
49186
- snapDivisions: number,
49285
+ // NOTE(benchristel): I copied the default snapDivisions from
49286
+ // number-line.tsx. See the parse-perseus-json/README.md for
49287
+ // an explanation of why we want to duplicate the default here.
49288
+ snapDivisions: defaulted(number, () => 2),
49187
49289
  tickStep: optional(nullable(number)),
49188
49290
  correctRel: optional(nullable(string)),
49189
49291
  correctX: number,
@@ -49196,18 +49298,18 @@ const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "d
49196
49298
  const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
49197
49299
  answers: array(object({
49198
49300
  message: string,
49199
- value: number,
49301
+ value: optional(number),
49200
49302
  status: string,
49201
49303
  answerForms: optional(array(parseMathFormat)),
49202
49304
  strict: boolean,
49203
49305
  maxError: optional(nullable(number)),
49204
49306
  simplify: optional(nullable(string))
49205
49307
  })),
49206
- labelText: string,
49308
+ labelText: optional(string),
49207
49309
  size: string,
49208
49310
  coefficient: boolean,
49209
49311
  rightAlign: optional(boolean),
49210
- static: boolean,
49312
+ static: defaulted(boolean, () => false),
49211
49313
  answerForms: optional(array(object({
49212
49314
  name: parseMathFormat,
49213
49315
  simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
@@ -49220,12 +49322,18 @@ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
49220
49322
  function parseRenderer(rawValue, ctx) {
49221
49323
  return parsePerseusRenderer(rawValue, ctx);
49222
49324
  }
49325
+ const largeToAuto = (height, ctx) => {
49326
+ if (height === "large") {
49327
+ return ctx.success("auto");
49328
+ }
49329
+ return ctx.success(height);
49330
+ };
49223
49331
  const parseOrdererWidget = parseWidget(constant("orderer"), object({
49224
- options: array(parseRenderer),
49332
+ options: defaulted(array(parseRenderer), () => []),
49225
49333
  correctOptions: array(parseRenderer),
49226
49334
  otherOptions: array(parseRenderer),
49227
- height: enumeration("normal", "auto"),
49228
- layout: enumeration("horizontal", "vertical")
49335
+ height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
49336
+ layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
49229
49337
  }));
49230
49338
 
49231
49339
  const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
@@ -49239,7 +49347,7 @@ const parsePassageWidget = parseWidget(constant("passage"), object({
49239
49347
  passageText: string,
49240
49348
  passageTitle: string,
49241
49349
  showLineNumbers: boolean,
49242
- static: boolean
49350
+ static: defaulted(boolean, () => false)
49243
49351
  }));
49244
49352
 
49245
49353
  const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
@@ -49260,7 +49368,10 @@ const parsePlotterWidget = parseWidget(constant("plotter"), object({
49260
49368
  picUrl: optional(nullable(string)),
49261
49369
  picSize: optional(nullable(number)),
49262
49370
  picBoxHeight: optional(nullable(number)),
49263
- plotDimensions: array(number)
49371
+ // NOTE(benchristel): I copied the default plotDimensions from
49372
+ // plotter.tsx. See the parse-perseus-json/README.md for an explanation
49373
+ // of why we want to duplicate the defaults here.
49374
+ plotDimensions: defaulted(array(number), () => [380, 300])
49264
49375
  }));
49265
49376
 
49266
49377
  const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
@@ -49412,6 +49523,10 @@ const parseWidgetsMapEntry = ([key, widget], widgetMap, ctx) => {
49412
49523
  return parseAndAssign(`table ${id}`, parseTableWidget);
49413
49524
  case "video":
49414
49525
  return parseAndAssign(`video ${id}`, parseVideoWidget);
49526
+ case "sequence":
49527
+ // sequence is a deprecated widget type, and the corresponding
49528
+ // widget component no longer exists.
49529
+ return parseAndAssign(`sequence ${id}`, parseDeprecatedWidget);
49415
49530
  default:
49416
49531
  if (getWidget(type)) {
49417
49532
  // @ts-expect-error - 'type' is not a valid widget type
@@ -49420,6 +49535,11 @@ const parseWidgetsMapEntry = ([key, widget], widgetMap, ctx) => {
49420
49535
  return ctx.failure("a valid widget type", type);
49421
49536
  }
49422
49537
  };
49538
+ const parseDeprecatedWidget = parseWidget(
49539
+ // Ignore the incoming widget type and hardcode "deprecated-standin"
49540
+ (_, ctx) => ctx.success("deprecated-standin"),
49541
+ // Allow any widget options
49542
+ object({}));
49423
49543
  const parseStringToPositiveInt = (rawValue, ctx) => {
49424
49544
  if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
49425
49545
  return ctx.failure("numeric string", rawValue);
@@ -49694,6 +49814,9 @@ const GrapherUtil = {
49694
49814
  getEquationString,
49695
49815
  typeToButton
49696
49816
  };
49817
+ const ScoringUtil = {
49818
+ keScoreFromPerseusScore
49819
+ };
49697
49820
 
49698
- export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusExpressionAnswerFormConsidered, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, addWidget, apiVersion, bodyXsmallBold, buildEmptyItemTreeForShape, buildMapper, components, containerSizeClass, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getClockwiseAngle, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, lockedFigureColors, lockedFigureFillStyles, makeSafeUrl, mathOnlyParser, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, scorePerseusItem, shapes, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
49821
+ export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusExpressionAnswerFormConsidered, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ScoringUtil, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, addWidget, apiVersion, bodyXsmallBold, buildEmptyItemTreeForShape, buildMapper, components, containerSizeClass, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getClockwiseAngle, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, lockedFigureColors, lockedFigureFillStyles, makeSafeUrl, mathOnlyParser, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, scorePerseusItem, shapes, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
49699
49822
  //# sourceMappingURL=index.js.map