@khanacademy/perseus 44.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,36 +14613,15 @@ const pluck = function pluck(table, subKey) {
14609
14613
  * });
14610
14614
  * {a: 2, b: 3}
14611
14615
  */
14612
- const mapObject$1 = function mapObject(obj, lambda) {
14616
+ const mapObject = function mapObject(obj, lambda) {
14613
14617
  const result = {};
14614
- _.each(_.keys(obj), function (key) {
14618
+ Object.keys(obj).forEach(key => {
14615
14619
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
14616
14620
  result[key] = lambda(obj[key], key);
14617
14621
  });
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
- _.each(arr, function (elem) {
14632
- result[elem] = lambda(elem);
14633
- });
14634
- return result;
14635
- };
14636
- var objective_ = {
14637
- pluck: pluck,
14638
- mapObject: mapObject$1,
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()
@@ -18719,6 +18702,140 @@ class QuestionParagraph extends React.Component {
18719
18702
  }
18720
18703
  }
18721
18704
 
18705
+ // These are all the current widgets that require user-input and are supported for automatic scoring
18706
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
18707
+
18708
+ /**
18709
+ * Regex for widget placeholders in a string.
18710
+ *
18711
+ * First capture group is the widget ID (ex. 'radio 1')
18712
+ * Second capture group is the widget type (ex. "radio)
18713
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
18714
+ */
18715
+ function getWidgetRegex() {
18716
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
18717
+ }
18718
+
18719
+ /**
18720
+ * Add a widget placeholder using the widget ID.
18721
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
18722
+ *
18723
+ * @param {string} id
18724
+ * @returns {string}
18725
+ */
18726
+ function addWidget(id) {
18727
+ return `[[☃ ${id}]]`;
18728
+ }
18729
+
18730
+ /**
18731
+ * Get a widget type by a widget's ID
18732
+ * (please don't derive type from ID)
18733
+ *
18734
+ * @param {string} widgetId the ID of the widget
18735
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18736
+ * @returns {string} the widget type (ie "radio")
18737
+ */
18738
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
18739
+ var _widget$type;
18740
+ const widget = widgetMap[WidgetId];
18741
+ return (_widget$type = widget == null ? void 0 : widget.type) != null ? _widget$type : null;
18742
+ }
18743
+
18744
+ /**
18745
+ * Does the content have a specific type of widget?
18746
+ *
18747
+ * @param {string} type the type of the widget in question (ie "radio")
18748
+ * @param {string} content the string to search through
18749
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18750
+ * @returns {boolean} if the content includes the widget type
18751
+ */
18752
+ function contentHasWidgetType(type, content, widgetMap) {
18753
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
18754
+ }
18755
+
18756
+ /**
18757
+ * Extract all widget IDs, which includes the widget type and instance number.
18758
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
18759
+ *
18760
+ * Content should contain Perseus widget placeholders,
18761
+ * which look like: '[[☃ radio 1]]'.
18762
+ *
18763
+ * @param {string} content
18764
+ * @returns {ReadonlyArray<string>} widgetIds
18765
+ */
18766
+ function getWidgetIdsFromContent(content) {
18767
+ const widgets = [];
18768
+ const localWidgetRegex = getWidgetRegex();
18769
+ let match = localWidgetRegex.exec(content);
18770
+ while (match !== null) {
18771
+ widgets.push(match[1]);
18772
+ match = localWidgetRegex.exec(content);
18773
+ }
18774
+ return widgets;
18775
+ }
18776
+
18777
+ /**
18778
+ * Get a list of widget IDs from content,
18779
+ * but only for specific widget types
18780
+ *
18781
+ * @param {string} type the type of widget (ie "radio")
18782
+ * @param {string} content the string to parse
18783
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18784
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
18785
+ */
18786
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
18787
+ const rv = [];
18788
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
18789
+ widgetIdsInContent.forEach(widgetId => {
18790
+ const widget = widgetMap[widgetId];
18791
+ if ((widget == null ? void 0 : widget.type) === type) {
18792
+ rv.push(widgetId);
18793
+ }
18794
+ });
18795
+ return rv;
18796
+ }
18797
+
18798
+ /**
18799
+ * Pull the widget map out of ItemData
18800
+ *
18801
+ * @param {PerseusItem} PerseusItem containing a widgetMap
18802
+ * @returns {WidgetMap} the widget map in the PerseusItem
18803
+ */
18804
+ function getWidgetsMapFromItemData(itemData) {
18805
+ return itemData.question.widgets;
18806
+ }
18807
+
18808
+ /**
18809
+ * Get the widget information from a WidgetMap
18810
+ *
18811
+ * @param {string} widgetId the ID of the widget
18812
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
18813
+ */
18814
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
18815
+ var _widgetMap$widgetId;
18816
+ return (_widgetMap$widgetId = widgetMap[widgetId]) != null ? _widgetMap$widgetId : null;
18817
+ }
18818
+
18819
+ /**
18820
+ * Get select widgets from a widget map.
18821
+ * Useful for multi-items that needs to split
18822
+ * one PerseusItem into several
18823
+ *
18824
+ * @param {ReadonlyArray<string>} widgetIds to extract
18825
+ * @param {PerseusWidgetsMap} widgetMap to extract from
18826
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
18827
+ */
18828
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
18829
+ const widgets = {};
18830
+ widgetIds.forEach(widgetId => {
18831
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
18832
+ if (widget) {
18833
+ widgets[widgetId] = widget;
18834
+ }
18835
+ });
18836
+ return widgets;
18837
+ }
18838
+
18722
18839
  const DEFAULT_ALIGNMENT$1 = "block";
