@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/index.js CHANGED
@@ -3918,6 +3918,7 @@ Zoom.prototype.zoomImage = function () {
3918
3918
  this._zoomOriginal();
3919
3919
  }.bind(this);
3920
3920
  img.src = this._targetImage.src;
3921
+ img.alt = this._targetImage.alt;
3921
3922
  this.$zoomedImage = $zoomedImage;
3922
3923
  };
3923
3924
  Zoom.prototype._zoomOriginal = function () {
@@ -8993,7 +8994,10 @@ var Tex$1 = {
8993
8994
  // We use createElement instead of JSX here because we can't name this file tex.tsx;
8994
8995
  // that name is already taken.
8995
8996
  // eslint-disable-next-line import/no-deprecated
8996
- render( /*#__PURE__*/React__namespace.createElement(TeX, {
8997
+ render(
8998
+ /*#__PURE__*/
8999
+ // eslint-disable-next-line react/no-children-prop
9000
+ React__namespace.createElement(TeX, {
8997
9001
  children: text,
8998
9002
  onRender: callback
8999
9003
  }), $texHolder[0]);
@@ -14757,36 +14761,15 @@ const pluck = function (table, subKey) {
14757
14761
  * });
14758
14762
  * {a: 2, b: 3}
14759
14763
  */
14760
- const mapObject$1 = function (obj, lambda) {
14764
+ const mapObject = function (obj, lambda) {
14761
14765
  const result = {};
14762
- ___default["default"].each(___default["default"].keys(obj), function (key) {
14766
+ Object.keys(obj).forEach(key => {
14763
14767
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
14764
14768
  result[key] = lambda(obj[key], key);
14765
14769
  });
14766
14770
  return result;
14767
14771
  };
14768
14772
 
14769
- /**
14770
- * Maps an array to an object
14771
- *
14772
- * > mapObjectFromArray(['a', 'b'], function(elem) {
14773
- * return elem + elem;
14774
- * });
14775
- * {a: 'aa', b: 'bb'}
14776
- */
14777
- const mapObjectFromArray = function (arr, lambda) {
14778
- const result = {};
14779
- ___default["default"].each(arr, function (elem) {
14780
- result[elem] = lambda(elem);
14781
- });
14782
- return result;
14783
- };
14784
- var objective_ = {
14785
- pluck: pluck,
14786
- mapObject: mapObject$1,
14787
- mapObjectFromArray: mapObjectFromArray
14788
- };
14789
-
14790
14773
  /**
14791
14774
  * MovableLine
14792
14775
  */
@@ -14851,7 +14834,7 @@ ___default["default"].extend(MovableLine$4.prototype, {
14851
14834
  // - are objects, not primitives (and need a deeper copy)
14852
14835
  // - they don't need getters created for them
14853
14836
  // TODO(jack): Consider "default" once we es3ify perseus
14854
- objective_.pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
14837
+ pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
14855
14838
  },
14856
14839
  /**
14857
14840
  * Resets the object to its state as if it were constructed with
@@ -15358,7 +15341,7 @@ class MovablePoint$5 {
15358
15341
  // - are objects, not primitives (and need a deeper copy)
15359
15342
  // - they don't need getters created for them
15360
15343
  // TODO(jack): Consider "default" once we es3ify perseus
15361
- objective_.pluck(MovablePointOptions, "standard")
15344
+ pluck(MovablePointOptions, "standard")
15362
15345
  // We only update props here, because we want things on state to
15363
15346
  // be persistent, and updated appropriately in modify()
15364
15347
  ),
@@ -16259,7 +16242,7 @@ ___default["default"].extend(MovablePolygon.prototype, {
16259
16242
  // - are objects, not primitives (and need a deeper copy)
16260
16243
  // - they don't need getters created for them
16261
16244
  // TODO(jack): Consider "default" once we es3ify perseus
16262
- objective_.pluck(MovablePolygonOptions, "standard")
16245
+ pluck(MovablePolygonOptions, "standard")
16263
16246
 
16264
16247
  // We only update props here, because we want things on state to
16265
16248
  // be persistent, and updated appropriately in modify()
@@ -18919,6 +18902,138 @@ class QuestionParagraph extends React__namespace.Component {
18919
18902
  }
18920
18903
  }
18921
18904
 
18905
+ // These are all the current widgets that require user-input and are supported for automatic scoring
18906
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
18907
+
18908
+ /**
18909
+ * Regex for widget placeholders in a string.
18910
+ *
18911
+ * First capture group is the widget ID (ex. 'radio 1')
18912
+ * Second capture group is the widget type (ex. "radio)
18913
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
18914
+ */
18915
+ function getWidgetRegex() {
18916
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
18917
+ }
18918
+
18919
+ /**
18920
+ * Add a widget placeholder using the widget ID.
18921
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
18922
+ *
18923
+ * @param {string} id
18924
+ * @returns {string}
18925
+ */
18926
+ function addWidget(id) {
18927
+ return `[[☃ ${id}]]`;
18928
+ }
18929
+
18930
+ /**
18931
+ * Get a widget type by a widget's ID
18932
+ * (please don't derive type from ID)
18933
+ *
18934
+ * @param {string} widgetId the ID of the widget
18935
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18936
+ * @returns {string} the widget type (ie "radio")
18937
+ */
18938
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
18939
+ const widget = widgetMap[WidgetId];
18940
+ return widget?.type ?? null;
18941
+ }
18942
+
18943
+ /**
18944
+ * Does the content have a specific type of widget?
18945
+ *
18946
+ * @param {string} type the type of the widget in question (ie "radio")
18947
+ * @param {string} content the string to search through
18948
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18949
+ * @returns {boolean} if the content includes the widget type
18950
+ */
18951
+ function contentHasWidgetType(type, content, widgetMap) {
18952
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
18953
+ }
18954
+
18955
+ /**
18956
+ * Extract all widget IDs, which includes the widget type and instance number.
18957
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
18958
+ *
18959
+ * Content should contain Perseus widget placeholders,
18960
+ * which look like: '[[☃ radio 1]]'.
18961
+ *
18962
+ * @param {string} content
18963
+ * @returns {ReadonlyArray<string>} widgetIds
18964
+ */
18965
+ function getWidgetIdsFromContent(content) {
18966
+ const widgets = [];
18967
+ const localWidgetRegex = getWidgetRegex();
18968
+ let match = localWidgetRegex.exec(content);
18969
+ while (match !== null) {
18970
+ widgets.push(match[1]);
18971
+ match = localWidgetRegex.exec(content);
18972
+ }
18973
+ return widgets;
18974
+ }
18975
+
18976
+ /**
18977
+ * Get a list of widget IDs from content,
18978
+ * but only for specific widget types
18979
+ *
18980
+ * @param {string} type the type of widget (ie "radio")
18981
+ * @param {string} content the string to parse
18982
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18983
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
18984
+ */
18985
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
18986
+ const rv = [];
18987
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
18988
+ widgetIdsInContent.forEach(widgetId => {
18989
+ const widget = widgetMap[widgetId];
18990
+ if (widget?.type === type) {
18991
+ rv.push(widgetId);
18992
+ }
18993
+ });
18994
+ return rv;
18995
+ }
18996
+
18997
+ /**
18998
+ * Pull the widget map out of ItemData
18999
+ *
19000
+ * @param {PerseusItem} PerseusItem containing a widgetMap
19001
+ * @returns {WidgetMap} the widget map in the PerseusItem
19002
+ */
19003
+ function getWidgetsMapFromItemData(itemData) {
19004
+ return itemData.question.widgets;
19005
+ }
19006
+
19007
+ /**
19008
+ * Get the widget information from a WidgetMap
19009
+ *
19010
+ * @param {string} widgetId the ID of the widget
19011
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
19012
+ */
19013
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
19014
+ return widgetMap[widgetId] ?? null;
19015
+ }
19016
+
19017
+ /**
19018
+ * Get select widgets from a widget map.
19019
+ * Useful for multi-items that needs to split
19020
+ * one PerseusItem into several
19021
+ *
19022
+ * @param {ReadonlyArray<string>} widgetIds to extract
19023
+ * @param {PerseusWidgetsMap} widgetMap to extract from
19024
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
19025
+ */
19026
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
19027
+ const widgets = {};
19028
+ widgetIds.forEach(widgetId => {
19029
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
19030
+ if (widget) {
19031
+ widgets[widgetId] = widget;
19032
+ }
19033
+ });
19034
+ return widgets;
19035
+ }
19036
+
18922
19037
  const DEFAULT_ALIGNMENT$1 = "block";
18923
19038
  // NOTE(kevinb): "default" is not one in `validAlignments`.
18924
19039
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -19349,36 +19464,67 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19349
19464
  isLintable: isLintable
19350
19465
  });
19351
19466
 
19467
+ function getUpgradedWidgetOptions(oldWidgetOptions) {
19468
+ // @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
19469
+ return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
19470
+ if (!widgetInfo.type || !widgetInfo.alignment) {
19471
+ const newValues = {};
19472
+ if (!widgetInfo.type) {
19473
+ // TODO: why does widget have no type?
19474
+ // We don't want to derive type from widget ID
19475
+ // see: LEMS-1845
19476
+ newValues.type = widgetId.split(" ")[0];
19477
+ }
19478
+ if (!widgetInfo.alignment) {
19479
+ newValues.alignment = "default";
19480
+ }
19481
+ widgetInfo = {
19482
+ ...widgetInfo,
19483
+ ...newValues
19484
+ };
19485
+ }
19486
+ return upgradeWidgetInfoToLatestVersion(widgetInfo);
19487
+ });
19488
+ }
19352
19489
  function emptyWidgetsFunctional(widgets,
19353
19490
  // This is a port of old code, I'm not sure why
19354
19491
  // we need widgetIds vs the keys of the widgets object
19355
19492
  widgetIds, userInputMap, strings, locale) {
19493
+ const upgradedWidgets = getUpgradedWidgetOptions(widgets);
19356
19494
  return widgetIds.filter(id => {
19357
- const widget = widgets[id];
19495
+ const widget = upgradedWidgets[id];
19358
19496
  if (!widget || widget.static) {
19359
19497
  // Static widgets shouldn't count as empty
19360
19498
  return false;
19361
19499
  }
19362
- let score = null;
19363
- const userInput = userInputMap[id];
19364
19500
  const scorer = getWidgetScorer(widget.type);
19365
- if (widget.type === "group") {
19366
- const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19367
- score = Util.flattenScores(scores);
19368
- } else if (scorer) {
19369
- score = scorer(userInput, widget.options, strings, locale);
19370
- }
19501
+ const score = scorer?.(userInputMap[id], widget.options, strings, locale);
19371
19502
  if (score) {
19372
19503
  return Util.scoreIsEmpty(score);
19373
19504
  }
19374
19505
  });
19375
19506
  }
