@khanacademy/perseus 45.0.0 → 45.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3918,6 +3918,7 @@ Zoom.prototype.zoomImage = function () {
3918
3918
  this._zoomOriginal();
3919
3919
  }.bind(this);
3920
3920
  img.src = this._targetImage.src;
3921
+ img.alt = this._targetImage.alt;
3921
3922
  this.$zoomedImage = $zoomedImage;
3922
3923
  };
3923
3924
  Zoom.prototype._zoomOriginal = function () {
@@ -8993,7 +8994,10 @@ var Tex$1 = {
8993
8994
  // We use createElement instead of JSX here because we can't name this file tex.tsx;
8994
8995
  // that name is already taken.
8995
8996
  // eslint-disable-next-line import/no-deprecated
8996
- render( /*#__PURE__*/React__namespace.createElement(TeX, {
8997
+ render(
8998
+ /*#__PURE__*/
8999
+ // eslint-disable-next-line react/no-children-prop
9000
+ React__namespace.createElement(TeX, {
8997
9001
  children: text,
8998
9002
  onRender: callback
8999
9003
  }), $texHolder[0]);
@@ -14757,7 +14761,7 @@ const pluck = function (table, subKey) {
14757
14761
  * });
14758
14762
  * {a: 2, b: 3}
14759
14763
  */
14760
- const mapObject$2 = function (obj, lambda) {
14764
+ const mapObject = function (obj, lambda) {
14761
14765
  const result = {};
14762
14766
  Object.keys(obj).forEach(key => {
14763
14767
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
@@ -14766,27 +14770,6 @@ const mapObject$2 = function (obj, lambda) {
14766
14770
  return result;
14767
14771
  };
14768
14772
 
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
14773
  /**
14791
14774
  * MovableLine
14792
14775
  */
@@ -14851,7 +14834,7 @@ ___default["default"].extend(MovableLine$4.prototype, {
14851
14834
  // - are objects, not primitives (and need a deeper copy)
14852
14835
  // - they don't need getters created for them
14853
14836
  // TODO(jack): Consider "default" once we es3ify perseus
14854
- objective_.pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
14837
+ pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
14855
14838
  },
14856
14839
  /**
14857
14840
  * Resets the object to its state as if it were constructed with
@@ -15358,7 +15341,7 @@ class MovablePoint$5 {
15358
15341
  // - are objects, not primitives (and need a deeper copy)
15359
15342
  // - they don't need getters created for them
15360
15343
  // TODO(jack): Consider "default" once we es3ify perseus
15361
- objective_.pluck(MovablePointOptions, "standard")
15344
+ pluck(MovablePointOptions, "standard")
15362
15345
  // We only update props here, because we want things on state to
15363
15346
  // be persistent, and updated appropriately in modify()
15364
15347
  ),
@@ -16259,7 +16242,7 @@ ___default["default"].extend(MovablePolygon.prototype, {
16259
16242
  // - are objects, not primitives (and need a deeper copy)
16260
16243
  // - they don't need getters created for them
16261
16244
  // TODO(jack): Consider "default" once we es3ify perseus
16262
- objective_.pluck(MovablePolygonOptions, "standard")
16245
+ pluck(MovablePolygonOptions, "standard")
16263
16246
 
16264
16247
  // We only update props here, because we want things on state to
16265
16248
  // be persistent, and updated appropriately in modify()
@@ -19481,12 +19464,9 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19481
19464
  isLintable: isLintable
19482
19465
  });
19483
19466
 
19484
- const {
19485
- mapObject: mapObject$1
19486
- } = objective_;
19487
19467
  function getUpgradedWidgetOptions(oldWidgetOptions) {
19488
19468
  // @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) => {
19469
+ return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
19490
19470
  if (!widgetInfo.type || !widgetInfo.alignment) {
19491
19471
  const newValues = {};
19492
19472
  if (!widgetInfo.type) {
@@ -19517,15 +19497,8 @@ widgetIds, userInputMap, strings, locale) {
19517
19497
  // Static widgets shouldn't count as empty
19518
19498
  return false;
19519
19499
  }
19520
- let score = null;
19521
- const userInput = userInputMap[id];
19522
19500
  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
- }
19501
+ const score = scorer?.(userInputMap[id], widget.options, strings, locale);
19529
19502
  if (score) {
19530
19503
  return Util.scoreIsEmpty(score);
19531
19504
  }
@@ -19565,11 +19538,9 @@ widgetIds, userInputMap, strings, locale) {
19565
19538
  }
19566
19539
  const userInput = userInputMap[id];
19567
19540
  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);
19541
+ const score = scorer?.(userInput, widget.options, strings, locale);
19542
+ if (score != null) {
19543
+ widgetScores[id] = score;
19573
19544
  }
19574
19545
  });
19575
19546
  return widgetScores;
@@ -19876,9 +19847,6 @@ class WidgetContainer extends React__namespace.Component {
19876
19847
  }
19877
19848
  }
19878
19849
 
19879
- const {
19880
- mapObject
19881
- } = objective_;
19882
19850
  const rContainsNonWhitespace = /\S/;
19883
19851
  const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
19884
19852
  const noopOnRender = () => {};
@@ -20995,6 +20963,13 @@ class Renderer extends React__namespace.Component {
20995
20963
  }, this);
20996
20964
  return state;
20997
20965
  };
20966
+
20967
+ /**
20968
+ * Returns an array of widget ids that are empty (meaning widgets where the
20969
+ * learner has not interacted with the widget yet or has not filled in all
20970
+ * fields). For example, the `interactive-graph` widget is considered
20971
+ * empty if the graph is in the starting state.
20972
+ */
20998
20973
  emptyWidgets() {
20999
20974
  return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
21000
20975
  }
@@ -21048,7 +21023,7 @@ class Renderer extends React__namespace.Component {
21048
21023
  }, 0);
21049
21024
  });
21050
21025
  };
21051
- setInputValue = (path, newValue, focus) => {
21026
+ setInputValue = (path, newValue, cb) => {
21052
21027
  // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
21053
21028
  const widgetId = ___default["default"].first(path);
21054
21029
  // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
@@ -21056,7 +21031,7 @@ class Renderer extends React__namespace.Component {
21056
21031
  const widget = this.getWidgetInstance(widgetId);
21057
21032
 
21058
21033
  // Widget handles parsing of the interWidgetPath.
21059
- widget?.setInputValue?.(interWidgetPath, newValue, focus);
21034
+ widget?.setInputValue?.(interWidgetPath, newValue, cb);
21060
21035
  };
21061
21036
 
21062
21037
  /**
@@ -21354,6 +21329,7 @@ class TextInput extends React__namespace.Component {
21354
21329
  value: formattedValue,
21355
21330
  type: "text",
21356
21331
  "aria-label": labelText,
21332
+ "aria-describedby": this.props["aria-describedby"],
21357
21333
  onChange: value => this.props.onChange(value),
21358
21334
  placeholder: placeholder,
21359
21335
  onFocus: onFocus,
@@ -21612,6 +21588,7 @@ class Tooltip extends React__namespace.Component {
21612
21588
  }, /*#__PURE__*/React__namespace.createElement("div", {
21613
21589
  ref: this.tooltipContainerRef,
21614
21590
  className: "tooltipContainer",
21591
+ role: "tooltip",
21615
21592
  style: {
21616
21593
  position: "absolute",
21617
21594
  // height must start out undefined, not null, so that
@@ -21688,9 +21665,15 @@ class InputWithExamples extends React__namespace.Component {
21688
21665
  };
21689
21666
  _renderInput = () => {
21690
21667
  const id = this._getUniqueId();
21668
+ const ariaId = `aria-for-${id}`;
21669
+ // Generate text from a known set of format options that will read well in a screen reader
21670
+ const examplesAria = this.props.examples.length === 7 ? "" : `${this.props.examples[0]}
21671
+ ${this.props.examples.slice(1).join(", or\n")}`
21672
+ // @ts-expect-error TS2550: Property replaceAll does not exist on type string.
21673
+ .replaceAll("*", "").replaceAll("$", "").replaceAll("\\ \\text{pi}", " pi").replaceAll("\\ ", " and ");
21691
21674
  const inputProps = {
21692
21675
  id: id,
21693
- "aria-describedby": id,
21676
+ "aria-describedby": ariaId,
21694
21677
  ref: "input",
21695
21678
  className: this._getInputClassName(),
21696
21679
  labelText: this.props.labelText,
@@ -21706,7 +21689,12 @@ class InputWithExamples extends React__namespace.Component {
21706
21689
  autoCorrect: "off",
21707
21690
  spellCheck: "false"
21708
21691
  };
21709
- return /*#__PURE__*/React__namespace.createElement(TextInput, inputProps);
21692
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(TextInput, inputProps), /*#__PURE__*/React__namespace.createElement("span", {
21693
+ id: ariaId,
21694
+ style: {
21695
+ display: "none"
21696
+ }
21697
+ }, examplesAria));
21710
21698
  };
21711
21699
  _handleFocus = () => {
21712
21700
  this.props.onFocus();
@@ -21747,8 +21735,12 @@ class InputWithExamples extends React__namespace.Component {
21747
21735
  };
21748
21736
  render() {
21749
21737
  const input = this._renderInput();
21750
- const examplesContent = ___default["default"].map(this.props.examples, example => {
21751
- return "- " + example;
21738
+ const examplesContent = this.props.examples.length <= 2 ? this.props.examples.join(" ") // A single item (with or without leading text) is not a "list"
21739
+ : this.props.examples // 2 or more items should display as a list
21740
+ .map((example, index) => {
21741
+ // If the first example is bold, then it is most likely a heading/leading text.
21742
+ // So, it shouldn't be part of the list.
21743
+ return index === 0 && example.startsWith("**") ? `${example}\n` : `- ${example}`;
21752
21744
  }).join("\n");
21753
21745
  const showExamples = this.props.shouldShowExamples && this.state.showExamples;
21754
21746
  return /*#__PURE__*/React__namespace.createElement(Tooltip, {
@@ -22402,7 +22394,7 @@ function scoreNumericInput(userInput, rubric, strings) {
22402
22394
  // We may have received TeX; try to parse it before grading.
22403
22395
  // If `currentValue` is not TeX, this should be a no-op.
22404
22396
  const currentValue = ParseTex(userInput.currentValue);
22405
- const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => Math.abs(answer.value) <= 1);
22397
+ const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value !== undefined && Math.abs(answer.value) <= 1);
22406
22398
 
22407
22399
  // The coefficient is an attribute of the widget
22408
22400
  let localValue = currentValue;
@@ -22478,7 +22470,8 @@ class NumericInput extends React__namespace.Component {
22478
22470
  state = {
22479
22471
  // keeps track of the other set of values when switching
22480
22472
  // between 0 and finite solutions
22481
- previousValues: [""]
22473
+ previousValues: [""],
22474
+ isFocused: false
22482
22475
  };
22483
22476
 
22484
22477
  // TODO(Nicole, Jeremy): This is maybe never used and should be removed
@@ -22543,9 +22536,21 @@ class NumericInput extends React__namespace.Component {
22543
22536
  };
22544
22537
  _handleFocus = () => {
22545
22538
  this.props.onFocus([]);
22539
+ this.setState(currentState => {
22540
+ return {
22541
+ ...currentState,
22542
+ isFocused: true
22543
+ };
22544
+ });
22546
22545
  };
22547
22546
  _handleBlur = () => {
22548
22547
  this.props.onBlur([]);
22548
+ this.setState(currentState => {
22549
+ return {
22550
+ ...currentState,
22551
+ isFocused: false
22552
+ };
22553
+ });
22549
22554
  };
22550
22555
  render() {
22551
22556
  let labelText = this.props.labelText;
@@ -22576,13 +22581,20 @@ class NumericInput extends React__namespace.Component {
22576
22581
  // component.
22577
22582
  const styles = aphrodite.StyleSheet.create({
22578
22583
  input: {
22584
+ borderRadius: "3px",
22585
+ borderWidth: this.state.isFocused ? "2px" : "1px",
22586
+ display: "inline-block",
22587
+ fontFamily: `Symbola, "Times New Roman", serif`,
22588
+ fontSize: "18px",
22589
+ height: "32px",
22590
+ lineHeight: "18px",
22591
+ padding: this.state.isFocused ? "4px" : "4px 5px",
22592
+ // account for added focus border thickness
22579
22593
  textAlign: this.props.rightAlign ? "right" : "left",
22580
- width: this.props.size === "small" ? 40 : 80,
22581
- padding: 0,
22582
- height: "auto"
22594
+ width: this.props.size === "small" ? 40 : 80
22583
22595
  }
22584
22596
  });
22585
- return /*#__PURE__*/React__namespace.createElement("div", null, /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
22597
+ return /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
22586
22598
  ref: ref => this.inputRef = ref,
22587
22599
  value: this.props.currentValue,
22588
22600
  onChange: this.handleChange,
@@ -22594,7 +22606,7 @@ class NumericInput extends React__namespace.Component {
22594
22606
  id: this.props.widgetId,
22595
22607
  disabled: this.props.apiOptions.readOnly,
22596
22608
  style: styles.input
22597
- }));
22609
+ });
22598
22610
  }
22599
22611
  }
22600
22612
 
@@ -27522,8 +27534,8 @@ var Definition$1 = {
27522
27534
  scorer: () => scoreNoop()
27523
27535
  };
27524
27536
 
27525
- // The props are type `any` on purpose so that this can receive props
27526
- // from any deprecated widget
27537
+ // There are no required props, but this component can receive props from any
27538
+ // deprecated widget
27527
27539
  class DeprecatedStandin extends React__default["default"].Component {
27528
27540
  static contextType = PerseusI18nContext;
27529
27541
  // this just helps with TS weak typing when a Widget
@@ -29646,6 +29658,13 @@ const getPromptJSON$f = rendererJSON => {
29646
29658
  };
29647
29659
  };
29648
29660
 
29661
+ // The `group` widget is basically a widget hosting a full Perseus system in
29662
+ // it. As such, scoring a group means scoring all widgets it contains.
29663
+ function scoreGroup(userInput, options, strings, locale) {
29664
+ const scores = scoreWidgetsFunctional(options.widgets, Object.keys(options.widgets), userInput, strings, locale);
29665
+ return Util.flattenScores(scores);
29666
+ }
29667
+
29649
29668
  // exports has no 'transform'
29650
29669
 
29651
29670
  class Group extends React__namespace.Component {
@@ -29797,6 +29816,9 @@ var Group$1 = {
29797
29816
  displayName: "Group (SAT only)",
29798
29817
  widget: Group,
29799
29818
  traverseChildWidgets: traverseChildWidgets,
29819
+ // TODO(LEMS-2656): remove TS suppression
29820
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
29821
+ scorer: scoreGroup,
29800
29822
  hidden: true,
29801
29823
  isLintable: true
29802
29824
  };
@@ -32540,6 +32562,61 @@ const LegacyGrid = _ref => {
32540
32562
 
32541
32563
  // Range is replaced within this file with Interval, but it is used elsewhere
32542
32564
  // and exported from the package, so we need to keep it around.
32565
+ /**
32566
+ * Our core set of Perseus widgets.
32567
+ *
32568
+ * This interface is the basis for "registering" all Perseus widget types.
32569
+ * There should be one key/value pair for each supported widget. If you create
32570
+ * a new widget, an entry should be added to this interface. Note that this
32571
+ * only registers the widget options type, you'll also need to register the
32572
+ * widget so that it's available at runtime (@see
32573
+ * {@link file://./widgets.ts#registerWidget}).
32574
+ *
32575
+ * Importantly, the key should be the name that is used in widget IDs. For most
32576
+ * widgets that is the same as the widget option's `type` field. In cases where
32577
+ * a widget has been deprecated and replaced with the deprecated-standin
32578
+ * widget, it should be the original widget type!
32579
+ *
32580
+ * If you define the widget outside of this package, you can still add the new
32581
+ * widget to this interface by writing the following in that package that
32582
+ * contains the widget. TypeScript will merge that definition of the
32583
+ * `PerseusWidgets` with the one defined below.
32584
+ *
32585
+ * ```typescript
32586
+ * declare module "@khanacademy/perseus" {
32587
+ * interface PerseusWidgetTypes {
32588
+ * // A new widget
32589
+ * "new-awesomeness": MyAwesomeNewWidget;
32590
+ *
32591
+ * // A deprecated widget
32592
+ * "super-old-widget": AutoCorrectWidget;
32593
+ * }
32594
+ * }
32595
+ *
32596
+ * // The new widget's options definition
32597
+ * type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>;
32598
+ *
32599
+ * // The deprecated widget's options definition
32600
+ * type SuperOldWidget = WidgetOptions<'super-old-widget', object>;
32601
+ * ```
32602
+ *
32603
+ * This interface can be extended through the magic of TypeScript "Declaration
32604
+ * merging". Specifically, we augment this module and extend this interface.
32605
+ *
32606
+ * @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
32607
+ */
32608
+ /**
32609
+ * A map of widget IDs to widget options. This is most often used as the type
32610
+ * for a set of widgets defined in a `PerseusItem` but can also be useful to
32611
+ * represent a function parameter where only `widgets` from a `PerseusItem` are
32612
+ * needed. Today Widget IDs are made up of the widget type and an incrementing
32613
+ * integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid
32614
+ * reading/parsing the widget id to derive any information from it, except in
32615
+ * the case of this map.
32616
+ *
32617
+ * @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
32618
+ * by augmenting the PerseusWidgetTypes with new widget types!
32619
+ */
32543
32620
  /**
32544
32621
  * A "PerseusItem" is a classic Perseus item. It is rendered by the
32545
32622
  * `ServerItemRenderer` and the layout is pre-set.
@@ -32555,6 +32632,9 @@ const LegacyGrid = _ref => {
32555
32632
  * A "MultiItem" is an advanced Perseus item. It is rendered by the
32556
32633
  * `MultiRenderer` and you can control the layout of individual parts of the
32557
32634
  * item.
32635
+ *
32636
+ * @deprecated MultiItem support is slated for removal in a future Perseus
32637
+ * release.
32558
32638
  */
32559
32639
  const ItemExtras = [
32560
32640
  // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
@@ -32576,6 +32656,11 @@ const ItemExtras = [
32576
32656
  // The user might benefit from using a statistics Z Table like https://www.ztable.net/
32577
32657
  "zTable"];
32578
32658
 
32659
+ /**
32660
+ * The type representing the common structure of all widget's options. The
32661
+ * `Options` generic type represents the widget-specific option data.
32662
+ */
32663
+
32579
32664
  // prettier-ignore
32580
32665
 
32581
32666
  // prettier-ignore
@@ -32644,7 +32729,9 @@ const ItemExtras = [
32644
32729
 
32645
32730
  //prettier-ignore
32646
32731
 
32647
- // A background image applied to various widgets.
32732
+ /**
32733
+ * A background image applied to various widgets.
32734
+ */
32648
32735
 
32649
32736
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
32650
32737
 
@@ -37288,8 +37375,6 @@ const makeInvalidTypeError = (functionName, graphType) => {
37288
37375
  });
37289
37376
  };
37290
37377
 
37291
- // There's no transform function in exports
37292
-
37293
37378
  // TODO: there's another, very similar getSinusoidCoefficients function
37294
37379
  // they should probably be merged
37295
37380
  function getSinusoidCoefficients(coords) {
@@ -42407,23 +42492,37 @@ const getPromptJSON$6 = (renderProps, userInput) => {
42407
42492
  };
42408
42493
  };
42409
42494
 
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];
42495
+ /**
42496
+ * Checks user input is within the allowed range and not the same as the initial
42497
+ * state.
42498
+ * @param userInput
42499
+ * @see 'scoreNumberLine' for the scoring logic.
42500
+ */
42501
+ function validateNumberLine(userInput) {
42502
+ const divisionRange = userInput.divisionRange;
42503
+ const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
42418
42504
 
42419
42505
  // TODO: I don't think isTickCrtl is a thing anymore
42420
- if (state.isTickCrtl && outsideAllowedRange) {
42506
+ if (userInput.isTickCrtl && outsideAllowedRange) {
42421
42507
  return {
42422
42508
  type: "invalid",
42423
42509
  message: "Number of divisions is outside the allowed range."
42424
42510
  };
42425
42511
  }
42426
- if (correctPos && correctRel === state.rel) {
42512
+ return null;
42513
+ }
42514
+
42515
+ function scoreNumberLine(userInput, scoringData) {
42516
+ const validationError = validateNumberLine(userInput);
42517
+ if (validationError) {
42518
+ return validationError;
42519
+ }
42520
+ const range = scoringData.range;
42521
+ const start = scoringData.initialX != null ? scoringData.initialX : range[0];
42522
+ const startRel = scoringData.isInequality ? "ge" : "eq";
42523
+ const correctRel = scoringData.correctRel || "eq";
42524
+ const correctPos = kmath.number.equal(userInput.numLinePosition, scoringData.correctX || 0);
42525
+ if (correctPos && correctRel === userInput.rel) {
42427
42526
  return {
42428
42527
  type: "points",
42429
42528
  earned: 1,
@@ -42431,7 +42530,7 @@ function scoreNumberLine(state, rubric) {
42431
42530
  message: null
42432
42531
  };
42433
42532
  }
42434
- if (state.numLinePosition === start && state.rel === startRel) {
42533
+ if (userInput.numLinePosition === start && userInput.rel === startRel) {
42435
42534
  // We're where we started.
42436
42535
  return {
42437
42536
  type: "invalid",
@@ -45380,7 +45479,7 @@ const init = function () {
45380
45479
 
45381
45480
  // This file is processed by a Rollup plugin (replace) to inject the production
45382
45481
  const libName = "@khanacademy/perseus";
45383
- const libVersion = "45.0.0";
45482
+ const libVersion = "45.1.0";
45384
45483
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45385
45484
 
45386
45485
  const apiVersion = {
@@ -45968,14 +46067,10 @@ class ServerItemRenderer extends React__namespace.Component {
45968
46067
  });
45969
46068
  }
45970
46069
  componentDidUpdate(prevProps, prevState) {
45971
- if (this.props.apiOptions.answerableCallback) {
46070
+ const answerableCallback = this.props.apiOptions.answerableCallback;
46071
+ if (answerableCallback != null) {
45972
46072
  const isAnswerable = this.questionRenderer.emptyWidgets().length === 0;
45973
- const {
45974
- answerableCallback
45975
- } = this.props.apiOptions;
45976
- if (answerableCallback) {
45977
- answerableCallback(isAnswerable);
45978
- }
46073
+ answerableCallback(isAnswerable);
45979
46074
  }
45980
46075
  if (!this._fullyRendered) {
45981
46076
  const assetsLoaded = Object.values(this.state.assetStatuses).every(Boolean);
@@ -46346,7 +46441,7 @@ const traverseRenderer$1 = function (rendererOptions, contentCallback, deepWidge
46346
46441
  newContent = modifiedContent;
46347
46442
  }
46348
46443
  }
46349
- const newWidgets = objective_.mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
46444
+ const newWidgets = mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
46350
46445
  // Widgets without info or a type are empty widgets, and
46351
46446
  // should always be renderable. It's also annoying to write
46352
46447
  // checks for this everywhere, so we just filter them out once and
@@ -48956,7 +49051,7 @@ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
48956
49051
  const parseDefinitionWidget = parseWidget(constant("definition"), object({
48957
49052
  togglePrompt: string,
48958
49053
  definition: string,
48959
- static: boolean
49054
+ static: defaulted(boolean, () => false)
48960
49055
  }));
48961
49056
 
48962
49057
  const parseDropdownWidget = parseWidget(constant("dropdown"), object({
@@ -49159,15 +49254,26 @@ const parseIframeWidget = parseWidget(constant("iframe"), object({
49159
49254
  static: defaulted(boolean, () => false)
49160
49255
  }));
49161
49256
 
49257
+ const stringToNumber = (rawValue, ctx) => {
49258
+ if (typeof rawValue === "number") {
49259
+ return ctx.success(rawValue);
49260
+ }
49261
+ const parsedNumber = +rawValue;
49262
+ if (rawValue === "" || isNaN(parsedNumber)) {
49263
+ return ctx.failure("a number or numeric string", rawValue);
49264
+ }
49265
+ return ctx.success(parsedNumber);
49266
+ };
49267
+
49268
+ const numericToNumber = pipeParsers(union(number).or(string).parser).then(stringToNumber).parser;
49162
49269
  const parsePerseusImageBackground = object({
49163
49270
  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)
49271
+ width: optional(numericToNumber),
49272
+ height: optional(numericToNumber),
49273
+ top: optional(numericToNumber),
49274
+ left: optional(numericToNumber),
49275
+ bottom: optional(numericToNumber),
49276
+ scale: optional(numericToNumber)
49171
49277
  });
49172
49278
 
49173
49279
  const pairOfNumbers$2 = pair(number, number);
@@ -49186,6 +49292,12 @@ const parseImageWidget = parseWidget(constant("image"), object({
49186
49292
  box: optional(pairOfNumbers$2)
49187
49293
  }));
49188
49294
 
49295
+ const booleanToString = (rawValue, ctx) => {
49296
+ if (typeof rawValue === "boolean") {
49297
+ return ctx.success(String(rawValue));
49298
+ }
49299
+ return ctx.failure("boolean", rawValue);
49300
+ };
49189
49301
  const parseInputNumberWidget = parseWidget(constant("input-number"), object({
49190
49302
  answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
49191
49303
  inexact: optional(boolean),
@@ -49193,7 +49305,11 @@ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
49193
49305
  rightAlign: optional(boolean),
49194
49306
  simplify: enumeration("required", "optional", "enforced"),
49195
49307
  size: enumeration("normal", "small"),
49196
- value: union(number).or(string).parser,
49308
+ // TODO(benchristel): there are some content items where value is a
49309
+ // boolean, even though that makes no sense. We should figure out if
49310
+ // those content items are actually published anywhere, and consider
49311
+ // updating them.
49312
+ value: union(number).or(string).or(booleanToString).parser,
49197
49313
  customKeypad: optional(boolean)
49198
49314
  }));
49199
49315
 
@@ -49491,18 +49607,27 @@ const parseLockedFunctionType = object({
49491
49607
  const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
49492
49608
  const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
49493
49609
  step: pairOfNumbers,
49494
- gridStep: pairOfNumbers,
49495
- snapStep: pairOfNumbers,
49610
+ // TODO(benchristel): rather than making gridStep and snapStep
49611
+ // optional, we should duplicate the defaulting logic from the
49612
+ // InteractiveGraph component. See parse-perseus-json/README.md for
49613
+ // why.
49614
+ gridStep: optional(pairOfNumbers),
49615
+ snapStep: optional(pairOfNumbers),
49496
49616
  backgroundImage: optional(parsePerseusImageBackground),
49497
49617
  markings: enumeration("graph", "grid", "none"),
49498
- labels: array(string),
49618
+ labels: optional(array(string)),
49499
49619
  showProtractor: boolean,
49500
49620
  showRuler: optional(boolean),
49501
49621
  showTooltips: optional(boolean),
49502
49622
  rulerLabel: optional(string),
49503
49623
  rulerTicks: optional(number),
49504
49624
  range: pair(pairOfNumbers, pairOfNumbers),
49505
- graph: parsePerseusGraphType,
49625
+ // NOTE(benchristel): I copied the default graph from
49626
+ // interactive-graph.tsx. See the parse-perseus-json/README.md for
49627
+ // an explanation of why we want to duplicate the default here.
49628
+ graph: defaulted(parsePerseusGraphType, () => ({
49629
+ type: "linear"
49630
+ })),
49506
49631
  correct: parsePerseusGraphType,
49507
49632
  // TODO(benchristel): default lockedFigures to empty array
49508
49633
  lockedFigures: optional(array(parseLockedFigure)),
@@ -49570,7 +49695,10 @@ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
49570
49695
  isTickCtrl: optional(nullable(boolean)),
49571
49696
  divisionRange: array(number),
49572
49697
  numDivisions: optional(nullable(number)),
49573
- snapDivisions: number,
49698
+ // NOTE(benchristel): I copied the default snapDivisions from
49699
+ // number-line.tsx. See the parse-perseus-json/README.md for
49700
+ // an explanation of why we want to duplicate the default here.
49701
+ snapDivisions: defaulted(number, () => 2),
49574
49702
  tickStep: optional(nullable(number)),
49575
49703
  correctRel: optional(nullable(string)),
49576
49704
  correctX: number,
@@ -49583,18 +49711,18 @@ const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "d
49583
49711
  const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
49584
49712
  answers: array(object({
49585
49713
  message: string,
49586
- value: number,
49714
+ value: optional(number),
49587
49715
  status: string,
49588
49716
  answerForms: optional(array(parseMathFormat)),
49589
49717
  strict: boolean,
49590
49718
  maxError: optional(nullable(number)),
49591
49719
  simplify: optional(nullable(string))
49592
49720
  })),
49593
- labelText: string,
49721
+ labelText: optional(string),
49594
49722
  size: string,
49595
49723
  coefficient: boolean,
49596
49724
  rightAlign: optional(boolean),
49597
- static: boolean,
49725
+ static: defaulted(boolean, () => false),
49598
49726
  answerForms: optional(array(object({
49599
49727
  name: parseMathFormat,
49600
49728
  simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
@@ -49607,12 +49735,18 @@ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
49607
49735
  function parseRenderer(rawValue, ctx) {
49608
49736
  return parsePerseusRenderer(rawValue, ctx);
49609
49737
  }
49738
+ const largeToAuto = (height, ctx) => {
49739
+ if (height === "large") {
49740
+ return ctx.success("auto");
49741
+ }
49742
+ return ctx.success(height);
49743
+ };
49610
49744
  const parseOrdererWidget = parseWidget(constant("orderer"), object({
49611
- options: array(parseRenderer),
49745
+ options: defaulted(array(parseRenderer), () => []),
49612
49746
  correctOptions: array(parseRenderer),
49613
49747
  otherOptions: array(parseRenderer),
49614
- height: enumeration("normal", "auto"),
49615
- layout: enumeration("horizontal", "vertical")
49748
+ height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
49749
+ layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
49616
49750
  }));
49617
49751
 
49618
49752
  const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
@@ -49626,7 +49760,7 @@ const parsePassageWidget = parseWidget(constant("passage"), object({
49626
49760
  passageText: string,
49627
49761
  passageTitle: string,
49628
49762
  showLineNumbers: boolean,
49629
- static: boolean
49763
+ static: defaulted(boolean, () => false)
49630
49764
  }));
49631
49765
 
49632
49766
  const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
@@ -49647,7 +49781,10 @@ const parsePlotterWidget = parseWidget(constant("plotter"), object({
49647
49781
  picUrl: optional(nullable(string)),
49648
49782
  picSize: optional(nullable(number)),
49649
49783
  picBoxHeight: optional(nullable(number)),
49650
- plotDimensions: array(number)
49784
+ // NOTE(benchristel): I copied the default plotDimensions from
49785
+ // plotter.tsx. See the parse-perseus-json/README.md for an explanation
49786
+ // of why we want to duplicate the defaults here.
49787
+ plotDimensions: defaulted(array(number), () => [380, 300])
49651
49788
  }));
49652
49789
 
49653
49790
  const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
@@ -49800,6 +49937,10 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
49800
49937
  return parseAndAssign(`table ${id}`, parseTableWidget);
49801
49938
  case "video":
49802
49939
  return parseAndAssign(`video ${id}`, parseVideoWidget);
49940
+ case "sequence":
49941
+ // sequence is a deprecated widget type, and the corresponding
49942
+ // widget component no longer exists.
49943
+ return parseAndAssign(`sequence ${id}`, parseDeprecatedWidget);
49803
49944
  default:
49804
49945
  if (getWidget(type)) {
49805
49946
  // @ts-expect-error - 'type' is not a valid widget type
@@ -49808,6 +49949,11 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
49808
49949
  return ctx.failure("a valid widget type", type);
49809
49950
  }
49810
49951
  };
49952
+ const parseDeprecatedWidget = parseWidget(
49953
+ // Ignore the incoming widget type and hardcode "deprecated-standin"
49954
+ (_, ctx) => ctx.success("deprecated-standin"),
49955
+ // Allow any widget options
49956
+ object({}));
49811
49957
  const parseStringToPositiveInt = (rawValue, ctx) => {
49812
49958
  if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
49813
49959
  return ctx.failure("numeric string", rawValue);