18723
18840
  // NOTE(kevinb): "default" is not one in `validAlignments`.
18724
18841
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -19150,36 +19267,64 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19150
19267
  isLintable: isLintable
19151
19268
  });
19152
19269
 
19270
+ function getUpgradedWidgetOptions(oldWidgetOptions) {
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; }'.
19272
+ return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
19273
+ if (!widgetInfo.type || !widgetInfo.alignment) {
19274
+ const newValues = {};
19275
+ if (!widgetInfo.type) {
19276
+ // TODO: why does widget have no type?
19277
+ // We don't want to derive type from widget ID
19278
+ // see: LEMS-1845
19279
+ newValues.type = widgetId.split(" ")[0];
19280
+ }
19281
+ if (!widgetInfo.alignment) {
19282
+ newValues.alignment = "default";
19283
+ }
19284
+ widgetInfo = _extends({}, widgetInfo, newValues);
19285
+ }
19286
+ return upgradeWidgetInfoToLatestVersion(widgetInfo);
19287
+ });
19288
+ }
19153
19289
  function emptyWidgetsFunctional(widgets,
19154
19290
  // This is a port of old code, I'm not sure why
19155
19291
  // we need widgetIds vs the keys of the widgets object
19156
19292
  widgetIds, userInputMap, strings, locale) {
19293
+ const upgradedWidgets = getUpgradedWidgetOptions(widgets);
19157
19294
  return widgetIds.filter(id => {
19158
- const widget = widgets[id];
19295
+ const widget = upgradedWidgets[id];
19159
19296
  if (!widget || widget.static) {
19160
19297
  // Static widgets shouldn't count as empty
19161
19298
  return false;
19162
19299
  }
19163
- let score = null;
19164
- const userInput = userInputMap[id];
19165
19300
  const scorer = getWidgetScorer(widget.type);
19166
- if (widget.type === "group") {
19167
- const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19168
- score = Util.flattenScores(scores);
19169
- } else if (scorer) {
19170
- score = scorer(userInput, widget.options, strings, locale);
19171
- }
19301
+ const score = scorer == null ? void 0 : scorer(userInputMap[id], widget.options, strings, locale);
19172
19302
  if (score) {
19173
19303
  return Util.scoreIsEmpty(score);
19174
19304
  }
19175
19305
  });
19176
19306
  }