19507
+
19508
+ // TODO: combine scorePerseusItem with scoreWidgetsFunctional
19509
+ // once scorePerseusItem is the only one calling scoreWidgetsFunctional
19510
+ function scorePerseusItem(perseusRenderData, userInputMap,
19511
+ // TODO(LEMS-2461,LEMS-2391): these probably
19512
+ // need to be removed before we move this to the server
19513
+ strings, locale) {
19514
+ // There seems to be a chance that PerseusRenderer.widgets might include
19515
+ // widget data for widgets that are not in PerseusRenderer.content,
19516
+ // so this checks that the widgets are being used before scoring them
19517
+ const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
19518
+ const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
19519
+ return Util.flattenScores(scores);
19520
+ }
19376
19521
  function scoreWidgetsFunctional(widgets,
19377
19522
  // This is a port of old code, I'm not sure why
19378
19523
  // we need widgetIds vs the keys of the widgets object
19379
19524
  widgetIds, userInputMap, strings, locale) {
19525
+ const upgradedWidgets = getUpgradedWidgetOptions(widgets);
19380
19526
  const gradedWidgetIds = widgetIds.filter(id => {
19381
- const props = widgets[id];
19527
+ const props = upgradedWidgets[id];
19382
19528
  const widgetIsGraded = props?.graded == null || props.graded;
19383
19529
  const widgetIsStatic = !!props?.static;
19384
19530
  // Ungraded widgets or widgets set to static shouldn't be graded.
@@ -19386,17 +19532,15 @@ widgetIds, userInputMap, strings, locale) {
19386
19532
  });
19387
19533
  const widgetScores = {};
19388
19534
  gradedWidgetIds.forEach(id => {
19389
- const widget = widgets[id];
19535
+ const widget = upgradedWidgets[id];
19390
19536
  if (!widget) {
19391
19537
  return;
19392
19538
  }
19393
19539
  const userInput = userInputMap[id];
19394
19540
  const scorer = getWidgetScorer(widget.type);
19395
- if (widget.type === "group") {
19396
- const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19397
- widgetScores[id] = Util.flattenScores(scores);
19398
- } else if (scorer) {
19399
- widgetScores[id] = scorer(userInput, widget.options, strings, locale);
19541
+ const score = scorer?.(userInput, widget.options, strings, locale);
19542
+ if (score != null) {
19543
+ widgetScores[id] = score;
19400
19544
  }
19401
19545
  });
19402
19546
  return widgetScores;
@@ -19703,9 +19847,6 @@ class WidgetContainer extends React__namespace.Component {
19703
19847
  }
19704
19848
  }
19705
19849
 
19706
- const {
19707
- mapObject
19708
- } = objective_;
19709
19850
  const rContainsNonWhitespace = /\S/;
19710
19851
  const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
19711
19852
  const noopOnRender = () => {};
@@ -19918,32 +20059,12 @@ class Renderer extends React__namespace.Component {
19918
20059
  };
19919
20060
  };
