@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/es/index.js CHANGED
@@ -3797,6 +3797,7 @@ Zoom.prototype.zoomImage = function () {
3797
3797
  this._zoomOriginal();
3798
3798
  }.bind(this);
3799
3799
  img.src = this._targetImage.src;
3800
+ img.alt = this._targetImage.alt;
3800
3801
  this.$zoomedImage = $zoomedImage;
3801
3802
  };
3802
3803
  Zoom.prototype._zoomOriginal = function () {
@@ -8872,7 +8873,10 @@ var Tex$1 = {
8872
8873
  // We use createElement instead of JSX here because we can't name this file tex.tsx;
8873
8874
  // that name is already taken.
8874
8875
  // eslint-disable-next-line import/no-deprecated
8875
- render( /*#__PURE__*/React.createElement(TeX, {
8876
+ render(
8877
+ /*#__PURE__*/
8878
+ // eslint-disable-next-line react/no-children-prop
8879
+ React.createElement(TeX, {
8876
8880
  children: text,
8877
8881
  onRender: callback
8878
8882
  }), $texHolder[0]);
@@ -14609,7 +14613,7 @@ const pluck = function pluck(table, subKey) {
14609
14613
  * });
14610
14614
  * {a: 2, b: 3}
14611
14615
  */
14612
- const mapObject$2 = function mapObject(obj, lambda) {
14616
+ const mapObject = function mapObject(obj, lambda) {
14613
14617
  const result = {};
14614
14618
  Object.keys(obj).forEach(key => {
14615
14619
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
@@ -14618,27 +14622,6 @@ const mapObject$2 = function mapObject(obj, lambda) {
14618
14622
  return result;
14619
14623
  };
14620
14624
 
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
14625
  /**
14643
14626
  * MovableLine
14644
14627
  */
@@ -14703,7 +14686,7 @@ _.extend(MovableLine$4.prototype, {
14703
14686
  // - are objects, not primitives (and need a deeper copy)
14704
14687
  // - they don't need getters created for them
14705
14688
  // TODO(jack): Consider "default" once we es3ify perseus
14706
- objective_.pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
14689
+ pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
14707
14690
  },
14708
14691
  /**
14709
14692
  * Resets the object to its state as if it were constructed with
@@ -15203,7 +15186,7 @@ class MovablePoint$5 {
15203
15186
  // - are objects, not primitives (and need a deeper copy)
15204
15187
  // - they don't need getters created for them
15205
15188
  // TODO(jack): Consider "default" once we es3ify perseus
15206
- objective_.pluck(MovablePointOptions, "standard")
15189
+ pluck(MovablePointOptions, "standard")
15207
15190
  // We only update props here, because we want things on state to
15208
15191
  // be persistent, and updated appropriately in modify()
15209
15192
  ), DEFAULT_PROPS$1);
@@ -16094,7 +16077,7 @@ _.extend(MovablePolygon.prototype, {
16094
16077
  // - are objects, not primitives (and need a deeper copy)
16095
16078
  // - they don't need getters created for them
16096
16079
  // TODO(jack): Consider "default" once we es3ify perseus
16097
- objective_.pluck(MovablePolygonOptions, "standard")
16080
+ pluck(MovablePolygonOptions, "standard")
16098
16081
 
16099
16082
  // We only update props here, because we want things on state to
16100
16083
  // be persistent, and updated appropriately in modify()
@@ -19284,12 +19267,9 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19284
19267
  isLintable: isLintable
19285
19268
  });
19286
19269
 
19287
- const {
19288
- mapObject: mapObject$1
19289
- } = objective_;
19290
19270
  function getUpgradedWidgetOptions(oldWidgetOptions) {
19291
19271
  // @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) => {
19272
+ return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
19293
19273
  if (!widgetInfo.type || !widgetInfo.alignment) {
19294
19274
  const newValues = {};
19295
19275
  if (!widgetInfo.type) {
@@ -19317,15 +19297,8 @@ widgetIds, userInputMap, strings, locale) {
19317
19297
  // Static widgets shouldn't count as empty
19318
19298
  return false;
19319
19299
  }
19320
- let score = null;
19321
- const userInput = userInputMap[id];
19322
19300
  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
- }
19301
+ const score = scorer == null ? void 0 : scorer(userInputMap[id], widget.options, strings, locale);
19329
19302
  if (score) {
19330
19303
  return Util.scoreIsEmpty(score);
19331
19304
  }
@@ -19365,11 +19338,9 @@ widgetIds, userInputMap, strings, locale) {
19365
19338
  }
19366
19339
  const userInput = userInputMap[id];
19367
19340
  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);
19341
+ const score = scorer == null ? void 0 : scorer(userInput, widget.options, strings, locale);
19342
+ if (score != null) {
19343
+ widgetScores[id] = score;
19373
19344
  }
19374
19345
  });
19375
19346
  return widgetScores;
@@ -19675,9 +19646,6 @@ WidgetContainer.defaultProps = {
19675
19646
  linterContext: linterContextDefault
19676
19647
  };
19677
19648
 
19678
- const {
19679
- mapObject
19680
- } = objective_;
19681
19649
  const rContainsNonWhitespace = /\S/;
19682
19650
  const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
19683
19651
  const noopOnRender = () => {};
@@ -20603,7 +20571,7 @@ class Renderer extends React.Component {
20603
20571
  }, 0);
20604
20572
  });
20605
20573
  };
20606
- this.setInputValue = (path, newValue, focus) => {
20574
+ this.setInputValue = (path, newValue, cb) => {
20607
20575
  // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
20608
20576
  const widgetId = _.first(path);
20609
20577
  // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
@@ -20611,7 +20579,7 @@ class Renderer extends React.Component {
20611
20579
  const widget = this.getWidgetInstance(widgetId);
20612
20580
 
20613
20581
  // Widget handles parsing of the interWidgetPath.
20614
- widget == null || widget.setInputValue == null || widget.setInputValue(interWidgetPath, newValue, focus);
20582
+ widget == null || widget.setInputValue == null || widget.setInputValue(interWidgetPath, newValue, cb);
20615
20583
  };
20616
20584
  /**
20617
20585
  * Returns an array of all widget IDs in the order they occur in
@@ -20832,6 +20800,12 @@ class Renderer extends React.Component {
20832
20800
  isLastUsedWidget: widgetId === this.state.lastUsedWidgetId
20833
20801
  });
20834
20802
  }
20803
+ /**
20804
+ * Returns an array of widget ids that are empty (meaning widgets where the
20805
+ * learner has not interacted with the widget yet or has not filled in all
20806
+ * fields). For example, the `interactive-graph` widget is considered
20807
+ * empty if the graph is in the starting state.
20808
+ */
20835
20809
  emptyWidgets() {
20836
20810
  return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20837
20811
  }
@@ -21096,6 +21070,7 @@ class TextInput extends React.Component {
21096
21070
  value: formattedValue,
21097
21071
  type: "text",
21098
21072
  "aria-label": labelText,
21073
+ "aria-describedby": this.props["aria-describedby"],
21099
21074
  onChange: value => this.props.onChange(value),
21100
21075
  placeholder: placeholder,
21101
21076
  onFocus: onFocus,
@@ -21362,6 +21337,7 @@ class Tooltip extends React.Component {
21362
21337
  }, /*#__PURE__*/React.createElement("div", {
21363
21338
  ref: this.tooltipContainerRef,
21364
21339
  className: "tooltipContainer",
21340
+ role: "tooltip",
21365
21341
  style: {
21366
21342
  position: "absolute",
21367
21343
  // height must start out undefined, not null, so that
@@ -21442,9 +21418,15 @@ class InputWithExamples extends React.Component {
21442
21418
  };
21443
21419
  this._renderInput = () => {
21444
21420
  const id = this._getUniqueId();
21421
+ const ariaId = `aria-for-${id}`;
21422
+ // Generate text from a known set of format options that will read well in a screen reader
21423
+ const examplesAria = this.props.examples.length === 7 ? "" : `${this.props.examples[0]}
21424
+ ${this.props.examples.slice(1).join(", or\n")}`
21425
+ // @ts-expect-error TS2550: Property replaceAll does not exist on type string.
21426
+ .replaceAll("*", "").replaceAll("$", "").replaceAll("\\ \\text{pi}", " pi").replaceAll("\\ ", " and ");
21445
21427
  const inputProps = {
21446
21428
  id: id,
21447
- "aria-describedby": id,
21429
+ "aria-describedby": ariaId,
21448
21430
  ref: "input",
21449
21431
  className: this._getInputClassName(),
21450
21432
  labelText: this.props.labelText,
@@ -21460,7 +21442,12 @@ class InputWithExamples extends React.Component {
21460
21442
  autoCorrect: "off",
21461
21443
  spellCheck: "false"
21462
21444
  };
21463
- return /*#__PURE__*/React.createElement(TextInput, inputProps);
21445
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(TextInput, inputProps), /*#__PURE__*/React.createElement("span", {
21446
+ id: ariaId,
21447
+ style: {
21448
+ display: "none"
21449
+ }
21450
+ }, examplesAria));
21464
21451
  };
21465
21452
  this._handleFocus = () => {
21466
21453
  this.props.onFocus();
@@ -21502,8 +21489,12 @@ class InputWithExamples extends React.Component {
21502
21489
  }
21503
21490
  render() {
21504
21491
  const input = this._renderInput();
21505
- const examplesContent = _.map(this.props.examples, example => {
21506
- return "- " + example;
21492
+ const examplesContent = this.props.examples.length <= 2 ? this.props.examples.join(" ") // A single item (with or without leading text) is not a "list"
21493
+ : this.props.examples // 2 or more items should display as a list
21494
+ .map((example, index) => {
21495
+ // If the first example is bold, then it is most likely a heading/leading text.
21496
+ // So, it shouldn't be part of the list.
21497
+ return index === 0 && example.startsWith("**") ? `${example}\n` : `- ${example}`;
21507
21498
  }).join("\n");
21508
21499
  const showExamples = this.props.shouldShowExamples && this.state.showExamples;
21509
21500
  return /*#__PURE__*/React.createElement(Tooltip, {
@@ -22177,7 +22168,7 @@ function scoreNumericInput(userInput, rubric, strings) {
22177
22168
  // We may have received TeX; try to parse it before grading.
22178
22169
  // If `currentValue` is not TeX, this should be a no-op.
22179
22170
  const currentValue = ParseTex(userInput.currentValue);
22180
- const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => Math.abs(answer.value) <= 1);
22171
+ const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value !== undefined && Math.abs(answer.value) <= 1);
22181
22172
 
22182
22173
  // The coefficient is an attribute of the widget
22183
22174
  let localValue = currentValue;
@@ -22238,7 +22229,8 @@ class NumericInput extends React.Component {
22238
22229
  this.state = {
22239
22230
  // keeps track of the other set of values when switching
22240
22231
  // between 0 and finite solutions
22241
- previousValues: [""]
22232
+ previousValues: [""],
22233
+ isFocused: false
22242
22234
  };
22243
22235
  // TODO(Nicole, Jeremy): This is maybe never used and should be removed
22244
22236
  this.examples = () => {
@@ -22301,9 +22293,19 @@ class NumericInput extends React.Component {
22301
22293
  };
22302
22294
  this._handleFocus = () => {
22303
22295
  this.props.onFocus([]);
22296
+ this.setState(currentState => {
22297
+ return _extends({}, currentState, {
22298
+ isFocused: true
22299
+ });
22300
+ });
22304
22301
  };
22305
22302
  this._handleBlur = () => {
22306
22303
  this.props.onBlur([]);
22304
+ this.setState(currentState => {
22305
+ return _extends({}, currentState, {
22306
+ isFocused: false
22307
+ });
22308
+ });
22307
22309
  };
22308
22310
  }
22309
22311
  static getUserInputFromProps(props) {
@@ -22346,13 +22348,20 @@ class NumericInput extends React.Component {
22346
22348
  // component.
22347
22349
  const styles = StyleSheet.create({
22348
22350
  input: {
22351
+ borderRadius: "3px",
22352
+ borderWidth: this.state.isFocused ? "2px" : "1px",
22353
+ display: "inline-block",
22354
+ fontFamily: `Symbola, "Times New Roman", serif`,
22355
+ fontSize: "18px",
22356
+ height: "32px",
22357
+ lineHeight: "18px",
22358
+ padding: this.state.isFocused ? "4px" : "4px 5px",
22359
+ // account for added focus border thickness
22349
22360
  textAlign: this.props.rightAlign ? "right" : "left",
22350
- width: this.props.size === "small" ? 40 : 80,
22351
- padding: 0,
22352
- height: "auto"
22361
+ width: this.props.size === "small" ? 40 : 80
22353
22362
  }
22354
22363
  });
22355
- return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(InputWithExamples, {
22364
+ return /*#__PURE__*/React.createElement(InputWithExamples, {
22356
22365
  ref: ref => this.inputRef = ref,
22357
22366
  value: this.props.currentValue,
22358
22367
  onChange: this.handleChange,
@@ -22364,7 +22373,7 @@ class NumericInput extends React.Component {
22364
22373
  id: this.props.widgetId,
22365
22374
  disabled: this.props.apiOptions.readOnly,
22366
22375
  style: styles.input
22367
- }));
22376
+ });
22368
22377
  }
22369
22378
  }
22370
22379
 
@@ -27260,8 +27269,8 @@ var Definition$1 = {
27260
27269
  scorer: () => scoreNoop()
27261
27270
  };
27262
27271
 
27263
- // The props are type `any` on purpose so that this can receive props
27264
- // from any deprecated widget
27272
+ // There are no required props, but this component can receive props from any
27273
+ // deprecated widget
27265
27274
  class DeprecatedStandin extends React__default.Component {
27266
27275
  constructor(...args) {
27267
27276
  super(...args);
@@ -29371,6 +29380,13 @@ const getPromptJSON$f = rendererJSON => {
29371
29380
  });
29372
29381
  };
29373
29382
 
29383
+ // The `group` widget is basically a widget hosting a full Perseus system in
29384
+ // it. As such, scoring a group means scoring all widgets it contains.
29385
+ function scoreGroup(userInput, options, strings, locale) {
29386
+ const scores = scoreWidgetsFunctional(options.widgets, Object.keys(options.widgets), userInput, strings, locale);
29387
+ return Util.flattenScores(scores);
29388
+ }
29389
+
29374
29390
  // exports has no 'transform'
29375
29391
 
29376
29392
  class Group extends React.Component {
@@ -29525,6 +29541,9 @@ var Group$1 = {
29525
29541
  displayName: "Group (SAT only)",
29526
29542
  widget: Group,
29527
29543
  traverseChildWidgets: traverseChildWidgets,
29544
+ // TODO(LEMS-2656): remove TS suppression
29545
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
29546
+ scorer: scoreGroup,
29528
29547
  hidden: true,
29529
29548
  isLintable: true
29530
29549
  };
@@ -32234,6 +32253,61 @@ const LegacyGrid = ({
32234
32253
 
32235
32254
  // Range is replaced within this file with Interval, but it is used elsewhere
32236
32255
  // and exported from the package, so we need to keep it around.
32256
+ /**
32257
+ * Our core set of Perseus widgets.
32258
+ *
32259
+ * This interface is the basis for "registering" all Perseus widget types.
32260
+ * There should be one key/value pair for each supported widget. If you create
32261
+ * a new widget, an entry should be added to this interface. Note that this
32262
+ * only registers the widget options type, you'll also need to register the
32263
+ * widget so that it's available at runtime (@see
32264
+ * {@link file://./widgets.ts#registerWidget}).
32265
+ *
32266
+ * Importantly, the key should be the name that is used in widget IDs. For most
32267
+ * widgets that is the same as the widget option's `type` field. In cases where
32268
+ * a widget has been deprecated and replaced with the deprecated-standin
32269
+ * widget, it should be the original widget type!
32270
+ *
32271
+ * If you define the widget outside of this package, you can still add the new
32272
+ * widget to this interface by writing the following in that package that
32273
+ * contains the widget. TypeScript will merge that definition of the
32274
+ * `PerseusWidgets` with the one defined below.
32275
+ *
32276
+ * ```typescript
32277
+ * declare module "@khanacademy/perseus" {
32278
+ * interface PerseusWidgetTypes {
32279
+ * // A new widget
32280
+ * "new-awesomeness": MyAwesomeNewWidget;
32281
+ *
32282
+ * // A deprecated widget
32283
+ * "super-old-widget": AutoCorrectWidget;
32284
+ * }
32285
+ * }
32286
+ *
32287
+ * // The new widget's options definition
32288
+ * type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>;
32289
+ *
32290
+ * // The deprecated widget's options definition
32291
+ * type SuperOldWidget = WidgetOptions<'super-old-widget', object>;
32292
+ * ```
32293
+ *
32294
+ * This interface can be extended through the magic of TypeScript "Declaration
32295
+ * merging". Specifically, we augment this module and extend this interface.
32296
+ *
32297
+ * @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
32298
+ */
32299
+ /**
32300
+ * A map of widget IDs to widget options. This is most often used as the type
32301
+ * for a set of widgets defined in a `PerseusItem` but can also be useful to
32302
+ * represent a function parameter where only `widgets` from a `PerseusItem` are
32303
+ * needed. Today Widget IDs are made up of the widget type and an incrementing
32304
+ * integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid
32305
+ * reading/parsing the widget id to derive any information from it, except in
32306
+ * the case of this map.
32307
+ *
32308
+ * @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
32309
+ * by augmenting the PerseusWidgetTypes with new widget types!
32310
+ */
32237
32311
  /**
32238
32312
  * A "PerseusItem" is a classic Perseus item. It is rendered by the
32239
32313
  * `ServerItemRenderer` and the layout is pre-set.
@@ -32249,6 +32323,9 @@ const LegacyGrid = ({
32249
32323
  * A "MultiItem" is an advanced Perseus item. It is rendered by the
32250
32324
  * `MultiRenderer` and you can control the layout of individual parts of the
32251
32325
  * item.
32326
+ *
32327
+ * @deprecated MultiItem support is slated for removal in a future Perseus
32328
+ * release.
32252
32329
  */
32253
32330
  const ItemExtras = [
32254
32331
  // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
@@ -32270,6 +32347,11 @@ const ItemExtras = [
32270
32347
  // The user might benefit from using a statistics Z Table like https://www.ztable.net/
32271
32348
  "zTable"];
32272
32349
 
32350
+ /**
32351
+ * The type representing the common structure of all widget's options. The
32352
+ * `Options` generic type represents the widget-specific option data.
32353
+ */
32354
+
32273
32355
  // prettier-ignore
32274
32356
 
32275
32357
  // prettier-ignore
@@ -32338,7 +32420,9 @@ const ItemExtras = [
32338
32420
 
32339
32421
  //prettier-ignore
32340
32422
 
32341
- // A background image applied to various widgets.
32423
+ /**
32424
+ * A background image applied to various widgets.
32425
+ */
32342
32426
 
32343
32427
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
32344
32428
 
@@ -36906,8 +36990,6 @@ const makeInvalidTypeError = (functionName, graphType) => {
36906
36990
  });
36907
36991
  };
36908
36992
 
36909
- // There's no transform function in exports
36910
-
36911
36993
  // TODO: there's another, very similar getSinusoidCoefficients function
36912
36994
  // they should probably be merged
36913
36995
  function getSinusoidCoefficients(coords) {
@@ -42043,23 +42125,37 @@ const getPromptJSON$6 = (renderProps, userInput) => {
42043
42125
  };
42044
42126
  };
42045
42127
 
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];
42128
+ /**
42129
+ * Checks user input is within the allowed range and not the same as the initial
42130
+ * state.
42131
+ * @param userInput
42132
+ * @see 'scoreNumberLine' for the scoring logic.
42133
+ */
42134
+ function validateNumberLine(userInput) {
42135
+ const divisionRange = userInput.divisionRange;
42136
+ const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
42054
42137
 
42055
42138
  // TODO: I don't think isTickCrtl is a thing anymore
42056
- if (state.isTickCrtl && outsideAllowedRange) {
42139
+ if (userInput.isTickCrtl && outsideAllowedRange) {
42057
42140
  return {
42058
42141
  type: "invalid",
42059
42142
  message: "Number of divisions is outside the allowed range."
42060
42143
  };
42061
42144
  }
42062
- if (correctPos && correctRel === state.rel) {
42145
+ return null;
42146
+ }
42147
+
42148
+ function scoreNumberLine(userInput, scoringData) {
42149
+ const validationError = validateNumberLine(userInput);
42150
+ if (validationError) {
42151
+ return validationError;
42152
+ }
42153
+ const range = scoringData.range;
42154
+ const start = scoringData.initialX != null ? scoringData.initialX : range[0];
42155
+ const startRel = scoringData.isInequality ? "ge" : "eq";
42156
+ const correctRel = scoringData.correctRel || "eq";
42157
+ const correctPos = number$1.equal(userInput.numLinePosition, scoringData.correctX || 0);
42158
+ if (correctPos && correctRel === userInput.rel) {
42063
42159
  return {
42064
42160
  type: "points",
42065
42161
  earned: 1,
@@ -42067,7 +42163,7 @@ function scoreNumberLine(state, rubric) {
42067
42163
  message: null
42068
42164
  };
42069
42165
  }
42070
- if (state.numLinePosition === start && state.rel === startRel) {
42166
+ if (userInput.numLinePosition === start && userInput.rel === startRel) {
42071
42167
  // We're where we started.
42072
42168
  return {
42073
42169
  type: "invalid",
@@ -45013,7 +45109,7 @@ const init = function init() {
45013
45109
 
45014
45110
  // This file is processed by a Rollup plugin (replace) to inject the production
45015
45111
  const libName = "@khanacademy/perseus";
45016
- const libVersion = "45.0.0";
45112
+ const libVersion = "45.1.0";
45017
45113
  addLibraryVersionToPerseusDebug(libName, libVersion);
45018
45114
 
45019
45115
  const apiVersion = {
@@ -45621,14 +45717,10 @@ class ServerItemRenderer extends React.Component {
45621
45717
  });
45622
45718
  }
45623
45719
  componentDidUpdate(prevProps, prevState) {
45624
- if (this.props.apiOptions.answerableCallback) {
45720
+ const answerableCallback = this.props.apiOptions.answerableCallback;
45721
+ if (answerableCallback != null) {
45625
45722
  const isAnswerable = this.questionRenderer.emptyWidgets().length === 0;
45626
- const {
45627
- answerableCallback
45628
- } = this.props.apiOptions;
45629
- if (answerableCallback) {
45630
- answerableCallback(isAnswerable);
45631
- }
45723
+ answerableCallback(isAnswerable);
45632
45724
  }
45633
45725
  if (!this._fullyRendered) {
45634
45726
  const assetsLoaded = Object.values(this.state.assetStatuses).every(Boolean);
@@ -45971,7 +46063,7 @@ const traverseRenderer$1 = function traverseRenderer(rendererOptions, contentCal
45971
46063
  newContent = modifiedContent;
45972
46064
  }
45973
46065
  }
45974
- const newWidgets = objective_.mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
46066
+ const newWidgets = mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
45975
46067
  // Widgets without info or a type are empty widgets, and
45976
46068
  // should always be renderable. It's also annoying to write
45977
46069
  // checks for this everywhere, so we just filter them out once and
@@ -48570,7 +48662,7 @@ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
48570
48662
  const parseDefinitionWidget = parseWidget(constant("definition"), object({
48571
48663
  togglePrompt: string,
48572
48664
  definition: string,
48573
- static: boolean
48665
+ static: defaulted(boolean, () => false)
48574
48666
  }));
48575
48667
 
48576
48668
  const parseDropdownWidget = parseWidget(constant("dropdown"), object({
@@ -48772,15 +48864,26 @@ const parseIframeWidget = parseWidget(constant("iframe"), object({
48772
48864
  static: defaulted(boolean, () => false)
48773
48865
  }));
48774
48866
 
48867
+ const stringToNumber = (rawValue, ctx) => {
48868
+ if (typeof rawValue === "number") {
48869
+ return ctx.success(rawValue);
48870
+ }
48871
+ const parsedNumber = +rawValue;
48872
+ if (rawValue === "" || isNaN(parsedNumber)) {
48873
+ return ctx.failure("a number or numeric string", rawValue);
48874
+ }
48875
+ return ctx.success(parsedNumber);
48876
+ };
48877
+
48878
+ const numericToNumber = pipeParsers(union(number).or(string).parser).then(stringToNumber).parser;
48775
48879
  const parsePerseusImageBackground = object({
48776
48880
  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)
48881
+ width: optional(numericToNumber),
48882
+ height: optional(numericToNumber),
48883
+ top: optional(numericToNumber),
48884
+ left: optional(numericToNumber),
48885
+ bottom: optional(numericToNumber),
48886
+ scale: optional(numericToNumber)
48784
48887
  });
48785
48888
 
48786
48889
  const pairOfNumbers$2 = pair(number, number);
@@ -48799,6 +48902,12 @@ const parseImageWidget = parseWidget(constant("image"), object({
48799
48902
  box: optional(pairOfNumbers$2)
48800
48903
  }));
48801
48904
 
48905
+ const booleanToString = (rawValue, ctx) => {
48906
+ if (typeof rawValue === "boolean") {
48907
+ return ctx.success(String(rawValue));
48908
+ }
48909
+ return ctx.failure("boolean", rawValue);
48910
+ };
48802
48911
  const parseInputNumberWidget = parseWidget(constant("input-number"), object({
48803
48912
  answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
48804
48913
  inexact: optional(boolean),
@@ -48806,7 +48915,11 @@ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
48806
48915
  rightAlign: optional(boolean),
48807
48916
  simplify: enumeration("required", "optional", "enforced"),
48808
48917
  size: enumeration("normal", "small"),
48809
- value: union(number).or(string).parser,
48918
+ // TODO(benchristel): there are some content items where value is a
48919
+ // boolean, even though that makes no sense. We should figure out if
48920
+ // those content items are actually published anywhere, and consider
48921
+ // updating them.
48922
+ value: union(number).or(string).or(booleanToString).parser,
48810
48923
  customKeypad: optional(boolean)
48811
48924
  }));
48812
48925
 
@@ -49104,18 +49217,27 @@ const parseLockedFunctionType = object({
49104
49217
  const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
49105
49218
  const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
49106
49219
  step: pairOfNumbers,
49107
- gridStep: pairOfNumbers,
49108
- snapStep: pairOfNumbers,
49220
+ // TODO(benchristel): rather than making gridStep and snapStep
49221
+ // optional, we should duplicate the defaulting logic from the
49222
+ // InteractiveGraph component. See parse-perseus-json/README.md for
49223
+ // why.
49224
+ gridStep: optional(pairOfNumbers),
49225
+ snapStep: optional(pairOfNumbers),
49109
49226
  backgroundImage: optional(parsePerseusImageBackground),
49110
49227
  markings: enumeration("graph", "grid", "none"),
49111
- labels: array(string),
49228
+ labels: optional(array(string)),
49112
49229
  showProtractor: boolean,
49113
49230
  showRuler: optional(boolean),
49114
49231
  showTooltips: optional(boolean),
49115
49232
  rulerLabel: optional(string),
49116
49233
  rulerTicks: optional(number),
49117
49234
  range: pair(pairOfNumbers, pairOfNumbers),
49118
- graph: parsePerseusGraphType,
49235
+ // NOTE(benchristel): I copied the default graph from
49236
+ // interactive-graph.tsx. See the parse-perseus-json/README.md for
49237
+ // an explanation of why we want to duplicate the default here.
49238
+ graph: defaulted(parsePerseusGraphType, () => ({
49239
+ type: "linear"
49240
+ })),
49119
49241
  correct: parsePerseusGraphType,
49120
49242
  // TODO(benchristel): default lockedFigures to empty array
49121
49243
  lockedFigures: optional(array(parseLockedFigure)),
@@ -49183,7 +49305,10 @@ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
49183
49305
  isTickCtrl: optional(nullable(boolean)),
49184
49306
  divisionRange: array(number),
49185
49307
  numDivisions: optional(nullable(number)),
49186
- snapDivisions: number,
49308
+ // NOTE(benchristel): I copied the default snapDivisions from
49309
+ // number-line.tsx. See the parse-perseus-json/README.md for
49310
+ // an explanation of why we want to duplicate the default here.
49311
+ snapDivisions: defaulted(number, () => 2),
49187
49312
  tickStep: optional(nullable(number)),
49188
49313
  correctRel: optional(nullable(string)),
49189
49314
  correctX: number,
@@ -49196,18 +49321,18 @@ const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "d
49196
49321
  const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
49197
49322
  answers: array(object({
49198
49323
  message: string,
49199
- value: number,
49324
+ value: optional(number),
49200
49325
  status: string,
49201
49326
  answerForms: optional(array(parseMathFormat)),
49202
49327
  strict: boolean,
49203
49328
  maxError: optional(nullable(number)),
49204
49329
  simplify: optional(nullable(string))
49205
49330
  })),
49206
- labelText: string,
49331
+ labelText: optional(string),
49207
49332
  size: string,
49208
49333
  coefficient: boolean,
49209
49334
  rightAlign: optional(boolean),
49210
- static: boolean,
49335
+ static: defaulted(boolean, () => false),
49211
49336
  answerForms: optional(array(object({
49212
49337
  name: parseMathFormat,
49213
49338
  simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
@@ -49220,12 +49345,18 @@ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
49220
49345
  function parseRenderer(rawValue, ctx) {
49221
49346
  return parsePerseusRenderer(rawValue, ctx);
49222
49347
  }
49348
+ const largeToAuto = (height, ctx) => {
49349
+ if (height === "large") {
49350
+ return ctx.success("auto");
49351
+ }
49352
+ return ctx.success(height);
49353
+ };
49223
49354
  const parseOrdererWidget = parseWidget(constant("orderer"), object({
49224
- options: array(parseRenderer),
49355
+ options: defaulted(array(parseRenderer), () => []),
49225
49356
  correctOptions: array(parseRenderer),
49226
49357
  otherOptions: array(parseRenderer),
49227
- height: enumeration("normal", "auto"),
49228
- layout: enumeration("horizontal", "vertical")
49358
+ height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
49359
+ layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
49229
49360
  }));
49230
49361
 
49231
49362
  const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
@@ -49239,7 +49370,7 @@ const parsePassageWidget = parseWidget(constant("passage"), object({
49239
49370
  passageText: string,
49240
49371
  passageTitle: string,
49241
49372
  showLineNumbers: boolean,
49242
- static: boolean
49373
+ static: defaulted(boolean, () => false)
49243
49374
  }));
49244
49375
 
49245
49376
  const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
@@ -49260,7 +49391,10 @@ const parsePlotterWidget = parseWidget(constant("plotter"), object({
49260
49391
  picUrl: optional(nullable(string)),
49261
49392
  picSize: optional(nullable(number)),
49262
49393
  picBoxHeight: optional(nullable(number)),
49263
- plotDimensions: array(number)
49394
+ // NOTE(benchristel): I copied the default plotDimensions from
49395
+ // plotter.tsx. See the parse-perseus-json/README.md for an explanation
49396
+ // of why we want to duplicate the defaults here.
49397
+ plotDimensions: defaulted(array(number), () => [380, 300])
49264
49398
  }));
49265
49399
 
49266
49400
  const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
@@ -49412,6 +49546,10 @@ const parseWidgetsMapEntry = ([key, widget], widgetMap, ctx) => {
49412
49546
  return parseAndAssign(`table ${id}`, parseTableWidget);
49413
49547
  case "video":
49414
49548
  return parseAndAssign(`video ${id}`, parseVideoWidget);
49549
+ case "sequence":
49550
+ // sequence is a deprecated widget type, and the corresponding
49551
+ // widget component no longer exists.
49552
+ return parseAndAssign(`sequence ${id}`, parseDeprecatedWidget);
49415
49553
  default:
49416
49554
  if (getWidget(type)) {
49417
49555
  // @ts-expect-error - 'type' is not a valid widget type
@@ -49420,6 +49558,11 @@ const parseWidgetsMapEntry = ([key, widget], widgetMap, ctx) => {
49420
49558
  return ctx.failure("a valid widget type", type);
49421
49559
  }
49422
49560
  };
49561
+ const parseDeprecatedWidget = parseWidget(
49562
+ // Ignore the incoming widget type and hardcode "deprecated-standin"
49563
+ (_, ctx) => ctx.success("deprecated-standin"),
49564
+ // Allow any widget options
49565
+ object({}));
49423
49566
  const parseStringToPositiveInt = (rawValue, ctx) => {
49424
49567
  if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
49425
49568
  return ctx.failure("numeric string", rawValue);