19307
+
19308
+ // TODO: combine scorePerseusItem with scoreWidgetsFunctional
19309
+ // once scorePerseusItem is the only one calling scoreWidgetsFunctional
19310
+ function scorePerseusItem(perseusRenderData, userInputMap,
19311
+ // TODO(LEMS-2461,LEMS-2391): these probably
19312
+ // need to be removed before we move this to the server
19313
+ strings, locale) {
19314
+ // There seems to be a chance that PerseusRenderer.widgets might include
19315
+ // widget data for widgets that are not in PerseusRenderer.content,
19316
+ // so this checks that the widgets are being used before scoring them
19317
+ const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
19318
+ const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
19319
+ return Util.flattenScores(scores);
19320
+ }
19177
19321
  function scoreWidgetsFunctional(widgets,
19178
19322
  // This is a port of old code, I'm not sure why
19179
19323
  // we need widgetIds vs the keys of the widgets object
19180
19324
  widgetIds, userInputMap, strings, locale) {
19325
+ const upgradedWidgets = getUpgradedWidgetOptions(widgets);
19181
19326
  const gradedWidgetIds = widgetIds.filter(id => {
19182
- const props = widgets[id];
19327
+ const props = upgradedWidgets[id];
19183
19328
  const widgetIsGraded = (props == null ? void 0 : props.graded) == null || props.graded;
19184
19329
  const widgetIsStatic = !!(props != null && props.static);
19185
19330
  // Ungraded widgets or widgets set to static shouldn't be graded.
@@ -19187,17 +19332,15 @@ widgetIds, userInputMap, strings, locale) {
19187
19332
  });
19188
19333
  const widgetScores = {};
19189
19334
  gradedWidgetIds.forEach(id => {
19190
- const widget = widgets[id];
19335
+ const widget = upgradedWidgets[id];
19191
19336
  if (!widget) {
19192
19337
  return;
19193
19338
  }
19194
19339
  const userInput = userInputMap[id];
19195
19340
  const scorer = getWidgetScorer(widget.type);
19196
- if (widget.type === "group") {
19197
- const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19198
- widgetScores[id] = Util.flattenScores(scores);
19199
- } else if (scorer) {
19200
- 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;
19201
19344
  }
19202
19345
  });
19203
19346
  return widgetScores;
@@ -19503,9 +19646,6 @@ WidgetContainer.defaultProps = {
19503
19646
  linterContext: linterContextDefault
19504
19647
  };
19505
19648
 
19506
- const {
19507
- mapObject
19508
- } = objective_;
19509
19649
  const rContainsNonWhitespace = /\S/;
19510
19650
  const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
19511
19651
  const noopOnRender = () => {};
@@ -19556,31 +19696,12 @@ class Renderer extends React.Component {
19556
19696
  return _extends({}, ApiOptions.defaults, this.props.apiOptions);
19557
19697
  };
19558
19698
  this._getInitialWidgetState = props => {
19559
- const allWidgetInfo = this._getAllWidgetsInfo(props);
19699
+ const allWidgetInfo = getUpgradedWidgetOptions(props.widgets);
19560
19700
  return {
19561
19701
  widgetInfo: allWidgetInfo,
19562
19702
  widgetProps: this._getAllWidgetsStartProps(allWidgetInfo, props)
19563
19703
  };
19564
19704
  };
19565
- // @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; }'.
19566
- this._getAllWidgetsInfo = props => {
19567
- return mapObject(props.widgets, (widgetInfo, widgetId) => {
19568
- if (!widgetInfo.type || !widgetInfo.alignment) {
19569
- const newValues = {};
19570
- if (!widgetInfo.type) {
19571
- // TODO: why does widget have no type?
19572
- // We don't want to derive type from widget ID
19573
- // see: LEMS-1845
19574
- newValues.type = widgetId.split(" ")[0];
19575
- }
19576
- if (!widgetInfo.alignment) {
19577
- newValues.alignment = "default";
19578
- }
19579
- widgetInfo = _.extend({}, widgetInfo, newValues);
19580
- }
19581
- return upgradeWidgetInfoToLatestVersion(widgetInfo);
19582
- });
19583
- };
19584
19705
  this._getAllWidgetsStartProps = (allWidgetInfo, props) => {
19585
19706
  const {
19586
19707
  apiOptions,
@@ -20026,7 +20147,7 @@ class Renderer extends React.Component {
20026
20147
  // Instead, we center them with css magic in styles.less
20027
20148
  // /cry(aria)
20028
20149
  this._foundTextNodes = true;
20029
- if (_.contains(this.widgetIds, node.id)) {
20150
+ if (this.widgetIds.includes(node.id)) {
20030
20151
  // We don't want to render a duplicate widget key/ref,
20031
20152
  // as this causes problems with react (for obvious
20032
20153
  // reasons). Instead we just notify the
@@ -20330,7 +20451,7 @@ class Renderer extends React.Component {
20330
20451
  };
20331
20452
  this.getInputPaths = () => {
20332
20453
  const inputPaths = [];
20333
- _.each(this.widgetIds, widgetId => {
20454
+ this.widgetIds.forEach(widgetId => {
20334
20455
  const widget = this.getWidgetInstance(widgetId);
20335
20456
  if (widget && widget.getInputPaths) {
20336
20457
  // Grab all input paths and add widgetID to the front
@@ -20338,7 +20459,7 @@ class Renderer extends React.Component {
20338
20459
  // Prefix paths with their widgetID and add to collective
20339
20460
  // list of paths.
20340
20461
  // @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
20341
- _.each(widgetInputPaths, inputPath => {
20462
+ widgetInputPaths.forEach(inputPath => {
20342
20463
  const relativeInputPath = [widgetId].concat(inputPath);
20343
20464
  inputPaths.push(relativeInputPath);
20344
20465
  });
@@ -20450,7 +20571,7 @@ class Renderer extends React.Component {
20450
20571
  }, 0);
20451
20572
  });
20452
20573
  };
20453
- this.setInputValue = (path, newValue, focus) => {
20574
+ this.setInputValue = (path, newValue, cb) => {
20454
20575
  // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
20455
20576
  const widgetId = _.first(path);
20456
20577
  // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
@@ -20458,7 +20579,7 @@ class Renderer extends React.Component {
20458
20579
  const widget = this.getWidgetInstance(widgetId);
20459
20580
 
20460
20581
  // Widget handles parsing of the interWidgetPath.
20461
- widget == null || widget.setInputValue == null || widget.setInputValue(interWidgetPath, newValue, focus);
20582
+ widget == null || widget.setInputValue == null || widget.setInputValue(interWidgetPath, newValue, cb);
20462
20583
  };
20463
20584
  /**
20464
20585
  * Returns an array of all widget IDs in the order they occur in
@@ -20467,12 +20588,20 @@ class Renderer extends React.Component {
20467
20588
  this.getWidgetIds = () => {
20468
20589
  return this.widgetIds;
20469
20590
  };
20591
+ /**
20592
+ * @deprecated use scorePerseusItem
20593
+ */
20594
+ this.guessAndScore = () => {
20595
+ const totalGuess = this.getUserInput();
20596
+ const totalScore = this.score();
20597
+ return [totalGuess, totalScore];
20598
+ };
20470
20599
  this.examples = () => {
20471
20600
  const widgetIds = this.widgetIds;
20472
- const examples = _.compact(_.map(widgetIds, widgetId => {
20601
+ const examples = widgetIds.map(widgetId => {
20473
20602
  const widget = this.getWidgetInstance(widgetId);
20474
20603
  return widget != null && widget.examples ? widget.examples() : null;
20475
- }));
20604
+ }).filter(Boolean);
20476
20605
 
20477
20606
  // no widgets with examples
20478
20607
  if (!examples.length) {
@@ -20671,6 +20800,12 @@ class Renderer extends React.Component {
20671
20800
  isLastUsedWidget: widgetId === this.state.lastUsedWidgetId
20672
20801
  });
20673
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
+ */
20674
20809
  emptyWidgets() {
20675
20810
  return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20676
20811
  }
@@ -20727,27 +20862,15 @@ class Renderer extends React.Component {
20727
20862
  }
20728
20863
 
20729
20864
  /**
20730
- * Returns an object mapping from widget ID to perseus-style score.
20731
- * The keys of this object are the values of the array returned
20732
- * from `getWidgetIds`.
20733
- */
20734
- scoreWidgets() {
20735
- return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20736
- }
20737
-
20738
- /**
20739
- * Grades the content.
20865
+ * Scores the content.
20866
+ *
20867
+ * @deprecated use scorePerseusItem
20740
20868
  */
20741
20869
  score() {
20742
- const scores = this.scoreWidgets();
20870
+ const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20743
20871
  const combinedScore = Util.flattenScores(scores);
20744
20872
  return combinedScore;
20745
20873
  }
20746
- guessAndScore() {
20747
- const totalGuess = this.getUserInput();
20748
- const totalScore = this.score();
20749
- return [totalGuess, totalScore];
20750
- }
20751
20874
  render() {
20752
20875
  const apiOptions = this.getApiOptions();
20753
20876
  if (this.reuseMarkdown) {
@@ -20947,6 +21070,7 @@ class TextInput extends React.Component {
20947
21070
  value: formattedValue,
20948
21071
  type: "text",
20949
21072
  "aria-label": labelText,
21073
+ "aria-describedby": this.props["aria-describedby"],
20950
21074
  onChange: value => this.props.onChange(value),
20951
21075
  placeholder: placeholder,
20952
21076
  onFocus: onFocus,
@@ -21213,6 +21337,7 @@ class Tooltip extends React.Component {
21213
21337
  }, /*#__PURE__*/React.createElement("div", {
21214
21338
  ref: this.tooltipContainerRef,
21215
21339
  className: "tooltipContainer",
21340
+ role: "tooltip",
21216
21341
  style: {
21217
21342
  position: "absolute",
21218
21343
  // height must start out undefined, not null, so that
@@ -21293,9 +21418,15 @@ class InputWithExamples extends React.Component {
21293
21418
  };
21294
21419
  this._renderInput = () => {
21295
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 ");
21296
21427
  const inputProps = {
21297
21428
  id: id,
21298
- "aria-describedby": id,
21429
+ "aria-describedby": ariaId,
21299
21430
  ref: "input",
21300
21431
  className: this._getInputClassName(),
21301
21432
  labelText: this.props.labelText,
@@ -21311,7 +21442,12 @@ class InputWithExamples extends React.Component {
21311
21442
  autoCorrect: "off",
21312
21443
  spellCheck: "false"
21313
21444
  };
21314
- 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));
21315
21451
  };
21316
21452
  this._handleFocus = () => {
21317
21453
  this.props.onFocus();
@@ -21353,8 +21489,12 @@ class InputWithExamples extends React.Component {
21353
21489
  }
21354
21490
  render() {
21355
21491
  const input = this._renderInput();
21356
- const examplesContent = _.map(this.props.examples, example => {
21357
- 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}`;
21358
21498
  }).join("\n");
21359
21499
  const showExamples = this.props.shouldShowExamples && this.state.showExamples;
21360
21500
  return /*#__PURE__*/React.createElement(Tooltip, {
@@ -22028,7 +22168,7 @@ function scoreNumericInput(userInput, rubric, strings) {
22028
22168
  // We may have received TeX; try to parse it before grading.
22029
22169
  // If `currentValue` is not TeX, this should be a no-op.
22030
22170
  const currentValue = ParseTex(userInput.currentValue);
22031
- 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);
22032
22172
 
22033
22173
  // The coefficient is an attribute of the widget
22034
22174
  let localValue = currentValue;
@@ -22089,7 +22229,8 @@ class NumericInput extends React.Component {
22089
22229
  this.state = {
22090
22230
  // keeps track of the other set of values when switching
22091
22231
  // between 0 and finite solutions
22092
- previousValues: [""]
22232
+ previousValues: [""],
22233
+ isFocused: false
22093
22234
  };
22094
22235
  // TODO(Nicole, Jeremy): This is maybe never used and should be removed
22095
22236
  this.examples = () => {
@@ -22152,9 +22293,19 @@ class NumericInput extends React.Component {
22152
22293
  };
22153
22294
  this._handleFocus = () => {
22154
22295
  this.props.onFocus([]);
22296
+ this.setState(currentState => {
22297
+ return _extends({}, currentState, {
22298
+ isFocused: true
22299
+ });
22300
+ });
22155
22301
  };
22156
22302
  this._handleBlur = () => {
22157
22303
  this.props.onBlur([]);
22304
+ this.setState(currentState => {
22305
+ return _extends({}, currentState, {
22306
+ isFocused: false
22307
+ });
22308
+ });
22158
22309
  };
22159
22310
  }
22160
22311
  static getUserInputFromProps(props) {
@@ -22197,13 +22348,20 @@ class NumericInput extends React.Component {
22197
22348
  // component.
22198
22349
  const styles = StyleSheet.create({
22199
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
22200
22360
  textAlign: this.props.rightAlign ? "right" : "left",
22201
- width: this.props.size === "small" ? 40 : 80,
22202
- padding: 0,
22203
- height: "auto"
22361
+ width: this.props.size === "small" ? 40 : 80
22204
22362
  }
22205
22363
  });
22206
- return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(InputWithExamples, {
22364
+ return /*#__PURE__*/React.createElement(InputWithExamples, {
22207
22365
  ref: ref => this.inputRef = ref,
22208
22366
  value: this.props.currentValue,
22209
22367
  onChange: this.handleChange,
@@ -22215,7 +22373,7 @@ class NumericInput extends React.Component {
22215
22373
  id: this.props.widgetId,
22216
22374
  disabled: this.props.apiOptions.readOnly,
22217
22375
  style: styles.input
22218
- }));
22376
+ });
22219
22377
  }
22220
22378
  }
22221
22379
 
@@ -27111,8 +27269,8 @@ var Definition$1 = {
27111
27269
  scorer: () => scoreNoop()
27112
27270
  };
27113
27271
 
27114
- // The props are type `any` on purpose so that this can receive props
27115
- // from any deprecated widget
27272
+ // There are no required props, but this component can receive props from any
27273
+ // deprecated widget
27116
27274
  class DeprecatedStandin extends React__default.Component {
27117
27275
  constructor(...args) {
27118
27276
  super(...args);
@@ -29222,6 +29380,13 @@ const getPromptJSON$f = rendererJSON => {
29222
29380
  });
29223
29381
  };
29224
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
+
29225
29390
  // exports has no 'transform'
29226
29391
 
29227
29392
  class Group extends React.Component {
@@ -29327,6 +29492,9 @@ class Group extends React.Component {
29327
29492
  this.change("widgets", this.rendererRef.props.widgets);
29328
29493
  }
29329
29494
  };
29495
+
29496
+ // TODO(LEMS-2391): replace this when there's a separate check
29497
+ // for valid/invalid state
29330
29498
  const score = (_this$rendererRef12 = this.rendererRef) == null ? void 0 : _this$rendererRef12.score();
29331
29499
  const isValid = score && score.type !== "invalid";
29332
29500
  const isInvalid = score && score.type === "invalid";
@@ -29373,6 +29541,9 @@ var Group$1 = {
29373
29541
  displayName: "Group (SAT only)",
29374
29542
  widget: Group,
29375
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,
29376
29547
  hidden: true,
29377
29548
  isLintable: true
29378
29549
  };
@@ -32082,6 +32253,61 @@ const LegacyGrid = ({
32082
32253
 
32083
32254
  // Range is replaced within this file with Interval, but it is used elsewhere
32084
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
+ */
32085
32311
  /**
32086
32312
  * A "PerseusItem" is a classic Perseus item. It is rendered by the
32087
32313
  * `ServerItemRenderer` and the layout is pre-set.
@@ -32097,6 +32323,9 @@ const LegacyGrid = ({
32097
32323
  * A "MultiItem" is an advanced Perseus item. It is rendered by the
32098
32324
  * `MultiRenderer` and you can control the layout of individual parts of the
32099
32325
  * item.
32326
+ *
32327
+ * @deprecated MultiItem support is slated for removal in a future Perseus
32328
+ * release.
32100
32329
  */
32101
32330
  const ItemExtras = [
32102
32331
  // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
@@ -32118,6 +32347,11 @@ const ItemExtras = [
32118
32347
  // The user might benefit from using a statistics Z Table like https://www.ztable.net/
32119
32348
  "zTable"];
32120
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
+
32121
32355
  // prettier-ignore
32122
32356
 
32123
32357
  // prettier-ignore
@@ -32186,7 +32420,9 @@ const ItemExtras = [
32186
32420
 
32187
32421
  //prettier-ignore
32188
32422
 
32189
- // A background image applied to various widgets.
32423
+ /**
32424
+ * A background image applied to various widgets.
32425
+ */
32190
32426
 
32191
32427
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
32192
32428
 
@@ -36754,8 +36990,6 @@ const makeInvalidTypeError = (functionName, graphType) => {
36754
36990
  });
36755
36991
  };
36756
36992
 
36757
- // There's no transform function in exports
36758
-
36759
36993
  // TODO: there's another, very similar getSinusoidCoefficients function
36760
36994
  // they should probably be merged
36761
36995
  function getSinusoidCoefficients(coords) {
@@ -41891,23 +42125,37 @@ const getPromptJSON$6 = (renderProps, userInput) => {
41891
42125
  };
41892
42126
  };
41893
42127
 
41894
- function scoreNumberLine(state, rubric) {
41895
- const range = rubric.range;
41896
- const divisionRange = state.divisionRange;
41897
- const start = rubric.initialX != null ? rubric.initialX : range[0];
41898
- const startRel = rubric.isInequality ? "ge" : "eq";
41899
- const correctRel = rubric.correctRel || "eq";
41900
- const correctPos = number$1.equal(state.numLinePosition, rubric.correctX || 0);
41901
- 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];
41902
42137
 
41903
42138
  // TODO: I don't think isTickCrtl is a thing anymore
41904
- if (state.isTickCrtl && outsideAllowedRange) {
42139
+ if (userInput.isTickCrtl && outsideAllowedRange) {
41905
42140
  return {
41906
42141
  type: "invalid",
41907
42142
  message: "Number of divisions is outside the allowed range."
41908
42143
  };
41909
42144
  }
41910
- 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) {
41911
42159
  return {
41912
42160
  type: "points",
41913
42161
  earned: 1,
@@ -41915,7 +42163,7 @@ function scoreNumberLine(state, rubric) {
41915
42163
  message: null
41916
42164
  };
41917
42165
  }
41918
- if (state.numLinePosition === start && state.rel === startRel) {
42166
+ if (userInput.numLinePosition === start && userInput.rel === startRel) {
41919
42167
  // We're where we started.
41920
42168
  return {
41921
42169
  type: "invalid",
@@ -44861,7 +45109,7 @@ const init = function init() {
44861
45109
 
44862
45110
  // This file is processed by a Rollup plugin (replace) to inject the production
44863
45111
  const libName = "@khanacademy/perseus";
44864
- const libVersion = "44.0.0";
45112
+ const libVersion = "45.1.0";
44865
45113
  addLibraryVersionToPerseusDebug(libName, libVersion);
44866
45114
 
44867
45115
  const apiVersion = {
@@ -45469,14 +45717,10 @@ class ServerItemRenderer extends React.Component {
45469
45717
  });
45470
45718
  }
45471
45719
  componentDidUpdate(prevProps, prevState) {
45472
- if (this.props.apiOptions.answerableCallback) {
45720
+ const answerableCallback = this.props.apiOptions.answerableCallback;
45721
+ if (answerableCallback != null) {
45473
45722
  const isAnswerable = this.questionRenderer.emptyWidgets().length === 0;
45474
- const {
45475
- answerableCallback
45476
- } = this.props.apiOptions;
45477
- if (answerableCallback) {
45478
- answerableCallback(isAnswerable);
45479
- }
45723
+ answerableCallback(isAnswerable);
45480
45724
  }
45481
45725
  if (!this._fullyRendered) {
45482
45726
  const assetsLoaded = Object.values(this.state.assetStatuses).every(Boolean);
@@ -45485,6 +45729,13 @@ class ServerItemRenderer extends React.Component {
45485
45729
  this.props.onRendered(true);
45486
45730
  }
45487
45731
  }
45732
+ if (this.props.score && this.props.score !== prevProps.score) {
45733
+ const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45734
+ this.setState({
45735
+ questionCompleted: this.props.score.correct,
45736
+ questionHighlightedWidgets: emptyQuestionAreaWidgets
45737
+ });
45738
+ }
45488
45739
  }
45489
45740
  componentWillUnmount() {
45490
45741
  if (this.blurTimeoutID != null) {
@@ -45602,18 +45853,37 @@ class ServerItemRenderer extends React.Component {
45602
45853
  return this.questionRenderer.getPromptJSON();
45603
45854
  }
45604
45855
 
45856
+ /**
45857
+ * Returns an array of the widget `.getUserInput()` results
45858
+ *
45859
+ * TODO: can we remove this? Seems to be just for backwards
45860
+ * compatibility with old Perseus Chrome logging
45861
+ * @deprecated use getUserInput
45862
+ */
45863
+ getUserInputLegacy() {
45864
+ return this.questionRenderer.getUserInput();
45865
+ }
45866
+
45867
+ /**
45868
+ * Returns an object of the widget `.getUserInput()` results
45869
+ */
45870
+ getUserInput() {
45871
+ return this.questionRenderer.getUserInputMap();
45872
+ }
45873
+
45605
45874
  /**
45606
45875
  * Grades the item.
45876
+ *
45877
+ * @deprecated use scorePerseusItem
45607
45878
  */
45608
45879
  scoreInput() {
45609
- const guessAndScore = this.questionRenderer.guessAndScore();
45610
- const guess = guessAndScore[0];
45611
- const score = guessAndScore[1];
45880
+ const guess = this.getUserInput();
45881
+ const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
45612
45882
 
45613
45883
  // Continue to include an empty guess for the now defunct answer area.
45614
45884
  // TODO(alex): Check whether we rely on the format here for
45615
45885
  // analyzing ProblemLogs. If not, remove this layer.
45616
- const maxCompatGuess = [guess, []];
45886
+ const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
45617
45887
  const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
45618
45888
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45619
45889
  this.setState({
@@ -45793,7 +46063,7 @@ const traverseRenderer$1 = function traverseRenderer(rendererOptions, contentCal
45793
46063
  newContent = modifiedContent;
45794
46064
  }
45795
46065
  }
45796
- const newWidgets = objective_.mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
46066
+ const newWidgets = mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
45797
46067
  // Widgets without info or a type are empty widgets, and
45798
46068
  // should always be renderable. It's also annoying to write
45799
46069
  // checks for this everywhere, so we just filter them out once and
@@ -47461,140 +47731,6 @@ const registerAllWidgetsForTesting = () => {
47461
47731
  replaceDeprecatedWidgets();
47462
47732
  };
47463
47733
 
47464
- // These are all the current widgets that require user-input and are supported for automatic scoring
47465
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
47466
-
47467
- /**
47468
- * Regex for widget placeholders in a string.
47469
- *
47470
- * First capture group is the widget ID (ex. 'radio 1')
47471
- * Second capture group is the widget type (ex. "radio)
47472
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
47473
- */
47474
- function getWidgetRegex() {
47475
- return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
47476
- }
47477
-
47478
- /**
47479
- * Add a widget placeholder using the widget ID.
47480
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
47481
- *
47482
- * @param {string} id
47483
- * @returns {string}
47484
- */
47485
- function addWidget(id) {
47486
- return `[[☃ ${id}]]`;
47487
- }
47488
-
47489
- /**
47490
- * Get a widget type by a widget's ID
47491
- * (please don't derive type from ID)
47492
- *
47493
- * @param {string} widgetId the ID of the widget
47494
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47495
- * @returns {string} the widget type (ie "radio")
47496
- */
47497
- function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
47498
- var _widget$type;
47499
- const widget = widgetMap[WidgetId];
47500
- return (_widget$type = widget == null ? void 0 : widget.type) != null ? _widget$type : null;
47501
- }
47502
-
47503
- /**
47504
- * Does the content have a specific type of widget?
47505
- *
47506
- * @param {string} type the type of the widget in question (ie "radio")
47507
- * @param {string} content the string to search through
47508
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47509
- * @returns {boolean} if the content includes the widget type
47510
- */
47511
- function contentHasWidgetType(type, content, widgetMap) {
47512
- return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
47513
- }
47514
-
47515
- /**
47516
- * Extract all widget IDs, which includes the widget type and instance number.
47517
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
47518
- *
47519
- * Content should contain Perseus widget placeholders,
47520
- * which look like: '[[☃ radio 1]]'.
47521
- *
47522
- * @param {string} content
47523
- * @returns {ReadonlyArray<string>} widgetIds
47524
- */
47525
- function getWidgetIdsFromContent(content) {
47526
- const widgets = [];
47527
- const localWidgetRegex = getWidgetRegex();
47528
- let match = localWidgetRegex.exec(content);
47529
- while (match !== null) {
47530
- widgets.push(match[1]);
47531
- match = localWidgetRegex.exec(content);
47532
- }
47533
- return widgets;
47534
- }
47535
-
47536
- /**
47537
- * Get a list of widget IDs from content,
47538
- * but only for specific widget types
47539
- *
47540
- * @param {string} type the type of widget (ie "radio")
47541
- * @param {string} content the string to parse
47542
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47543
- * @returns {ReadonlyArray<string>} the widget type (ie "radio")
47544
- */
47545
- function getWidgetIdsFromContentByType(type, content, widgetMap) {
47546
- const rv = [];
47547
- const widgetIdsInContent = getWidgetIdsFromContent(content);
47548
- widgetIdsInContent.forEach(widgetId => {
47549
- const widget = widgetMap[widgetId];
47550
- if ((widget == null ? void 0 : widget.type) === type) {
47551
- rv.push(widgetId);
47552
- }
47553
- });
47554
- return rv;
47555
- }
47556
-
47557
- /**
47558
- * Pull the widget map out of ItemData
47559
- *
47560
- * @param {PerseusItem} PerseusItem containing a widgetMap
47561
- * @returns {WidgetMap} the widget map in the PerseusItem
47562
- */
47563
- function getWidgetsMapFromItemData(itemData) {
47564
- return itemData.question.widgets;
47565
- }
47566
-
47567
- /**
47568
- * Get the widget information from a WidgetMap
47569
- *
47570
- * @param {string} widgetId the ID of the widget
47571
- * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
47572
- */
47573
- function getWidgetFromWidgetMap(widgetId, widgetMap) {
47574
- var _widgetMap$widgetId;
47575
- return (_widgetMap$widgetId = widgetMap[widgetId]) != null ? _widgetMap$widgetId : null;
47576
- }
47577
-
47578
- /**
47579
- * Get select widgets from a widget map.
47580
- * Useful for multi-items that needs to split
47581
- * one PerseusItem into several
47582
- *
47583
- * @param {ReadonlyArray<string>} widgetIds to extract
47584
- * @param {PerseusWidgetsMap} widgetMap to extract from
47585
- * @return {PerseusWidgetsMap} a new widget map with requested widgets
47586
- */
47587
- function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
47588
- const widgets = {};
47589
- widgetIds.forEach(widgetId => {
47590
- const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
47591
- if (widget) {
47592
- widgets[widgetId] = widget;
47593
- }
47594
- });
47595
- return widgets;
47596
- }
47597
-
47598
47734
  /**
47599
47735
  * This function extracts the answers from the widgets.
47600
47736
  *
@@ -48526,7 +48662,7 @@ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
48526
48662
  const parseDefinitionWidget = parseWidget(constant("definition"), object({
48527
48663
  togglePrompt: string,
48528
48664
  definition: string,
48529
- static: boolean
48665
+ static: defaulted(boolean, () => false)
48530
48666
  }));
48531
48667
 
48532
48668
  const parseDropdownWidget = parseWidget(constant("dropdown"), object({
@@ -48728,15 +48864,26 @@ const parseIframeWidget = parseWidget(constant("iframe"), object({
48728
48864
  static: defaulted(boolean, () => false)
48729
48865
  }));
48730
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;
48731
48879
  const parsePerseusImageBackground = object({
48732
48880
  url: optional(nullable(string)),
48733
- width: optional(number),
48734
- height: optional(number),
48735
- top: optional(number),
48736
- left: optional(number),
48737
- bottom: optional(number),
48738
- // TODO(benchristel): convert scale to a number
48739
- 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)
48740
48887
  });
48741
48888
 
48742
48889
  const pairOfNumbers$2 = pair(number, number);
@@ -48755,6 +48902,12 @@ const parseImageWidget = parseWidget(constant("image"), object({
48755
48902
  box: optional(pairOfNumbers$2)
48756
48903
  }));
48757
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
+ };
48758
48911
  const parseInputNumberWidget = parseWidget(constant("input-number"), object({
48759
48912
  answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
48760
48913
  inexact: optional(boolean),
@@ -48762,7 +48915,11 @@ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
48762
48915
  rightAlign: optional(boolean),
48763
48916
  simplify: enumeration("required", "optional", "enforced"),
48764
48917
  size: enumeration("normal", "small"),
48765
- 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,
48766
48923
  customKeypad: optional(boolean)
48767
48924
  }));
48768
48925
 
@@ -49060,18 +49217,27 @@ const parseLockedFunctionType = object({
49060
49217
  const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
49061
49218
  const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
49062
49219
  step: pairOfNumbers,
49063
- gridStep: pairOfNumbers,
49064
- 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),
49065
49226
  backgroundImage: optional(parsePerseusImageBackground),
49066
49227
  markings: enumeration("graph", "grid", "none"),
49067
- labels: array(string),
49228
+ labels: optional(array(string)),
49068
49229
  showProtractor: boolean,
49069
49230
  showRuler: optional(boolean),
49070
49231
  showTooltips: optional(boolean),
49071
49232
  rulerLabel: optional(string),
49072
49233
  rulerTicks: optional(number),
49073
49234
  range: pair(pairOfNumbers, pairOfNumbers),
49074
- 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
+ })),
49075
49241
  correct: parsePerseusGraphType,
49076
49242
  // TODO(benchristel): default lockedFigures to empty array
49077
49243
  lockedFigures: optional(array(parseLockedFigure)),
@@ -49139,7 +49305,10 @@ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
49139
49305
  isTickCtrl: optional(nullable(boolean)),
49140
49306
  divisionRange: array(number),
49141
49307
  numDivisions: optional(nullable(number)),
49142
- 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),
49143
49312
  tickStep: optional(nullable(number)),
49144
49313
  correctRel: optional(nullable(string)),
49145
49314
  correctX: number,
@@ -49152,18 +49321,18 @@ const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "d
49152
49321
  const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
49153
49322
  answers: array(object({
49154
49323
  message: string,
49155
- value: number,
49324
+ value: optional(number),
49156
49325
  status: string,
49157
49326
  answerForms: optional(array(parseMathFormat)),
49158
49327
  strict: boolean,
49159
49328
  maxError: optional(nullable(number)),
49160
49329
  simplify: optional(nullable(string))
49161
49330
  })),
49162
- labelText: string,
49331
+ labelText: optional(string),
49163
49332
  size: string,
49164
49333
  coefficient: boolean,
49165
49334
  rightAlign: optional(boolean),
49166
- static: boolean,
49335
+ static: defaulted(boolean, () => false),
49167
49336
  answerForms: optional(array(object({
49168
49337
  name: parseMathFormat,
49169
49338
  simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
@@ -49176,12 +49345,18 @@ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
49176
49345
  function parseRenderer(rawValue, ctx) {
49177
49346
  return parsePerseusRenderer(rawValue, ctx);
49178
49347
  }
49348
+ const largeToAuto = (height, ctx) => {
49349
+ if (height === "large") {
49350
+ return ctx.success("auto");
49351
+ }
49352
+ return ctx.success(height);
49353
+ };
49179
49354
  const parseOrdererWidget = parseWidget(constant("orderer"), object({
49180
- options: array(parseRenderer),
49355
+ options: defaulted(array(parseRenderer), () => []),
49181
49356
  correctOptions: array(parseRenderer),
49182
49357
  otherOptions: array(parseRenderer),
49183
- height: enumeration("normal", "auto"),
49184
- layout: enumeration("horizontal", "vertical")
49358
+ height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
49359
+ layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
49185
49360
  }));
49186
49361
 
49187
49362
  const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
@@ -49195,7 +49370,7 @@ const parsePassageWidget = parseWidget(constant("passage"), object({
49195
49370
  passageText: string,
49196
49371
  passageTitle: string,
49197
49372
  showLineNumbers: boolean,
49198
- static: boolean
49373
+ static: defaulted(boolean, () => false)
49199
49374
  }));
49200
49375
 
49201
49376
  const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
@@ -49216,7 +49391,10 @@ const parsePlotterWidget = parseWidget(constant("plotter"), object({
49216
49391
  picUrl: optional(nullable(string)),
49217
49392
  picSize: optional(nullable(number)),
49218
49393
  picBoxHeight: optional(nullable(number)),
49219
- 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])
49220
49398
  }));
49221
49399
 
49222
49400
  const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
@@ -49368,6 +49546,10 @@ const parseWidgetsMapEntry = ([key, widget], widgetMap, ctx) => {
49368
49546
  return parseAndAssign(`table ${id}`, parseTableWidget);
49369
49547
  case "video":
49370
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);
49371
49553
  default:
49372
49554
  if (getWidget(type)) {
49373
49555
  // @ts-expect-error - 'type' is not a valid widget type
@@ -49376,6 +49558,11 @@ const parseWidgetsMapEntry = ([key, widget], widgetMap, ctx) => {
49376
49558
  return ctx.failure("a valid widget type", type);
49377
49559
  }
49378
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({}));
49379
49566
  const parseStringToPositiveInt = (rawValue, ctx) => {
49380
49567
  if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
49381
49568
  return ctx.failure("numeric string", rawValue);
@@ -49651,5 +49838,5 @@ const GrapherUtil = {
49651
49838
  typeToButton
49652
49839
  };
49653
49840
 
49654
- 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, shapes, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
49841
+ 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 };
49655
49842
  //# sourceMappingURL=index.js.map