19920
20061
  _getInitialWidgetState = props => {
19921
- const allWidgetInfo = this._getAllWidgetsInfo(props);
20062
+ const allWidgetInfo = getUpgradedWidgetOptions(props.widgets);
19922
20063
  return {
19923
20064
  widgetInfo: allWidgetInfo,
19924
20065
  widgetProps: this._getAllWidgetsStartProps(allWidgetInfo, props)
19925
20066
  };
19926
20067
  };
19927
-
19928
- // @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; }'.
19929
- _getAllWidgetsInfo = props => {
19930
- return mapObject(props.widgets, (widgetInfo, widgetId) => {
19931
- if (!widgetInfo.type || !widgetInfo.alignment) {
19932
- const newValues = {};
19933
- if (!widgetInfo.type) {
19934
- // TODO: why does widget have no type?
19935
- // We don't want to derive type from widget ID
19936
- // see: LEMS-1845
19937
- newValues.type = widgetId.split(" ")[0];
19938
- }
19939
- if (!widgetInfo.alignment) {
19940
- newValues.alignment = "default";
19941
- }
19942
- widgetInfo = ___default["default"].extend({}, widgetInfo, newValues);
19943
- }
19944
- return upgradeWidgetInfoToLatestVersion(widgetInfo);
19945
- });
19946
- };
19947
20068
  _getAllWidgetsStartProps = (allWidgetInfo, props) => {
19948
20069
  const {
19949
20070
  apiOptions,
@@ -20442,7 +20563,7 @@ class Renderer extends React__namespace.Component {
20442
20563
  // Instead, we center them with css magic in styles.less
20443
20564
  // /cry(aria)
20444
20565
  this._foundTextNodes = true;
20445
- if (___default["default"].contains(this.widgetIds, node.id)) {
20566
+ if (this.widgetIds.includes(node.id)) {
20446
20567
  // We don't want to render a duplicate widget key/ref,
20447
20568
  // as this causes problems with react (for obvious
20448
20569
  // reasons). Instead we just notify the
@@ -20767,7 +20888,7 @@ class Renderer extends React__namespace.Component {
20767
20888
  };
20768
20889
  getInputPaths = () => {
20769
20890
  const inputPaths = [];
20770
- ___default["default"].each(this.widgetIds, widgetId => {
20891
+ this.widgetIds.forEach(widgetId => {
20771
20892
  const widget = this.getWidgetInstance(widgetId);
20772
20893
  if (widget && widget.getInputPaths) {
20773
20894
  // Grab all input paths and add widgetID to the front
@@ -20775,7 +20896,7 @@ class Renderer extends React__namespace.Component {
20775
20896
  // Prefix paths with their widgetID and add to collective
20776
20897
  // list of paths.
20777
20898
  // @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
20778
- ___default["default"].each(widgetInputPaths, inputPath => {
20899
+ widgetInputPaths.forEach(inputPath => {
20779
20900
  const relativeInputPath = [widgetId].concat(inputPath);
20780
20901
  inputPaths.push(relativeInputPath);
20781
20902
  });
@@ -20842,6 +20963,13 @@ class Renderer extends React__namespace.Component {
20842
20963
  }, this);
20843
20964
  return state;
20844
20965
  };
20966
+
20967
+ /**
20968
+ * Returns an array of widget ids that are empty (meaning widgets where the
20969
+ * learner has not interacted with the widget yet or has not filled in all
20970
+ * fields). For example, the `interactive-graph` widget is considered
20971
+ * empty if the graph is in the starting state.
20972
+ */
20845
20973
  emptyWidgets() {
20846
20974
  return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20847
20975
  }
@@ -20895,7 +21023,7 @@ class Renderer extends React__namespace.Component {
20895
21023
  }, 0);
20896
21024
  });
20897
21025
  };
20898
- setInputValue = (path, newValue, focus) => {
21026
+ setInputValue = (path, newValue, cb) => {
20899
21027
  // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
20900
21028
  const widgetId = ___default["default"].first(path);
20901
21029
  // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
@@ -20903,7 +21031,7 @@ class Renderer extends React__namespace.Component {
20903
21031
  const widget = this.getWidgetInstance(widgetId);
20904
21032
 
20905
21033
  // Widget handles parsing of the interWidgetPath.
20906
- widget?.setInputValue?.(interWidgetPath, newValue, focus);
21034
+ widget?.setInputValue?.(interWidgetPath, newValue, cb);
20907
21035
  };
20908
21036
 
20909
21037
  /**
@@ -20968,33 +21096,30 @@ class Renderer extends React__namespace.Component {
20968
21096
  }
20969
21097
 
20970
21098
  /**
20971
- * Returns an object mapping from widget ID to perseus-style score.
20972
- * The keys of this object are the values of the array returned
20973
- * from `getWidgetIds`.
20974
- */
20975
- scoreWidgets() {
20976
- return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20977
- }
20978
-
20979
- /**
20980
- * Grades the content.
21099
+ * Scores the content.
21100
+ *
21101
+ * @deprecated use scorePerseusItem
20981
21102
  */
20982
21103
  score() {
20983
- const scores = this.scoreWidgets();
21104
+ const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20984
21105
  const combinedScore = Util.flattenScores(scores);
20985
21106
  return combinedScore;
20986
21107
  }
20987
- guessAndScore() {
21108
+
21109
+ /**
21110
+ * @deprecated use scorePerseusItem
21111
+ */
21112
+ guessAndScore = () => {
20988
21113
  const totalGuess = this.getUserInput();
20989
21114
  const totalScore = this.score();
20990
21115
  return [totalGuess, totalScore];
20991
- }
21116
+ };
20992
21117
  examples = () => {
20993
21118
  const widgetIds = this.widgetIds;
20994
- const examples = ___default["default"].compact(___default["default"].map(widgetIds, widgetId => {
21119
+ const examples = widgetIds.map(widgetId => {
20995
21120
  const widget = this.getWidgetInstance(widgetId);
20996
21121
  return widget != null && widget.examples ? widget.examples() : null;
20997
- }));
21122
+ }).filter(Boolean);
20998
21123
 
20999
21124
  // no widgets with examples
21000
21125
  if (!examples.length) {
@@ -21204,6 +21329,7 @@ class TextInput extends React__namespace.Component {
21204
21329
  value: formattedValue,
21205
21330
  type: "text",
21206
21331
  "aria-label": labelText,
21332
+ "aria-describedby": this.props["aria-describedby"],
21207
21333
  onChange: value => this.props.onChange(value),
21208
21334
  placeholder: placeholder,
21209
21335
  onFocus: onFocus,
@@ -21462,6 +21588,7 @@ class Tooltip extends React__namespace.Component {
21462
21588
  }, /*#__PURE__*/React__namespace.createElement("div", {
21463
21589
  ref: this.tooltipContainerRef,
21464
21590
  className: "tooltipContainer",
21591
+ role: "tooltip",
21465
21592
  style: {
21466
21593
  position: "absolute",
21467
21594
  // height must start out undefined, not null, so that
@@ -21538,9 +21665,15 @@ class InputWithExamples extends React__namespace.Component {
21538
21665
  };
21539
21666
  _renderInput = () => {
21540
21667
  const id = this._getUniqueId();
21668
+ const ariaId = `aria-for-${id}`;
21669
+ // Generate text from a known set of format options that will read well in a screen reader
21670
+ const examplesAria = this.props.examples.length === 7 ? "" : `${this.props.examples[0]}
21671
+ ${this.props.examples.slice(1).join(", or\n")}`
21672
+ // @ts-expect-error TS2550: Property replaceAll does not exist on type string.
21673
+ .replaceAll("*", "").replaceAll("$", "").replaceAll("\\ \\text{pi}", " pi").replaceAll("\\ ", " and ");
21541
21674
  const inputProps = {
21542
21675
  id: id,
21543
- "aria-describedby": id,
21676
+ "aria-describedby": ariaId,
21544
21677
  ref: "input",
21545
21678
  className: this._getInputClassName(),
21546
21679
  labelText: this.props.labelText,
@@ -21556,7 +21689,12 @@ class InputWithExamples extends React__namespace.Component {
21556
21689
  autoCorrect: "off",
21557
21690
  spellCheck: "false"
21558
21691
  };
21559
- return /*#__PURE__*/React__namespace.createElement(TextInput, inputProps);
21692
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(TextInput, inputProps), /*#__PURE__*/React__namespace.createElement("span", {
21693
+ id: ariaId,
21694
+ style: {
21695
+ display: "none"
21696
+ }
21697
+ }, examplesAria));
21560
21698
  };
21561
21699
  _handleFocus = () => {
21562
21700
  this.props.onFocus();
@@ -21597,8 +21735,12 @@ class InputWithExamples extends React__namespace.Component {
21597
21735
  };
21598
21736
  render() {
21599
21737
  const input = this._renderInput();
21600
- const examplesContent = ___default["default"].map(this.props.examples, example => {
21601
- return "- " + example;
21738
+ const examplesContent = this.props.examples.length <= 2 ? this.props.examples.join(" ") // A single item (with or without leading text) is not a "list"
21739
+ : this.props.examples // 2 or more items should display as a list
21740
+ .map((example, index) => {
21741
+ // If the first example is bold, then it is most likely a heading/leading text.
21742
+ // So, it shouldn't be part of the list.
21743
+ return index === 0 && example.startsWith("**") ? `${example}\n` : `- ${example}`;
21602
21744
  }).join("\n");
21603
21745
  const showExamples = this.props.shouldShowExamples && this.state.showExamples;
21604
21746
  return /*#__PURE__*/React__namespace.createElement(Tooltip, {
@@ -22252,7 +22394,7 @@ function scoreNumericInput(userInput, rubric, strings) {
22252
22394
  // We may have received TeX; try to parse it before grading.
22253
22395
  // If `currentValue` is not TeX, this should be a no-op.
22254
22396
  const currentValue = ParseTex(userInput.currentValue);
22255
- const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => Math.abs(answer.value) <= 1);
22397
+ const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value !== undefined && Math.abs(answer.value) <= 1);
22256
22398
 
22257
22399
  // The coefficient is an attribute of the widget
22258
22400
  let localValue = currentValue;
@@ -22328,7 +22470,8 @@ class NumericInput extends React__namespace.Component {
22328
22470
  state = {
22329
22471
  // keeps track of the other set of values when switching
22330
22472
  // between 0 and finite solutions
22331
- previousValues: [""]
22473
+ previousValues: [""],
22474
+ isFocused: false
22332
22475
  };
22333
22476
 
22334
22477
  // TODO(Nicole, Jeremy): This is maybe never used and should be removed
@@ -22393,9 +22536,21 @@ class NumericInput extends React__namespace.Component {
22393
22536
  };
22394
22537
  _handleFocus = () => {
22395
22538
  this.props.onFocus([]);
22539
+ this.setState(currentState => {
22540
+ return {
22541
+ ...currentState,
22542
+ isFocused: true
22543
+ };
22544
+ });
22396
22545
  };
22397
22546
  _handleBlur = () => {
22398
22547
  this.props.onBlur([]);
22548
+ this.setState(currentState => {
22549
+ return {
22550
+ ...currentState,
22551
+ isFocused: false
22552
+ };
22553
+ });
22399
22554
  };
22400
22555
  render() {
22401
22556
  let labelText = this.props.labelText;
@@ -22426,13 +22581,20 @@ class NumericInput extends React__namespace.Component {
22426
22581
  // component.
22427
22582
  const styles = aphrodite.StyleSheet.create({
22428
22583
  input: {
22584
+ borderRadius: "3px",
22585
+ borderWidth: this.state.isFocused ? "2px" : "1px",
22586
+ display: "inline-block",
22587
+ fontFamily: `Symbola, "Times New Roman", serif`,
22588
+ fontSize: "18px",
22589
+ height: "32px",
22590
+ lineHeight: "18px",
22591
+ padding: this.state.isFocused ? "4px" : "4px 5px",
22592
+ // account for added focus border thickness
22429
22593
  textAlign: this.props.rightAlign ? "right" : "left",
22430
- width: this.props.size === "small" ? 40 : 80,
22431
- padding: 0,
22432
- height: "auto"
22594
+ width: this.props.size === "small" ? 40 : 80
22433
22595
  }
22434
22596
  });
22435
- return /*#__PURE__*/React__namespace.createElement("div", null, /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
22597
+ return /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
22436
22598
  ref: ref => this.inputRef = ref,
22437
22599
  value: this.props.currentValue,
22438
22600
  onChange: this.handleChange,
@@ -22444,7 +22606,7 @@ class NumericInput extends React__namespace.Component {
22444
22606
  id: this.props.widgetId,
22445
22607
  disabled: this.props.apiOptions.readOnly,
22446
22608
  style: styles.input
22447
- }));
22609
+ });
22448
22610
  }
22449
22611
  }
22450
22612
 
@@ -27372,8 +27534,8 @@ var Definition$1 = {
27372
27534
  scorer: () => scoreNoop()
27373
27535
  };
27374
27536
 
27375
- // The props are type `any` on purpose so that this can receive props
27376
- // from any deprecated widget
27537
+ // There are no required props, but this component can receive props from any
27538
+ // deprecated widget
27377
27539
  class DeprecatedStandin extends React__default["default"].Component {
27378
27540
  static contextType = PerseusI18nContext;
27379
27541
  // this just helps with TS weak typing when a Widget
@@ -29496,6 +29658,13 @@ const getPromptJSON$f = rendererJSON => {
29496
29658
  };
29497
29659
  };
29498
29660
 
29661
+ // The `group` widget is basically a widget hosting a full Perseus system in
29662
+ // it. As such, scoring a group means scoring all widgets it contains.
29663
+ function scoreGroup(userInput, options, strings, locale) {
29664
+ const scores = scoreWidgetsFunctional(options.widgets, Object.keys(options.widgets), userInput, strings, locale);
29665
+ return Util.flattenScores(scores);
29666
+ }
29667
+
29499
29668
  // exports has no 'transform'
29500
29669
 
29501
29670
  class Group extends React__namespace.Component {
@@ -29602,6 +29771,9 @@ class Group extends React__namespace.Component {
29602
29771
  this.change("widgets", this.rendererRef.props.widgets);
29603
29772
  }
29604
29773
  };
29774
+
29775
+ // TODO(LEMS-2391): replace this when there's a separate check
29776
+ // for valid/invalid state
29605
29777
  const score = this.rendererRef?.score();
29606
29778
  const isValid = score && score.type !== "invalid";
29607
29779
  const isInvalid = score && score.type === "invalid";
@@ -29644,6 +29816,9 @@ var Group$1 = {
29644
29816
  displayName: "Group (SAT only)",
29645
29817
  widget: Group,
29646
29818
  traverseChildWidgets: traverseChildWidgets,
29819
+ // TODO(LEMS-2656): remove TS suppression
29820
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
29821
+ scorer: scoreGroup,
29647
29822
  hidden: true,
29648
29823
  isLintable: true
29649
29824
  };
@@ -32387,6 +32562,61 @@ const LegacyGrid = _ref => {
32387
32562
 
32388
32563
  // Range is replaced within this file with Interval, but it is used elsewhere
32389
32564
  // and exported from the package, so we need to keep it around.
32565
+ /**
32566
+ * Our core set of Perseus widgets.
32567
+ *
32568
+ * This interface is the basis for "registering" all Perseus widget types.
32569
+ * There should be one key/value pair for each supported widget. If you create
32570
+ * a new widget, an entry should be added to this interface. Note that this
32571
+ * only registers the widget options type, you'll also need to register the
32572
+ * widget so that it's available at runtime (@see
32573
+ * {@link file://./widgets.ts#registerWidget}).
32574
+ *
32575
+ * Importantly, the key should be the name that is used in widget IDs. For most
32576
+ * widgets that is the same as the widget option's `type` field. In cases where
32577
+ * a widget has been deprecated and replaced with the deprecated-standin
32578
+ * widget, it should be the original widget type!
32579
+ *
32580
+ * If you define the widget outside of this package, you can still add the new
32581
+ * widget to this interface by writing the following in that package that
32582
+ * contains the widget. TypeScript will merge that definition of the
32583
+ * `PerseusWidgets` with the one defined below.
32584
+ *
32585
+ * ```typescript
32586
+ * declare module "@khanacademy/perseus" {
32587
+ * interface PerseusWidgetTypes {
32588
+ * // A new widget
32589
+ * "new-awesomeness": MyAwesomeNewWidget;
32590
+ *
32591
+ * // A deprecated widget
32592
+ * "super-old-widget": AutoCorrectWidget;
32593
+ * }
32594
+ * }
32595
+ *
32596
+ * // The new widget's options definition
32597
+ * type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>;
32598
+ *
32599
+ * // The deprecated widget's options definition
32600
+ * type SuperOldWidget = WidgetOptions<'super-old-widget', object>;
32601
+ * ```
32602
+ *
32603
+ * This interface can be extended through the magic of TypeScript "Declaration
32604
+ * merging". Specifically, we augment this module and extend this interface.
32605
+ *
32606
+ * @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
32607
+ */
32608
+ /**
32609
+ * A map of widget IDs to widget options. This is most often used as the type
32610
+ * for a set of widgets defined in a `PerseusItem` but can also be useful to
32611
+ * represent a function parameter where only `widgets` from a `PerseusItem` are
32612
+ * needed. Today Widget IDs are made up of the widget type and an incrementing
32613
+ * integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid
32614
+ * reading/parsing the widget id to derive any information from it, except in
32615
+ * the case of this map.
32616
+ *
32617
+ * @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
32618
+ * by augmenting the PerseusWidgetTypes with new widget types!
32619
+ */
32390
32620
  /**
32391
32621
  * A "PerseusItem" is a classic Perseus item. It is rendered by the
32392
32622
  * `ServerItemRenderer` and the layout is pre-set.
@@ -32402,6 +32632,9 @@ const LegacyGrid = _ref => {
32402
32632
  * A "MultiItem" is an advanced Perseus item. It is rendered by the
32403
32633
  * `MultiRenderer` and you can control the layout of individual parts of the
32404
32634
  * item.
32635
+ *
32636
+ * @deprecated MultiItem support is slated for removal in a future Perseus
32637
+ * release.
32405
32638
  */
32406
32639
  const ItemExtras = [
32407
32640
  // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
@@ -32423,6 +32656,11 @@ const ItemExtras = [
32423
32656
  // The user might benefit from using a statistics Z Table like https://www.ztable.net/
32424
32657
  "zTable"];
32425
32658
 
32659
+ /**
32660
+ * The type representing the common structure of all widget's options. The
32661
+ * `Options` generic type represents the widget-specific option data.
32662
+ */
32663
+
32426
32664
  // prettier-ignore
32427
32665
 
32428
32666
  // prettier-ignore
@@ -32491,7 +32729,9 @@ const ItemExtras = [
32491
32729
 
32492
32730
  //prettier-ignore
32493
32731
 
32494
- // A background image applied to various widgets.
32732
+ /**
32733
+ * A background image applied to various widgets.
32734
+ */
32495
32735
 
32496
32736
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
32497
32737
 
@@ -37135,8 +37375,6 @@ const makeInvalidTypeError = (functionName, graphType) => {
37135
37375
  });
37136
37376
  };
37137
37377
 
37138
- // There's no transform function in exports
37139
-
37140
37378
  // TODO: there's another, very similar getSinusoidCoefficients function
37141
37379
  // they should probably be merged
37142
37380
  function getSinusoidCoefficients(coords) {
@@ -42254,23 +42492,37 @@ const getPromptJSON$6 = (renderProps, userInput) => {
42254
42492
  };
42255
42493
  };
42256
42494
 
42257
- function scoreNumberLine(state, rubric) {
42258
- const range = rubric.range;
42259
- const divisionRange = state.divisionRange;
42260
- const start = rubric.initialX != null ? rubric.initialX : range[0];
42261
- const startRel = rubric.isInequality ? "ge" : "eq";
42262
- const correctRel = rubric.correctRel || "eq";
42263
- const correctPos = kmath.number.equal(state.numLinePosition, rubric.correctX || 0);
42264
- const outsideAllowedRange = state.numDivisions > divisionRange[1] || state.numDivisions < divisionRange[0];
42495
+ /**
42496
+ * Checks user input is within the allowed range and not the same as the initial
42497
+ * state.
42498
+ * @param userInput
42499
+ * @see 'scoreNumberLine' for the scoring logic.
42500
+ */
42501
+ function validateNumberLine(userInput) {
42502
+ const divisionRange = userInput.divisionRange;
42503
+ const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
42265
42504
 
42266
42505
  // TODO: I don't think isTickCrtl is a thing anymore
42267
- if (state.isTickCrtl && outsideAllowedRange) {
42506
+ if (userInput.isTickCrtl && outsideAllowedRange) {
42268
42507
  return {
42269
42508
  type: "invalid",
42270
42509
  message: "Number of divisions is outside the allowed range."
42271
42510
  };
42272
42511
  }
42273
- if (correctPos && correctRel === state.rel) {
42512
+ return null;
42513
+ }
42514
+
42515
+ function scoreNumberLine(userInput, scoringData) {
42516
+ const validationError = validateNumberLine(userInput);
42517
+ if (validationError) {
42518
+ return validationError;
42519
+ }
42520
+ const range = scoringData.range;
42521
+ const start = scoringData.initialX != null ? scoringData.initialX : range[0];
42522
+ const startRel = scoringData.isInequality ? "ge" : "eq";
42523
+ const correctRel = scoringData.correctRel || "eq";
42524
+ const correctPos = kmath.number.equal(userInput.numLinePosition, scoringData.correctX || 0);
42525
+ if (correctPos && correctRel === userInput.rel) {
42274
42526
  return {
42275
42527
  type: "points",
42276
42528
  earned: 1,
@@ -42278,7 +42530,7 @@ function scoreNumberLine(state, rubric) {
42278
42530
  message: null
42279
42531
  };
42280
42532
  }
42281
- if (state.numLinePosition === start && state.rel === startRel) {
42533
+ if (userInput.numLinePosition === start && userInput.rel === startRel) {
42282
42534
  // We're where we started.
42283
42535
  return {
42284
42536
  type: "invalid",
@@ -45227,7 +45479,7 @@ const init = function () {
45227
45479
 
45228
45480
  // This file is processed by a Rollup plugin (replace) to inject the production
45229
45481
  const libName = "@khanacademy/perseus";
45230
- const libVersion = "44.0.0";
45482
+ const libVersion = "45.1.0";
45231
45483
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45232
45484
 
45233
45485
  const apiVersion = {
@@ -45815,14 +46067,10 @@ class ServerItemRenderer extends React__namespace.Component {
45815
46067
  });
45816
46068
  }
45817
46069
  componentDidUpdate(prevProps, prevState) {
45818
- if (this.props.apiOptions.answerableCallback) {
46070
+ const answerableCallback = this.props.apiOptions.answerableCallback;
46071
+ if (answerableCallback != null) {
45819
46072
  const isAnswerable = this.questionRenderer.emptyWidgets().length === 0;
45820
- const {
45821
- answerableCallback
45822
- } = this.props.apiOptions;
45823
- if (answerableCallback) {
45824
- answerableCallback(isAnswerable);
45825
- }
46073
+ answerableCallback(isAnswerable);
45826
46074
  }
45827
46075
  if (!this._fullyRendered) {
45828
46076
  const assetsLoaded = Object.values(this.state.assetStatuses).every(Boolean);
@@ -45831,6 +46079,13 @@ class ServerItemRenderer extends React__namespace.Component {
45831
46079
  this.props.onRendered(true);
45832
46080
  }
45833
46081
  }
46082
+ if (this.props.score && this.props.score !== prevProps.score) {
46083
+ const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
46084
+ this.setState({
46085
+ questionCompleted: this.props.score.correct,
46086
+ questionHighlightedWidgets: emptyQuestionAreaWidgets
46087
+ });
46088
+ }
45834
46089
  }
45835
46090
  componentWillUnmount() {
45836
46091
  if (this.blurTimeoutID != null) {
@@ -45966,18 +46221,37 @@ class ServerItemRenderer extends React__namespace.Component {
45966
46221
  return this.questionRenderer.getPromptJSON();
45967
46222
  }
45968
46223
 
46224
+ /**
46225
+ * Returns an array of the widget `.getUserInput()` results
46226
+ *
46227
+ * TODO: can we remove this? Seems to be just for backwards
46228
+ * compatibility with old Perseus Chrome logging
46229
+ * @deprecated use getUserInput
46230
+ */
46231
+ getUserInputLegacy() {
46232
+ return this.questionRenderer.getUserInput();
46233
+ }
46234
+
46235
+ /**
46236
+ * Returns an object of the widget `.getUserInput()` results
46237
+ */
46238
+ getUserInput() {
46239
+ return this.questionRenderer.getUserInputMap();
46240
+ }
46241
+
45969
46242
  /**
45970
46243
  * Grades the item.
46244
+ *
46245
+ * @deprecated use scorePerseusItem
45971
46246
  */
45972
46247
  scoreInput() {
45973
- const guessAndScore = this.questionRenderer.guessAndScore();
45974
- const guess = guessAndScore[0];
45975
- const score = guessAndScore[1];
46248
+ const guess = this.getUserInput();
46249
+ const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
45976
46250
 
45977
46251
  // Continue to include an empty guess for the now defunct answer area.
45978
46252
  // TODO(alex): Check whether we rely on the format here for
45979
46253
  // analyzing ProblemLogs. If not, remove this layer.
45980
- const maxCompatGuess = [guess, []];
46254
+ const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
45981
46255
  const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
45982
46256
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45983
46257
  this.setState({
@@ -46167,7 +46441,7 @@ const traverseRenderer$1 = function (rendererOptions, contentCallback, deepWidge
46167
46441
  newContent = modifiedContent;
46168
46442
  }
46169
46443
  }
46170
- const newWidgets = objective_.mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
46444
+ const newWidgets = mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
46171
46445
  // Widgets without info or a type are empty widgets, and
46172
46446
  // should always be renderable. It's also annoying to write
46173
46447
  // checks for this everywhere, so we just filter them out once and
@@ -47839,138 +48113,6 @@ const registerAllWidgetsForTesting = () => {
47839
48113
  replaceDeprecatedWidgets();
47840
48114
  };
47841
48115
 
47842
- // These are all the current widgets that require user-input and are supported for automatic scoring
47843
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
47844
-
47845
- /**
47846
- * Regex for widget placeholders in a string.
47847
- *
47848
- * First capture group is the widget ID (ex. 'radio 1')
47849
- * Second capture group is the widget type (ex. "radio)
47850
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
47851
- */
47852
- function getWidgetRegex() {
47853
- return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
47854
- }
47855
-
47856
- /**
47857
- * Add a widget placeholder using the widget ID.
47858
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
47859
- *
47860
- * @param {string} id
47861
- * @returns {string}
47862
- */
47863
- function addWidget(id) {
47864
- return `[[☃ ${id}]]`;
47865
- }
47866
-
47867
- /**
47868
- * Get a widget type by a widget's ID
47869
- * (please don't derive type from ID)
47870
- *
47871
- * @param {string} widgetId the ID of the widget
47872
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47873
- * @returns {string} the widget type (ie "radio")
47874
- */
47875
- function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
47876
- const widget = widgetMap[WidgetId];
47877
- return widget?.type ?? null;
47878
- }
47879
-
47880
- /**
47881
- * Does the content have a specific type of widget?
47882
- *
47883
- * @param {string} type the type of the widget in question (ie "radio")
47884
- * @param {string} content the string to search through
47885
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47886
- * @returns {boolean} if the content includes the widget type
47887
- */
47888
- function contentHasWidgetType(type, content, widgetMap) {
47889
- return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
47890
- }
47891
-
47892
- /**
47893
- * Extract all widget IDs, which includes the widget type and instance number.
47894
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
47895
- *
47896
- * Content should contain Perseus widget placeholders,
47897
- * which look like: '[[☃ radio 1]]'.
47898
- *
47899
- * @param {string} content
47900
- * @returns {ReadonlyArray<string>} widgetIds
47901
- */
47902
- function getWidgetIdsFromContent(content) {
47903
- const widgets = [];
47904
- const localWidgetRegex = getWidgetRegex();
47905
- let match = localWidgetRegex.exec(content);
47906
- while (match !== null) {
47907
- widgets.push(match[1]);
47908
- match = localWidgetRegex.exec(content);
47909
- }
47910
- return widgets;
47911
- }
47912
-
47913
- /**
47914
- * Get a list of widget IDs from content,
47915
- * but only for specific widget types
47916
- *
47917
- * @param {string} type the type of widget (ie "radio")
47918
- * @param {string} content the string to parse
47919
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47920
- * @returns {ReadonlyArray<string>} the widget type (ie "radio")
47921
- */
47922
- function getWidgetIdsFromContentByType(type, content, widgetMap) {
47923
- const rv = [];
47924
- const widgetIdsInContent = getWidgetIdsFromContent(content);
47925
- widgetIdsInContent.forEach(widgetId => {
47926
- const widget = widgetMap[widgetId];
47927
- if (widget?.type === type) {
47928
- rv.push(widgetId);
47929
- }
47930
- });
47931
- return rv;
47932
- }
47933
-
47934
- /**
47935
- * Pull the widget map out of ItemData
47936
- *
47937
- * @param {PerseusItem} PerseusItem containing a widgetMap
47938
- * @returns {WidgetMap} the widget map in the PerseusItem
47939
- */
47940
- function getWidgetsMapFromItemData(itemData) {
47941
- return itemData.question.widgets;
47942
- }
47943
-
47944
- /**
47945
- * Get the widget information from a WidgetMap
47946
- *
47947
- * @param {string} widgetId the ID of the widget
47948
- * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
47949
- */
47950
- function getWidgetFromWidgetMap(widgetId, widgetMap) {
47951
- return widgetMap[widgetId] ?? null;
47952
- }
47953
-
47954
- /**
47955
- * Get select widgets from a widget map.
47956
- * Useful for multi-items that needs to split
47957
- * one PerseusItem into several
47958
- *
47959
- * @param {ReadonlyArray<string>} widgetIds to extract
47960
- * @param {PerseusWidgetsMap} widgetMap to extract from
47961
- * @return {PerseusWidgetsMap} a new widget map with requested widgets
47962
- */
47963
- function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
47964
- const widgets = {};
47965
- widgetIds.forEach(widgetId => {
47966
- const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
47967
- if (widget) {
47968
- widgets[widgetId] = widget;
47969
- }
47970
- });
47971
- return widgets;
47972
- }
47973
-
47974
48116
  /**
47975
48117
  * This function extracts the answers from the widgets.
47976
48118
  *
@@ -48909,7 +49051,7 @@ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
48909
49051
  const parseDefinitionWidget = parseWidget(constant("definition"), object({
48910
49052
  togglePrompt: string,
48911
49053
  definition: string,
48912
- static: boolean
49054
+ static: defaulted(boolean, () => false)
48913
49055
  }));
48914
49056
 
48915
49057
  const parseDropdownWidget = parseWidget(constant("dropdown"), object({
@@ -49112,15 +49254,26 @@ const parseIframeWidget = parseWidget(constant("iframe"), object({
49112
49254
  static: defaulted(boolean, () => false)
49113
49255
  }));
49114
49256
 
49257
+ const stringToNumber = (rawValue, ctx) => {
49258
+ if (typeof rawValue === "number") {
49259
+ return ctx.success(rawValue);
49260
+ }
49261
+ const parsedNumber = +rawValue;
49262
+ if (rawValue === "" || isNaN(parsedNumber)) {
49263
+ return ctx.failure("a number or numeric string", rawValue);
49264
+ }
49265
+ return ctx.success(parsedNumber);
49266
+ };
49267
+
49268
+ const numericToNumber = pipeParsers(union(number).or(string).parser).then(stringToNumber).parser;
49115
49269
  const parsePerseusImageBackground = object({
49116
49270
  url: optional(nullable(string)),
49117
- width: optional(number),
49118
- height: optional(number),
49119
- top: optional(number),
49120
- left: optional(number),
49121
- bottom: optional(number),
49122
- // TODO(benchristel): convert scale to a number
49123
- scale: optional(union(number).or(string).parser)
49271
+ width: optional(numericToNumber),
49272
+ height: optional(numericToNumber),
49273
+ top: optional(numericToNumber),
49274
+ left: optional(numericToNumber),
49275
+ bottom: optional(numericToNumber),
49276
+ scale: optional(numericToNumber)
49124
49277
  });
49125
49278
 
49126
49279
  const pairOfNumbers$2 = pair(number, number);
@@ -49139,6 +49292,12 @@ const parseImageWidget = parseWidget(constant("image"), object({
49139
49292
  box: optional(pairOfNumbers$2)
49140
49293
  }));
49141
49294
 
49295
+ const booleanToString = (rawValue, ctx) => {
49296
+ if (typeof rawValue === "boolean") {
49297
+ return ctx.success(String(rawValue));
49298
+ }
49299
+ return ctx.failure("boolean", rawValue);
49300
+ };
49142
49301
  const parseInputNumberWidget = parseWidget(constant("input-number"), object({
49143
49302
  answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
49144
49303
  inexact: optional(boolean),
@@ -49146,7 +49305,11 @@ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
49146
49305
  rightAlign: optional(boolean),
49147
49306
  simplify: enumeration("required", "optional", "enforced"),
49148
49307
  size: enumeration("normal", "small"),
49149
- value: union(number).or(string).parser,
49308
+ // TODO(benchristel): there are some content items where value is a
49309
+ // boolean, even though that makes no sense. We should figure out if
49310
+ // those content items are actually published anywhere, and consider
49311
+ // updating them.
49312
+ value: union(number).or(string).or(booleanToString).parser,
49150
49313
  customKeypad: optional(boolean)
49151
49314
  }));
49152
49315
 
@@ -49444,18 +49607,27 @@ const parseLockedFunctionType = object({
49444
49607
  const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
49445
49608
  const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
49446
49609
  step: pairOfNumbers,
49447
- gridStep: pairOfNumbers,
49448
- snapStep: pairOfNumbers,
49610
+ // TODO(benchristel): rather than making gridStep and snapStep
49611
+ // optional, we should duplicate the defaulting logic from the
49612
+ // InteractiveGraph component. See parse-perseus-json/README.md for
49613
+ // why.
49614
+ gridStep: optional(pairOfNumbers),
49615
+ snapStep: optional(pairOfNumbers),
49449
49616
  backgroundImage: optional(parsePerseusImageBackground),
49450
49617
  markings: enumeration("graph", "grid", "none"),
49451
- labels: array(string),
49618
+ labels: optional(array(string)),
49452
49619
  showProtractor: boolean,
49453
49620
  showRuler: optional(boolean),
49454
49621
  showTooltips: optional(boolean),
49455
49622
  rulerLabel: optional(string),
49456
49623
  rulerTicks: optional(number),
49457
49624
  range: pair(pairOfNumbers, pairOfNumbers),
49458
- graph: parsePerseusGraphType,
49625
+ // NOTE(benchristel): I copied the default graph from
49626
+ // interactive-graph.tsx. See the parse-perseus-json/README.md for
49627
+ // an explanation of why we want to duplicate the default here.
49628
+ graph: defaulted(parsePerseusGraphType, () => ({
49629
+ type: "linear"
49630
+ })),
49459
49631
  correct: parsePerseusGraphType,
49460
49632
  // TODO(benchristel): default lockedFigures to empty array
49461
49633
  lockedFigures: optional(array(parseLockedFigure)),
@@ -49523,7 +49695,10 @@ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
49523
49695
  isTickCtrl: optional(nullable(boolean)),
49524
49696
  divisionRange: array(number),
49525
49697
  numDivisions: optional(nullable(number)),
49526
- snapDivisions: number,
49698
+ // NOTE(benchristel): I copied the default snapDivisions from
49699
+ // number-line.tsx. See the parse-perseus-json/README.md for
49700
+ // an explanation of why we want to duplicate the default here.
49701
+ snapDivisions: defaulted(number, () => 2),
49527
49702
  tickStep: optional(nullable(number)),
49528
49703
  correctRel: optional(nullable(string)),
49529
49704
  correctX: number,
@@ -49536,18 +49711,18 @@ const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "d
49536
49711
  const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
49537
49712
  answers: array(object({
49538
49713
  message: string,
49539
- value: number,
49714
+ value: optional(number),
49540
49715
  status: string,
49541
49716
  answerForms: optional(array(parseMathFormat)),
49542
49717
  strict: boolean,
49543
49718
  maxError: optional(nullable(number)),
49544
49719
  simplify: optional(nullable(string))
49545
49720
  })),
49546
- labelText: string,
49721
+ labelText: optional(string),
49547
49722
  size: string,
49548
49723
  coefficient: boolean,
49549
49724
  rightAlign: optional(boolean),
49550
- static: boolean,
49725
+ static: defaulted(boolean, () => false),
49551
49726
  answerForms: optional(array(object({
49552
49727
  name: parseMathFormat,
49553
49728
  simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
@@ -49560,12 +49735,18 @@ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
49560
49735
  function parseRenderer(rawValue, ctx) {
49561
49736
  return parsePerseusRenderer(rawValue, ctx);
49562
49737
  }
49738
+ const largeToAuto = (height, ctx) => {
49739
+ if (height === "large") {
49740
+ return ctx.success("auto");
49741
+ }
49742
+ return ctx.success(height);
49743
+ };
49563
49744
  const parseOrdererWidget = parseWidget(constant("orderer"), object({
49564
- options: array(parseRenderer),
49745
+ options: defaulted(array(parseRenderer), () => []),
49565
49746
  correctOptions: array(parseRenderer),
49566
49747
  otherOptions: array(parseRenderer),
49567
- height: enumeration("normal", "auto"),
49568
- layout: enumeration("horizontal", "vertical")
49748
+ height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
49749
+ layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
49569
49750
  }));
49570
49751
 
49571
49752
  const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
@@ -49579,7 +49760,7 @@ const parsePassageWidget = parseWidget(constant("passage"), object({
49579
49760
  passageText: string,
49580
49761
  passageTitle: string,
49581
49762
  showLineNumbers: boolean,
49582
- static: boolean
49763
+ static: defaulted(boolean, () => false)
49583
49764
  }));
49584
49765
 
49585
49766
  const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
@@ -49600,7 +49781,10 @@ const parsePlotterWidget = parseWidget(constant("plotter"), object({
49600
49781
  picUrl: optional(nullable(string)),
49601
49782
  picSize: optional(nullable(number)),
49602
49783
  picBoxHeight: optional(nullable(number)),
49603
- plotDimensions: array(number)
49784
+ // NOTE(benchristel): I copied the default plotDimensions from
49785
+ // plotter.tsx. See the parse-perseus-json/README.md for an explanation
49786
+ // of why we want to duplicate the defaults here.
49787
+ plotDimensions: defaulted(array(number), () => [380, 300])
49604
49788
  }));
49605
49789
 
49606
49790
  const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
@@ -49753,6 +49937,10 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
49753
49937
  return parseAndAssign(`table ${id}`, parseTableWidget);
49754
49938
  case "video":
49755
49939
  return parseAndAssign(`video ${id}`, parseVideoWidget);
49940
+ case "sequence":
49941
+ // sequence is a deprecated widget type, and the corresponding
49942
+ // widget component no longer exists.
49943
+ return parseAndAssign(`sequence ${id}`, parseDeprecatedWidget);
49756
49944
  default:
49757
49945
  if (getWidget(type)) {
49758
49946
  // @ts-expect-error - 'type' is not a valid widget type
@@ -49761,6 +49949,11 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
49761
49949
  return ctx.failure("a valid widget type", type);
49762
49950
  }
49763
49951
  };
49952
+ const parseDeprecatedWidget = parseWidget(
49953
+ // Ignore the incoming widget type and hardcode "deprecated-standin"
49954
+ (_, ctx) => ctx.success("deprecated-standin"),
49955
+ // Allow any widget options
49956
+ object({}));
49764
49957
  const parseStringToPositiveInt = (rawValue, ctx) => {
49765
49958
  if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
49766
49959
  return ctx.failure("numeric string", rawValue);
@@ -50140,6 +50333,7 @@ exports.parsePerseusItem = parsePerseusItem;
50140
50333
  exports.plotterPlotTypes = plotterPlotTypes;
50141
50334
  exports.preprocessTex = preprocessTex;
50142
50335
  exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
50336
+ exports.scorePerseusItem = scorePerseusItem;
50143
50337
  exports.shapes = shapes;
50144
50338
  exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
50145
50339
  exports.traverse = traverse;