@khanacademy/perseus 41.4.0 → 42.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/es/index.css +3 -0
  2. package/dist/es/index.js +879 -343
  3. package/dist/es/index.js.map +1 -1
  4. package/dist/es/strings.js +3 -2
  5. package/dist/es/strings.js.map +1 -1
  6. package/dist/index.css +3 -0
  7. package/dist/index.d.ts +6 -3
  8. package/dist/index.js +876 -338
  9. package/dist/index.js.map +1 -1
  10. package/dist/renderer-util.d.ts +3 -2
  11. package/dist/renderer.d.ts +7 -10
  12. package/dist/server-item-renderer.d.ts +34 -3
  13. package/dist/strings.d.ts +2 -1
  14. package/dist/strings.js +3 -2
  15. package/dist/strings.js.map +1 -1
  16. package/dist/types.d.ts +6 -1
  17. package/dist/util/parse-perseus-json/exhaustive-test-tool/index.d.ts +2 -0
  18. package/dist/util/parse-perseus-json/perseus-parsers/graded-group-set-widget.d.ts +3 -0
  19. package/dist/util/parse-perseus-json/perseus-parsers/graded-group-widget.d.ts +16 -0
  20. package/dist/util/parse-perseus-json/perseus-parsers/grapher-widget.d.ts +3 -0
  21. package/dist/util/parse-perseus-json/perseus-parsers/group-widget.d.ts +3 -0
  22. package/dist/util/parse-perseus-json/perseus-parsers/iframe-widget.d.ts +3 -0
  23. package/dist/util/parse-perseus-json/perseus-parsers/image-widget.d.ts +3 -0
  24. package/dist/util/parse-perseus-json/perseus-parsers/input-number-widget.d.ts +3 -0
  25. package/dist/util/parse-perseus-json/perseus-parsers/interaction-widget.d.ts +3 -0
  26. package/dist/util/parse-perseus-json/perseus-parsers/label-image-widget.d.ts +3 -0
  27. package/dist/util/parse-perseus-json/perseus-parsers/matcher-widget.d.ts +3 -0
  28. package/dist/util/parse-perseus-json/perseus-parsers/matrix-widget.d.ts +3 -0
  29. package/dist/util/parse-perseus-json/perseus-parsers/measurer-widget.d.ts +3 -0
  30. package/dist/util/parse-perseus-json/perseus-parsers/molecule-renderer-widget.d.ts +3 -0
  31. package/dist/util/parse-perseus-json/perseus-parsers/number-line-widget.d.ts +3 -0
  32. package/dist/util/parse-perseus-json/perseus-parsers/numeric-input-widget.d.ts +3 -0
  33. package/dist/util/parse-perseus-json/perseus-parsers/orderer-widget.d.ts +3 -0
  34. package/dist/util/parse-perseus-json/perseus-parsers/passage-ref-widget.d.ts +3 -0
  35. package/dist/util/parse-perseus-json/perseus-parsers/passage-widget.d.ts +3 -0
  36. package/dist/util/parse-perseus-json/perseus-parsers/phet-simulation-widget.d.ts +3 -0
  37. package/dist/util/parse-perseus-json/perseus-parsers/plotter-widget.d.ts +3 -0
  38. package/dist/util/parse-perseus-json/perseus-parsers/python-program-widget.d.ts +3 -0
  39. package/dist/util/parse-perseus-json/perseus-parsers/radio-widget.d.ts +3 -0
  40. package/dist/util/parse-perseus-json/perseus-parsers/sorter-widget.d.ts +3 -0
  41. package/dist/util/parse-perseus-json/perseus-parsers/table-widget.d.ts +3 -0
  42. package/dist/util/parse-perseus-json/perseus-parsers/video-widget.d.ts +3 -0
  43. package/dist/widget-ai-utils/graded-group/{prompt-utils.d.ts → graded-group-ai-utils.d.ts} +1 -1
  44. package/dist/widget-ai-utils/graded-group-set/{prompt-utils.d.ts → graded-group-set-ai-utils.d.ts} +1 -1
  45. package/dist/widget-ai-utils/group/{prompt-utils.d.ts → group-ai-utils.d.ts} +1 -1
  46. package/dist/widget-ai-utils/prompt-types.d.ts +33 -0
  47. package/dist/widget-ai-utils/unsupported-widget.d.ts +1 -1
  48. package/dist/widgets/categorizer/categorizer.d.ts +1 -1
  49. package/dist/widgets/definition/definition.d.ts +1 -1
  50. package/dist/widgets/dropdown/dropdown.d.ts +1 -1
  51. package/dist/widgets/explanation/explanation.d.ts +1 -1
  52. package/dist/widgets/expression/expression.d.ts +1 -1
  53. package/dist/widgets/graded-group/graded-group.d.ts +1 -1
  54. package/dist/widgets/graded-group-set/graded-group-set.d.ts +1 -1
  55. package/dist/widgets/grapher/grapher.d.ts +1 -1
  56. package/dist/widgets/group/group.d.ts +1 -1
  57. package/dist/widgets/image/image.d.ts +1 -1
  58. package/dist/widgets/input-number/input-number.d.ts +1 -1
  59. package/dist/widgets/interactive-graph.d.ts +1 -1
  60. package/dist/widgets/interactive-graphs/graphs/components/movable-point.d.ts +9 -0
  61. package/dist/widgets/interactive-graphs/graphs/components/use-control-point.d.ts +9 -0
  62. package/dist/widgets/label-image/label-image.d.ts +1 -1
  63. package/dist/widgets/matcher/matcher.d.ts +1 -1
  64. package/dist/widgets/matrix/matrix.d.ts +1 -1
  65. package/dist/widgets/number-line/number-line.d.ts +1 -1
  66. package/dist/widgets/numeric-input/numeric-input.d.ts +7 -0
  67. package/dist/widgets/orderer/orderer.d.ts +1 -1
  68. package/dist/widgets/passage/passage.d.ts +1 -1
  69. package/dist/widgets/passage-ref/passage-ref.d.ts +1 -1
  70. package/dist/widgets/radio/radio-component.d.ts +1 -1
  71. package/dist/widgets/sorter/sorter.d.ts +1 -1
  72. package/package.json +1 -1
  73. package/dist/prompt-types.d.ts +0 -33
  74. /package/dist/widget-ai-utils/categorizer/{prompt-utils.d.ts → categorizer-ai-utils.d.ts} +0 -0
  75. /package/dist/widget-ai-utils/cs-program/{prompt-utils.d.ts → cs-program-ai-utils.d.ts} +0 -0
  76. /package/dist/widget-ai-utils/definition/{prompt-utils.d.ts → definition-ai-utils.d.ts} +0 -0
  77. /package/dist/widget-ai-utils/dropdown/{prompt-utils.d.ts → dropdown-ai-utils.d.ts} +0 -0
  78. /package/dist/widget-ai-utils/explanation/{prompt-utils.d.ts → explanation-ai-utils.d.ts} +0 -0
  79. /package/dist/widget-ai-utils/expression/{prompt-utils.d.ts → expression-ai-utils.d.ts} +0 -0
  80. /package/dist/widget-ai-utils/grapher/{prompt-utils.d.ts → grapher-ai-utils.d.ts} +0 -0
  81. /package/dist/widget-ai-utils/iframe/{prompt-utils.d.ts → iframe-ai-utils.d.ts} +0 -0
  82. /package/dist/widget-ai-utils/image/{prompt-utils.d.ts → image-ai-utils.d.ts} +0 -0
  83. /package/dist/widget-ai-utils/input-number/{prompt-utils.d.ts → input-number-ai-utils.d.ts} +0 -0
  84. /package/dist/widget-ai-utils/interaction/{prompt-utils.d.ts → interaction-ai-utils.d.ts} +0 -0
  85. /package/dist/widget-ai-utils/interactive-graph/{prompt-utils.d.ts → interactive-graph-ai-utils.d.ts} +0 -0
  86. /package/dist/widget-ai-utils/label-image/{prompt-utils.d.ts → label-image-ai-utils.d.ts} +0 -0
  87. /package/dist/widget-ai-utils/matcher/{prompt-utils.d.ts → matcher-ai-utils.d.ts} +0 -0
  88. /package/dist/widget-ai-utils/matrix/{prompt-utils.d.ts → matrix-ai-utils.d.ts} +0 -0
  89. /package/dist/widget-ai-utils/measurer/{prompt-utils.d.ts → measurer-ai-utils.d.ts} +0 -0
  90. /package/dist/widget-ai-utils/number-line/{prompt-utils.d.ts → number-line-ai-utils.d.ts} +0 -0
  91. /package/dist/widget-ai-utils/orderer/{prompt-utils.d.ts → orderer-ai-utils.d.ts} +0 -0
  92. /package/dist/widget-ai-utils/passage/{prompt-utils.d.ts → passage-ai-utils.d.ts} +0 -0
  93. /package/dist/widget-ai-utils/passage-ref/{prompt-utils.d.ts → passage-ref-ai-utils.d.ts} +0 -0
  94. /package/dist/widget-ai-utils/phet-simulation/{prompt-utils.d.ts → phet-simulation-ai-utils.d.ts} +0 -0
  95. /package/dist/widget-ai-utils/plotter/{prompt-utils.d.ts → plotter-ai-utils.d.ts} +0 -0
  96. /package/dist/widget-ai-utils/python-program/{prompt-utils.d.ts → python-ai-utils.d.ts} +0 -0
  97. /package/dist/widget-ai-utils/radio/{prompt-utils.d.ts → radio-ai-utils.d.ts} +0 -0
  98. /package/dist/widget-ai-utils/sorter/{prompt-utils.d.ts → sorter-ai-utils.d.ts} +0 -0
  99. /package/dist/widget-ai-utils/video/{prompt-utils.d.ts → video-ai-utils.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -371,10 +371,11 @@ const mockStrings = {
371
371
  graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
372
372
  srPointAtCoordinates: _ref27 => {
373
373
  let {
374
+ num,
374
375
  x,
375
376
  y
376
377
  } = _ref27;
377
- return `Point at ${x} comma ${y}`;
378
+ return `Point ${num} at ${x} comma ${y}`;
378
379
  },
379
380
  srInteractiveElements: _ref28 => {
380
381
  let {
@@ -2712,7 +2713,7 @@ const keypadConfigurationForProps = widgetOptions => {
2712
2713
  times: widgetOptions.times
2713
2714
  };
2714
2715
  };
2715
- const propUpgrades$2 = {
2716
+ const propUpgrades$3 = {
2716
2717
  /* c8 ignore next */
2717
2718
  "1": v0props => ({
2718
2719
  times: v0props.times,
@@ -2772,7 +2773,7 @@ var Expression$1 = {
2772
2773
  major: 1,
2773
2774
  minor: 0
2774
2775
  },
2775
- propUpgrades: propUpgrades$2,
2776
+ propUpgrades: propUpgrades$3,
2776
2777
  // For use by the editor
2777
2778
  isLintable: true,
2778
2779
  scorer: scoreExpression,
@@ -18874,6 +18875,138 @@ class QuestionParagraph extends React__namespace.Component {
18874
18875
  }
18875
18876
  }
18876
18877
 
18878
+ // These are all the current widgets that require user-input and are supported for automatic scoring
18879
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
18880
+
18881
+ /**
18882
+ * Regex for widget placeholders in a string.
18883
+ *
18884
+ * First capture group is the widget ID (ex. 'radio 1')
18885
+ * Second capture group is the widget type (ex. "radio)
18886
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
18887
+ */
18888
+ function getWidgetRegex() {
18889
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
18890
+ }
18891
+
18892
+ /**
18893
+ * Add a widget placeholder using the widget ID.
18894
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
18895
+ *
18896
+ * @param {string} id
18897
+ * @returns {string}
18898
+ */
18899
+ function addWidget(id) {
18900
+ return `[[☃ ${id}]]`;
18901
+ }
18902
+
18903
+ /**
18904
+ * Get a widget type by a widget's ID
18905
+ * (please don't derive type from ID)
18906
+ *
18907
+ * @param {string} widgetId the ID of the widget
18908
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18909
+ * @returns {string} the widget type (ie "radio")
18910
+ */
18911
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
18912
+ const widget = widgetMap[WidgetId];
18913
+ return widget?.type ?? null;
18914
+ }
18915
+
18916
+ /**
18917
+ * Does the content have a specific type of widget?
18918
+ *
18919
+ * @param {string} type the type of the widget in question (ie "radio")
18920
+ * @param {string} content the string to search through
18921
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18922
+ * @returns {boolean} if the content includes the widget type
18923
+ */
18924
+ function contentHasWidgetType(type, content, widgetMap) {
18925
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
18926
+ }
18927
+
18928
+ /**
18929
+ * Extract all widget IDs, which includes the widget type and instance number.
18930
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
18931
+ *
18932
+ * Content should contain Perseus widget placeholders,
18933
+ * which look like: '[[☃ radio 1]]'.
18934
+ *
18935
+ * @param {string} content
18936
+ * @returns {ReadonlyArray<string>} widgetIds
18937
+ */
18938
+ function getWidgetIdsFromContent(content) {
18939
+ const widgets = [];
18940
+ const localWidgetRegex = getWidgetRegex();
18941
+ let match = localWidgetRegex.exec(content);
18942
+ while (match !== null) {
18943
+ widgets.push(match[1]);
18944
+ match = localWidgetRegex.exec(content);
18945
+ }
18946
+ return widgets;
18947
+ }
18948
+
18949
+ /**
18950
+ * Get a list of widget IDs from content,
18951
+ * but only for specific widget types
18952
+ *
18953
+ * @param {string} type the type of widget (ie "radio")
18954
+ * @param {string} content the string to parse
18955
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18956
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
18957
+ */
18958
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
18959
+ const rv = [];
18960
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
18961
+ widgetIdsInContent.forEach(widgetId => {
18962
+ const widget = widgetMap[widgetId];
18963
+ if (widget?.type === type) {
18964
+ rv.push(widgetId);
18965
+ }
18966
+ });
18967
+ return rv;
18968
+ }
18969
+
18970
+ /**
18971
+ * Pull the widget map out of ItemData
18972
+ *
18973
+ * @param {PerseusItem} PerseusItem containing a widgetMap
18974
+ * @returns {WidgetMap} the widget map in the PerseusItem
18975
+ */
18976
+ function getWidgetsMapFromItemData(itemData) {
18977
+ return itemData.question.widgets;
18978
+ }
18979
+
18980
+ /**
18981
+ * Get the widget information from a WidgetMap
18982
+ *
18983
+ * @param {string} widgetId the ID of the widget
18984
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
18985
+ */
18986
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
18987
+ return widgetMap[widgetId] ?? null;
18988
+ }
18989
+
18990
+ /**
18991
+ * Get select widgets from a widget map.
18992
+ * Useful for multi-items that needs to split
18993
+ * one PerseusItem into several
18994
+ *
18995
+ * @param {ReadonlyArray<string>} widgetIds to extract
18996
+ * @param {PerseusWidgetsMap} widgetMap to extract from
18997
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
18998
+ */
18999
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
19000
+ const widgets = {};
19001
+ widgetIds.forEach(widgetId => {
19002
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
19003
+ if (widget) {
19004
+ widgets[widgetId] = widget;
19005
+ }
19006
+ });
19007
+ return widgets;
19008
+ }
19009
+
18877
19010
  const DEFAULT_ALIGNMENT$1 = "block";
18878
19011
  // NOTE(kevinb): "default" is not one in `validAlignments`.
18879
19012
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -18924,6 +19057,9 @@ const replaceDeprecatedWidgets = () => {
18924
19057
  replaceWidget("sequence", "deprecated-standin");
18925
19058
  replaceWidget("simulator", "deprecated-standin");
18926
19059
  replaceWidget("unit-input", "deprecated-standin");
19060
+
19061
+ // Input-Number is a special case as it is being replaced by Numeric-Input
19062
+ replaceWidget("input-number", "numeric-input");
18927
19063
  };
18928
19064
  const registerEditors = editorsToRegister => {
18929
19065
  editorsToRegister.forEach(editor => {
@@ -18958,6 +19094,11 @@ const replaceDeprecatedEditors = () => {
18958
19094
  replaceEditor("sequence", "deprecated-standin");
18959
19095
  replaceEditor("simulator", "deprecated-standin");
18960
19096
  replaceEditor("unit-input", "deprecated-standin");
19097
+
19098
+ // We're replacing the input-number editor with the numeric-input editor here,
19099
+ // but we're also modifying the JSON content to convert input-number into the
19100
+ // numeric-input format over in editor-page.tsx on line 92.
19101
+ replaceEditor("input-number", "numeric-input");
18961
19102
  };
18962
19103
  const getWidget = name => {
18963
19104
  // TODO(alex): Consider referring to these as renderers to avoid
@@ -19004,10 +19145,8 @@ const getVersionVector = () => {
19004
19145
  const getPublicWidgets = () => {
19005
19146
  // TODO(alex): Update underscore.js so that _.pick can take a function.
19006
19147
  // @ts-expect-error - TS2740 - Type 'Pick<{ [key: string]: Readonly<{ name: string; displayName: string; getWidget?: (() => ComponentType<any>) | undefined; accessible?: boolean | ((props: any) => boolean) | undefined; hidden?: boolean | undefined; ... 10 more ...; widget: ComponentType<...>; }>; }, string>' is missing the following properties from type 'readonly Readonly<{ name: string; displayName: string; getWidget?: (() => ComponentType<any>) | undefined; accessible?: boolean | ((props: any) => boolean) | undefined; hidden?: boolean | undefined; ... 10 more ...; widget: ComponentType<...>; }>[]': length, concat, join, slice, and 18 more.
19007
- return ___default["default"].pick(widgets,
19008
- // @ts-expect-error - TS2345 - Argument of type '(name: string) => boolean | undefined' is not assignable to parameter of type 'Iteratee<string[], boolean, string>'.
19009
- ___default["default"].reject(___default["default"].keys(widgets), function (name) {
19010
- return widgets[name].hidden;
19148
+ return ___default["default"].pick(widgets, ___default["default"].reject(___default["default"].keys(widgets), function (name) {
19149
+ return widgets[name].hidden || name === "input-number";
19011
19150
  }));
19012
19151
  };
19013
19152
  const isAccessible = widgetInfo => {
@@ -19328,6 +19467,20 @@ widgetIds, userInputMap, strings, locale) {
19328
19467
  }
19329
19468
  });
19330
19469
  }
19470
+
19471
+ // TODO: combine scorePerseusItem with scoreWidgetsFunctional
19472
+ // once scorePerseusItem is the only one calling scoreWidgetsFunctional
19473
+ function scorePerseusItem(perseusRenderData, userInputMap,
19474
+ // TODO(LEMS-2461,LEMS-2391): these probably
19475
+ // need to be removed before we move this to the server
19476
+ strings, locale) {
19477
+ // There seems to be a chance that PerseusRenderer.widgets might include
19478
+ // widget data for widgets that are not in PerseusRenderer.content,
19479
+ // so this checks that the widgets are being used before scoring them
19480
+ const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
19481
+ const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
19482
+ return Util.flattenScores(scores);
19483
+ }
19331
19484
  function scoreWidgetsFunctional(widgets,
19332
19485
  // This is a port of old code, I'm not sure why
19333
19486
  // we need widgetIds vs the keys of the widgets object
@@ -19348,7 +19501,7 @@ widgetIds, userInputMap, strings, locale) {
19348
19501
  const userInput = userInputMap[id];
19349
19502
  const scorer = getWidgetScorer(widget.type);
19350
19503
  if (widget.type === "group") {
19351
- const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19504
+ const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
19352
19505
  widgetScores[id] = Util.flattenScores(scores);
19353
19506
  } else if (scorer) {
19354
19507
  widgetScores[id] = scorer(userInput, widget.options, strings, locale);
@@ -20397,7 +20550,7 @@ class Renderer extends React__namespace.Component {
20397
20550
  // Instead, we center them with css magic in styles.less
20398
20551
  // /cry(aria)
20399
20552
  this._foundTextNodes = true;
20400
- if (___default["default"].contains(this.widgetIds, node.id)) {
20553
+ if (this.widgetIds.includes(node.id)) {
20401
20554
  // We don't want to render a duplicate widget key/ref,
20402
20555
  // as this causes problems with react (for obvious
20403
20556
  // reasons). Instead we just notify the
@@ -20722,7 +20875,7 @@ class Renderer extends React__namespace.Component {
20722
20875
  };
20723
20876
  getInputPaths = () => {
20724
20877
  const inputPaths = [];
20725
- ___default["default"].each(this.widgetIds, widgetId => {
20878
+ this.widgetIds.forEach(widgetId => {
20726
20879
  const widget = this.getWidgetInstance(widgetId);
20727
20880
  if (widget && widget.getInputPaths) {
20728
20881
  // Grab all input paths and add widgetID to the front
@@ -20730,7 +20883,7 @@ class Renderer extends React__namespace.Component {
20730
20883
  // Prefix paths with their widgetID and add to collective
20731
20884
  // list of paths.
20732
20885
  // @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
20733
- ___default["default"].each(widgetInputPaths, inputPath => {
20886
+ widgetInputPaths.forEach(inputPath => {
20734
20887
  const relativeInputPath = [widgetId].concat(inputPath);
20735
20888
  inputPaths.push(relativeInputPath);
20736
20889
  });
@@ -20923,33 +21076,30 @@ class Renderer extends React__namespace.Component {
20923
21076
  }
20924
21077
 
20925
21078
  /**
20926
- * Returns an object mapping from widget ID to perseus-style score.
20927
- * The keys of this object are the values of the array returned
20928
- * from `getWidgetIds`.
20929
- */
20930
- scoreWidgets() {
20931
- return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20932
- }
20933
-
20934
- /**
20935
- * Grades the content.
21079
+ * Scores the content.
21080
+ *
21081
+ * @deprecated use scorePerseusItem
20936
21082
  */
20937
21083
  score() {
20938
- const scores = this.scoreWidgets();
21084
+ const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20939
21085
  const combinedScore = Util.flattenScores(scores);
20940
21086
  return combinedScore;
20941
21087
  }
20942
- guessAndScore() {
21088
+
21089
+ /**
21090
+ * @deprecated use scorePerseusItem
21091
+ */
21092
+ guessAndScore = () => {
20943
21093
  const totalGuess = this.getUserInput();
20944
21094
  const totalScore = this.score();
20945
21095
  return [totalGuess, totalScore];
20946
- }
21096
+ };
20947
21097
  examples = () => {
20948
21098
  const widgetIds = this.widgetIds;
20949
- const examples = ___default["default"].compact(___default["default"].map(widgetIds, widgetId => {
21099
+ const examples = widgetIds.map(widgetId => {
20950
21100
  const widget = this.getWidgetInstance(widgetId);
20951
21101
  return widget != null && widget.examples ? widget.examples() : null;
20952
- }));
21102
+ }).filter(Boolean);
20953
21103
 
20954
21104
  // no widgets with examples
20955
21105
  if (!examples.length) {
@@ -22377,7 +22527,7 @@ class NumericInput extends React__namespace.Component {
22377
22527
  height: "auto"
22378
22528
  }
22379
22529
  });
22380
- return /*#__PURE__*/React__namespace.createElement("div", null, /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
22530
+ return /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
22381
22531
  ref: ref => this.inputRef = ref,
22382
22532
  value: this.props.currentValue,
22383
22533
  onChange: this.handleChange,
@@ -22389,7 +22539,7 @@ class NumericInput extends React__namespace.Component {
22389
22539
  id: this.props.widgetId,
22390
22540
  disabled: this.props.apiOptions.readOnly,
22391
22541
  style: styles.input
22392
- }));
22542
+ });
22393
22543
  }
22394
22544
  }
22395
22545
 
@@ -22447,13 +22597,67 @@ const propsTransform = function (widgetOptions) {
22447
22597
  });
22448
22598
  return rendererProps;
22449
22599
  };
22600
+
22601
+ // This function is being used to replace the input-number widget
22602
+ // with the numeric-input widget
22603
+ const propUpgrades$2 = {
22604
+ /* c8 ignore next */
22605
+ "1": initialProps => {
22606
+ // If the initialProps has a value, it means we're upgrading from
22607
+ // input-number to numeric-input. In this case, we need to upgrade
22608
+ // the widget options accordingly.
22609
+ if (initialProps.value) {
22610
+ // If the answerType is not number or percent, we need to provide
22611
+ // the answer form for the numeric-input widget
22612
+ const provideAnswerForm = initialProps.answerType !== "number" && initialProps.answerType !== "percent";
22613
+
22614
+ // We need to determine the mathFormat for the numeric-input widget
22615
+ const mathFormat = initialProps.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
22616
+ : initialProps.answerType; // Otherwise, we can use the answerType directly
22617
+
22618
+ // If adjusting this logic, also adjust the logic in the convertInputNumberWidgetOptions
22619
+ // function in input-number.ts in the Perseus Editor package's util folder
22620
+ const answers = [{
22621
+ value: initialProps.value,
22622
+ simplify: initialProps.simplify,
22623
+ answerForms: provideAnswerForm ? [mathFormat] : undefined,
22624
+ strict: initialProps.inexact,
22625
+ // We only want to set maxError if the inexact prop is true
22626
+ maxError: initialProps.inexact ? initialProps.maxError : 0,
22627
+ status: "correct",
22628
+ // Input-number only allows correct answers
22629
+ message: ""
22630
+ }];
22631
+ return {
22632
+ answers,
22633
+ size: initialProps.size,
22634
+ coefficient: false,
22635
+ // input-number doesn't have a coefficient prop
22636
+ labelText: "",
22637
+ // input-number doesn't have a labelText prop
22638
+ static: false,
22639
+ // static is always false for numeric-input
22640
+ rightAlign: initialProps.rightAlign || false
22641
+ };
22642
+ } else {
22643
+ // Otherwise simply return the initialProps as there's no differences
22644
+ // between v0 and v1 for numeric-input
22645
+ return initialProps;
22646
+ }
22647
+ }
22648
+ };
22450
22649
  var NumericInput$1 = {
22451
22650
  name: "numeric-input",
22452
22651
  displayName: "Numeric input",
22453
22652
  defaultAlignment: "inline-block",
22454
22653
  accessible: true,
22455
22654
  widget: NumericInput,
22655
+ version: {
22656
+ major: 1,
22657
+ minor: 0
22658
+ },
22456
22659
  transform: propsTransform,
22660
+ propUpgrades: propUpgrades$2,
22457
22661
  isLintable: true,
22458
22662
  scorer: scoreNumericInput,
22459
22663
  getOneCorrectAnswerFromRubric(rubric) {
@@ -29470,6 +29674,9 @@ class Group extends React__namespace.Component {
29470
29674
  this.change("widgets", this.rendererRef.props.widgets);
29471
29675
  }
29472
29676
  };
29677
+
29678
+ // TODO(LEMS-2391): replace this when there's a separate check
29679
+ // for valid/invalid state
29473
29680
  const score = this.rendererRef?.score();
29474
29681
  const isValid = score && score.type !== "invalid";
29475
29682
  const isInvalid = score && score.type === "invalid";
@@ -31313,6 +31520,108 @@ function AxisArrows() {
31313
31520
  })));
31314
31521
  }
31315
31522
 
31523
+ /**
31524
+ * 44 is touch best practice and AAA compliant for WCAG
31525
+ * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
31526
+ */
31527
+ const TARGET_SIZE = 44;
31528
+ const REMOVE_BUTTON_ID = "perseus_mafs_remove_button";
31529
+
31530
+ // same as pointsFromNormalized in interactive-graph.tsx
31531
+ const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
31532
+ // maps over [x, y]
31533
+ coords.map((coord, i) => {
31534
+ const axisRange = range[i];
31535
+ if (noSnap) {
31536
+ return axisRange[MIN] + size(axisRange) * coord;
31537
+ }
31538
+ const axisStep = step[i];
31539
+ const nSteps = Math.floor(size(axisRange) / axisStep);
31540
+ const tick = Math.round(coord * nSteps);
31541
+ return axisRange[MIN] + axisStep * tick;
31542
+ }));
31543
+
31544
+ // same as normalizeCoords in interactive-graph.tsx
31545
+ const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
31546
+ return (coord + ranges[i][1]) / size(ranges[i]);
31547
+ }));
31548
+
31549
+ // Returns the closest point to the given `point` that is within the graph
31550
+ // bounds given in `state`.
31551
+ function bound$1(_ref) {
31552
+ let {
31553
+ snapStep,
31554
+ range,
31555
+ point
31556
+ } = _ref;
31557
+ const boundingBox = inset(snapStep, range);
31558
+ return clampToBox(boundingBox, point);
31559
+ }
31560
+ function isUnlimitedGraphState(state) {
31561
+ return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
31562
+ }
31563
+
31564
+ /**
31565
+ * Replace all text outside of the $ TeX blocks with `\\text{...}`
31566
+ * This way, the entire resulting string can be rendered within <TeX>
31567
+ * and the text outside of the $ blocks will be non-TeX text.
31568
+ */
31569
+ function replaceOutsideTeX(mathString) {
31570
+ // All the information we need is in the first section,
31571
+ // whether it's typed as "blockmath" or "paragraph"
31572
+ const firstSection = SimpleMarkdown__namespace.parse(mathString)[0];
31573
+
31574
+ // If it's blockMath, the outer level has the full math content.
31575
+ if (firstSection.type === "blockMath") {
31576
+ return firstSection.content;
31577
+ }
31578
+
31579
+ // If it's a paragraph, we need to iterate through the sections
31580
+ // to look for individual math blocks.
31581
+ const condensedNodes = condenseTextNodes(firstSection.content);
31582
+ let result = "";
31583
+ for (const piece of condensedNodes) {
31584
+ piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
31585
+ }
31586
+ return result;
31587
+ }
31588
+ // Helper function for replaceOutsideTeX()
31589
+ // Condense adjacent text nodes into a single text node
31590
+ function condenseTextNodes(nodes) {
31591
+ const result = [];
31592
+ if (!nodes) {
31593
+ return result;
31594
+ }
31595
+ let currentText = "";
31596
+ for (const node of nodes) {
31597
+ if (node.type === "math") {
31598
+ if (currentText) {
31599
+ result.push({
31600
+ type: "text",
31601
+ content: currentText
31602
+ });
31603
+ currentText = "";
31604
+ }
31605
+ result.push(node);
31606
+ } else {
31607
+ currentText += node.content;
31608
+ }
31609
+ }
31610
+ if (currentText) {
31611
+ result.push({
31612
+ type: "text",
31613
+ content: currentText
31614
+ });
31615
+ }
31616
+ return result;
31617
+ }
31618
+
31619
+ // Helper function for replaceOutsideTeX()
31620
+ function escapeSpecialChars(str) {
31621
+ // Escape $, \, {, and } characters
31622
+ return str.replace(/([$\\{}])/g, "\\$1");
31623
+ }
31624
+
31316
31625
  function AxisLabels() {
31317
31626
  const {
31318
31627
  range,
@@ -31344,7 +31653,7 @@ function AxisLabels() {
31344
31653
  fontSize: "14px",
31345
31654
  transform: "translate(7px, -50%)"
31346
31655
  }
31347
- }, /*#__PURE__*/React__default["default"].createElement(TeX, null, xAxisLabelText)), /*#__PURE__*/React__default["default"].createElement("span", {
31656
+ }, /*#__PURE__*/React__default["default"].createElement(TeX, null, replaceOutsideTeX(xAxisLabelText))), /*#__PURE__*/React__default["default"].createElement("span", {
31348
31657
  style: {
31349
31658
  position: "absolute",
31350
31659
  left: x2,
@@ -31352,7 +31661,7 @@ function AxisLabels() {
31352
31661
  fontSize: "14px",
31353
31662
  transform: "translate(-50%, 0px)"
31354
31663
  }
31355
- }, /*#__PURE__*/React__default["default"].createElement(TeX, null, yAxisLabelText)));
31664
+ }, /*#__PURE__*/React__default["default"].createElement(TeX, null, replaceOutsideTeX(yAxisLabelText))));
31356
31665
  }
31357
31666
 
31358
31667
  // The size of the ticks and labels in pixels
@@ -32259,17 +32568,12 @@ const ItemExtras = [
32259
32568
  // prettier-ignore
32260
32569
 
32261
32570
  // prettier-ignore
32262
-
32571
+ // While this widget is deprecated, we still need the type for conversion purposes
32263
32572
  // prettier-ignore
32264
-
32265
32573
  // prettier-ignore
32266
-
32267
32574
  // prettier-ignore
32268
-
32269
32575
  //prettier-ignore
32270
-
32271
32576
  // A background image applied to various widgets.
32272
-
32273
32577
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
32274
32578
 
32275
32579
  // 2D range: xMin, xMax, yMin, yMax
@@ -32295,108 +32599,6 @@ const lockedFigureFillStyles = {
32295
32599
 
32296
32600
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
32297
32601
 
32298
- /**
32299
- * 44 is touch best practice and AAA compliant for WCAG
32300
- * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
32301
- */
32302
- const TARGET_SIZE = 44;
32303
- const REMOVE_BUTTON_ID = "perseus_mafs_remove_button";
32304
-
32305
- // same as pointsFromNormalized in interactive-graph.tsx
32306
- const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
32307
- // maps over [x, y]
32308
- coords.map((coord, i) => {
32309
- const axisRange = range[i];
32310
- if (noSnap) {
32311
- return axisRange[MIN] + size(axisRange) * coord;
32312
- }
32313
- const axisStep = step[i];
32314
- const nSteps = Math.floor(size(axisRange) / axisStep);
32315
- const tick = Math.round(coord * nSteps);
32316
- return axisRange[MIN] + axisStep * tick;
32317
- }));
32318
-
32319
- // same as normalizeCoords in interactive-graph.tsx
32320
- const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
32321
- return (coord + ranges[i][1]) / size(ranges[i]);
32322
- }));
32323
-
32324
- // Returns the closest point to the given `point` that is within the graph
32325
- // bounds given in `state`.
32326
- function bound$1(_ref) {
32327
- let {
32328
- snapStep,
32329
- range,
32330
- point
32331
- } = _ref;
32332
- const boundingBox = inset(snapStep, range);
32333
- return clampToBox(boundingBox, point);
32334
- }
32335
- function isUnlimitedGraphState(state) {
32336
- return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
32337
- }
32338
-
32339
- /**
32340
- * Replace all text outside of the $ TeX blocks with `\\text{...}`
32341
- * This way, the entire resulting string can be rendered within <TeX>
32342
- * and the text outside of the $ blocks will be non-TeX text.
32343
- */
32344
- function replaceOutsideTeX(mathString) {
32345
- // All the information we need is in the first section,
32346
- // whether it's typed as "blockmath" or "paragraph"
32347
- const firstSection = SimpleMarkdown__namespace.parse(mathString)[0];
32348
-
32349
- // If it's blockMath, the outer level has the full math content.
32350
- if (firstSection.type === "blockMath") {
32351
- return firstSection.content;
32352
- }
32353
-
32354
- // If it's a paragraph, we need to iterate through the sections
32355
- // to look for individual math blocks.
32356
- const condensedNodes = condenseTextNodes(firstSection.content);
32357
- let result = "";
32358
- for (const piece of condensedNodes) {
32359
- piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
32360
- }
32361
- return result;
32362
- }
32363
- // Helper function for replaceOutsideTeX()
32364
- // Condense adjacent text nodes into a single text node
32365
- function condenseTextNodes(nodes) {
32366
- const result = [];
32367
- if (!nodes) {
32368
- return result;
32369
- }
32370
- let currentText = "";
32371
- for (const node of nodes) {
32372
- if (node.type === "math") {
32373
- if (currentText) {
32374
- result.push({
32375
- type: "text",
32376
- content: currentText
32377
- });
32378
- currentText = "";
32379
- }
32380
- result.push(node);
32381
- } else {
32382
- currentText += node.content;
32383
- }
32384
- }
32385
- if (currentText) {
32386
- result.push({
32387
- type: "text",
32388
- content: currentText
32389
- });
32390
- }
32391
- return result;
32392
- }
32393
-
32394
- // Helper function for replaceOutsideTeX()
32395
- function escapeSpecialChars(str) {
32396
- // Escape $, \, {, and } characters
32397
- return str.replace(/([$\\{}])/g, "\\$1");
32398
- }
32399
-
32400
32602
  function LockedLabel(props) {
32401
32603
  const {
32402
32604
  coord,
@@ -33625,6 +33827,7 @@ function useControlPoint(params) {
33625
33827
  } = useGraphConfig();
33626
33828
  const {
33627
33829
  point,
33830
+ sequenceNumber,
33628
33831
  color,
33629
33832
  cursor,
33630
33833
  constrain = p => snap(snapStep, p),
@@ -33665,6 +33868,7 @@ function useControlPoint(params) {
33665
33868
  ref: focusableHandleRef,
33666
33869
  role: "button",
33667
33870
  "aria-label": strings.srPointAtCoordinates({
33871
+ num: sequenceNumber,
33668
33872
  x: srFormatNumber(point[X], locale),
33669
33873
  y: srFormatNumber(point[Y], locale)
33670
33874
  })
@@ -33742,6 +33946,7 @@ const MovableLine = props => {
33742
33946
  focusableHandle: focusableHandle1
33743
33947
  } = useControlPoint({
33744
33948
  point: start,
33949
+ sequenceNumber: 1,
33745
33950
  color,
33746
33951
  onMove: p => onMovePoint(0, p)
33747
33952
  });
@@ -33750,6 +33955,7 @@ const MovableLine = props => {
33750
33955
  focusableHandle: focusableHandle2
33751
33956
  } = useControlPoint({
33752
33957
  point: end,
33958
+ sequenceNumber: 2,
33753
33959
  color,
33754
33960
  onMove: p => onMovePoint(1, p)
33755
33961
  });
@@ -33931,14 +34137,17 @@ function AngleGraph(props) {
33931
34137
  // Render the lines, angle, and then movable points
33932
34138
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, svgLines, /*#__PURE__*/React__namespace.createElement(Angle, angleParams), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33933
34139
  point: coords[1],
34140
+ sequenceNumber: 1,
33934
34141
  constrain: p => p,
33935
34142
  onMove: destination => dispatch(actions.angle.movePoint(1, destination))
33936
34143
  }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33937
34144
  point: coords[0],
34145
+ sequenceNumber: 2,
33938
34146
  constrain: getAngleSideConstraint(coords[0], coords[1], snapDegrees || 1),
33939
34147
  onMove: destination => dispatch(actions.angle.movePoint(0, destination))
33940
34148
  }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33941
34149
  point: coords[2],
34150
+ sequenceNumber: 3,
33942
34151
  constrain: getAngleSideConstraint(coords[2], coords[1], snapDegrees || 1),
33943
34152
  onMove: destination => dispatch(actions.angle.movePoint(2, destination))
33944
34153
  }));
@@ -34103,6 +34312,7 @@ function CircleGraph(props) {
34103
34312
  onMove: c => dispatch(actions.circle.moveCenter(c))
34104
34313
  }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34105
34314
  point: radiusPoint,
34315
+ sequenceNumber: 1,
34106
34316
  cursor: "ew-resize",
34107
34317
  onMove: newRadiusPoint => {
34108
34318
  dispatch(actions.circle.moveRadiusPoint(newRadiusPoint));
@@ -34278,6 +34488,7 @@ function LimitedPointGraph(props) {
34278
34488
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34279
34489
  key: i,
34280
34490
  point: point,
34491
+ sequenceNumber: i + 1,
34281
34492
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination))
34282
34493
  })));
34283
34494
  }
@@ -34319,6 +34530,7 @@ function UnlimitedPointGraph(props) {
34319
34530
  }), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34320
34531
  key: i,
34321
34532
  point: point,
34533
+ sequenceNumber: i + 1,
34322
34534
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
34323
34535
  ref: ref => {
34324
34536
  itemsRef.current[i] = ref;
@@ -34350,9 +34562,10 @@ function describePointGraph(state, i18n) {
34350
34562
  if (state.coords.length === 0) {
34351
34563
  return strings.srNoInteractiveElements;
34352
34564
  }
34353
- const pointDescriptions = state.coords.map(_ref2 => {
34565
+ const pointDescriptions = state.coords.map((_ref2, index) => {
34354
34566
  let [x, y] = _ref2;
34355
34567
  return strings.srPointAtCoordinates({
34568
+ num: index + 1,
34356
34569
  x: srFormatNumber(x, locale),
34357
34570
  y: srFormatNumber(y, locale)
34358
34571
  });
@@ -34472,6 +34685,7 @@ const LimitedPolygonGraph = props => {
34472
34685
  key: "point-" + i,
34473
34686
  constrain: constrain,
34474
34687
  point: point,
34688
+ sequenceNumber: i + 1,
34475
34689
  onMove: destination => {
34476
34690
  const now = Date.now();
34477
34691
  const targetFPS = 40;
@@ -34618,6 +34832,7 @@ const UnlimitedPolygonGraph = props => {
34618
34832
  }), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34619
34833
  key: i,
34620
34834
  point: point,
34835
+ sequenceNumber: i + 1,
34621
34836
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
34622
34837
  ref: ref => {
34623
34838
  pointRef.current[i] = ref;
@@ -34690,6 +34905,7 @@ function QuadraticGraph(props) {
34690
34905
  }), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34691
34906
  key: "point-" + i,
34692
34907
  point: coord,
34908
+ sequenceNumber: i + 1,
34693
34909
  onMove: destination => dispatch(actions.quadratic.movePoint(i, destination))
34694
34910
  })));
34695
34911
  }
@@ -34818,6 +35034,7 @@ function SinusoidGraph(props) {
34818
35034
  }), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34819
35035
  key: "point-" + i,
34820
35036
  point: coord,
35037
+ sequenceNumber: i + 1,
34821
35038
  onMove: destination => dispatch(actions.sinusoid.movePoint(i, destination))
34822
35039
  })));
34823
35040
  }
@@ -44986,7 +45203,7 @@ const init = function () {
44986
45203
 
44987
45204
  // This file is processed by a Rollup plugin (replace) to inject the production
44988
45205
  const libName = "@khanacademy/perseus";
44989
- const libVersion = "41.4.0";
45206
+ const libVersion = "42.0.0";
44990
45207
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
44991
45208
 
44992
45209
  const apiVersion = {
@@ -45591,6 +45808,13 @@ class ServerItemRenderer extends React__namespace.Component {
45591
45808
  this.props.onRendered(true);
45592
45809
  }
45593
45810
  }
45811
+ if (this.props.score && this.props.score !== prevProps.score) {
45812
+ const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45813
+ this.setState({
45814
+ questionCompleted: this.props.score.correct,
45815
+ questionHighlightedWidgets: emptyQuestionAreaWidgets
45816
+ });
45817
+ }
45594
45818
  }
45595
45819
  componentWillUnmount() {
45596
45820
  if (this.blurTimeoutID != null) {
@@ -45679,7 +45903,7 @@ class ServerItemRenderer extends React__namespace.Component {
45679
45903
 
45680
45904
  /**
45681
45905
  * Accepts a question area widgetId, or an answer area widgetId of
45682
- * the form "answer-input-number 1", or the string "answer-area"
45906
+ * the form "answer-numeric-input 1", or the string "answer-area"
45683
45907
  * for the whole answer area (if the answer area is a single widget).
45684
45908
  */
45685
45909
  _setWidgetProps(widgetId, newProps, callback) {
@@ -45726,18 +45950,37 @@ class ServerItemRenderer extends React__namespace.Component {
45726
45950
  return this.questionRenderer.getPromptJSON();
45727
45951
  }
45728
45952
 
45953
+ /**
45954
+ * Returns an array of the widget `.getUserInput()` results
45955
+ *
45956
+ * TODO: can we remove this? Seems to be just for backwards
45957
+ * compatibility with old Perseus Chrome logging
45958
+ * @deprecated use getUserInput
45959
+ */
45960
+ getUserInputLegacy() {
45961
+ return this.questionRenderer.getUserInput();
45962
+ }
45963
+
45964
+ /**
45965
+ * Returns an object of the widget `.getUserInput()` results
45966
+ */
45967
+ getUserInput() {
45968
+ return this.questionRenderer.getUserInputMap();
45969
+ }
45970
+
45729
45971
  /**
45730
45972
  * Grades the item.
45973
+ *
45974
+ * @deprecated use scorePerseusItem
45731
45975
  */
45732
45976
  scoreInput() {
45733
- const guessAndScore = this.questionRenderer.guessAndScore();
45734
- const guess = guessAndScore[0];
45735
- const score = guessAndScore[1];
45977
+ const guess = this.getUserInput();
45978
+ const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
45736
45979
 
45737
45980
  // Continue to include an empty guess for the now defunct answer area.
45738
45981
  // TODO(alex): Check whether we rely on the format here for
45739
45982
  // analyzing ProblemLogs. If not, remove this layer.
45740
- const maxCompatGuess = [guess, []];
45983
+ const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
45741
45984
  const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
45742
45985
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45743
45986
  this.setState({
@@ -47599,138 +47842,6 @@ const registerAllWidgetsForTesting = () => {
47599
47842
  replaceDeprecatedWidgets();
47600
47843
  };
47601
47844
 
47602
- // These are all the current widgets that require user-input and are supported for automatic scoring
47603
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
47604
-
47605
- /**
47606
- * Regex for widget placeholders in a string.
47607
- *
47608
- * First capture group is the widget ID (ex. 'radio 1')
47609
- * Second capture group is the widget type (ex. "radio)
47610
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
47611
- */
47612
- function getWidgetRegex() {
47613
- return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
47614
- }
47615
-
47616
- /**
47617
- * Add a widget placeholder using the widget ID.
47618
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
47619
- *
47620
- * @param {string} id
47621
- * @returns {string}
47622
- */
47623
- function addWidget(id) {
47624
- return `[[☃ ${id}]]`;
47625
- }
47626
-
47627
- /**
47628
- * Get a widget type by a widget's ID
47629
- * (please don't derive type from ID)
47630
- *
47631
- * @param {string} widgetId the ID of the widget
47632
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47633
- * @returns {string} the widget type (ie "radio")
47634
- */
47635
- function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
47636
- const widget = widgetMap[WidgetId];
47637
- return widget?.type ?? null;
47638
- }
47639
-
47640
- /**
47641
- * Does the content have a specific type of widget?
47642
- *
47643
- * @param {string} type the type of the widget in question (ie "radio")
47644
- * @param {string} content the string to search through
47645
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47646
- * @returns {boolean} if the content includes the widget type
47647
- */
47648
- function contentHasWidgetType(type, content, widgetMap) {
47649
- return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
47650
- }
47651
-
47652
- /**
47653
- * Extract all widget IDs, which includes the widget type and instance number.
47654
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
47655
- *
47656
- * Content should contain Perseus widget placeholders,
47657
- * which look like: '[[☃ radio 1]]'.
47658
- *
47659
- * @param {string} content
47660
- * @returns {ReadonlyArray<string>} widgetIds
47661
- */
47662
- function getWidgetIdsFromContent(content) {
47663
- const widgets = [];
47664
- const localWidgetRegex = getWidgetRegex();
47665
- let match = localWidgetRegex.exec(content);
47666
- while (match !== null) {
47667
- widgets.push(match[1]);
47668
- match = localWidgetRegex.exec(content);
47669
- }
47670
- return widgets;
47671
- }
47672
-
47673
- /**
47674
- * Get a list of widget IDs from content,
47675
- * but only for specific widget types
47676
- *
47677
- * @param {string} type the type of widget (ie "radio")
47678
- * @param {string} content the string to parse
47679
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47680
- * @returns {ReadonlyArray<string>} the widget type (ie "radio")
47681
- */
47682
- function getWidgetIdsFromContentByType(type, content, widgetMap) {
47683
- const rv = [];
47684
- const widgetIdsInContent = getWidgetIdsFromContent(content);
47685
- widgetIdsInContent.forEach(widgetId => {
47686
- const widget = widgetMap[widgetId];
47687
- if (widget?.type === type) {
47688
- rv.push(widgetId);
47689
- }
47690
- });
47691
- return rv;
47692
- }
47693
-
47694
- /**
47695
- * Pull the widget map out of ItemData
47696
- *
47697
- * @param {PerseusItem} PerseusItem containing a widgetMap
47698
- * @returns {WidgetMap} the widget map in the PerseusItem
47699
- */
47700
- function getWidgetsMapFromItemData(itemData) {
47701
- return itemData.question.widgets;
47702
- }
47703
-
47704
- /**
47705
- * Get the widget information from a WidgetMap
47706
- *
47707
- * @param {string} widgetId the ID of the widget
47708
- * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
47709
- */
47710
- function getWidgetFromWidgetMap(widgetId, widgetMap) {
47711
- return widgetMap[widgetId] ?? null;
47712
- }
47713
-
47714
- /**
47715
- * Get select widgets from a widget map.
47716
- * Useful for multi-items that needs to split
47717
- * one PerseusItem into several
47718
- *
47719
- * @param {ReadonlyArray<string>} widgetIds to extract
47720
- * @param {PerseusWidgetsMap} widgetMap to extract from
47721
- * @return {PerseusWidgetsMap} a new widget map with requested widgets
47722
- */
47723
- function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
47724
- const widgets = {};
47725
- widgetIds.forEach(widgetId => {
47726
- const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
47727
- if (widget) {
47728
- widgets[widgetId] = widget;
47729
- }
47730
- });
47731
- return widgets;
47732
- }
47733
-
47734
47845
  /**
47735
47846
  * This function extracts the answers from the widgets.
47736
47847
  *
@@ -48663,6 +48774,118 @@ const parseExpressionWidget = parseWidget(constant("expression"), object({
48663
48774
  buttonsVisible: optional(enumeration("always", "never", "focused"))
48664
48775
  }));
48665
48776
 
48777
+ const parsePerseusRenderer = object({
48778
+ content: string,
48779
+ // This module has an import cycle with parseWidgetsMap, because the
48780
+ // `group` widget can contain another renderer.
48781
+ // The anonymous function below ensures that we don't try to access
48782
+ // parseWidgetsMap before it's defined.
48783
+ widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
48784
+ metadata: optional(array(string)),
48785
+ images: record(string, object({
48786
+ width: number,
48787
+ height: number
48788
+ }))
48789
+ });
48790
+
48791
+ const parseGradedGroupWidgetOptions = object({
48792
+ title: string,
48793
+ hasHint: optional(nullable(boolean)),
48794
+ // This module has an import cycle with parsePerseusRenderer.
48795
+ // The anonymous function below ensures that we don't try to access
48796
+ // parsePerseusRenderer before it's defined.
48797
+ hint: optional(nullable((rawVal, ctx) => parsePerseusRenderer(rawVal, ctx))),
48798
+ content: string,
48799
+ // This module has an import cycle with parseWidgetsMap.
48800
+ // The anonymous function below ensures that we don't try to access
48801
+ // parseWidgetsMap before it's defined.
48802
+ widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
48803
+ widgetEnabled: optional(nullable(boolean)),
48804
+ immutableWidgets: optional(nullable(boolean)),
48805
+ images: record(string, object({
48806
+ width: number,
48807
+ height: number
48808
+ }))
48809
+ });
48810
+ const parseGradedGroupWidget = parseWidget(constant("graded-group"), parseGradedGroupWidgetOptions);
48811
+
48812
+ const parseGradedGroupSetWidget = parseWidget(constant("graded-group-set"), object({
48813
+ gradedGroups: array(parseGradedGroupWidgetOptions)
48814
+ }));
48815
+
48816
+ const pairOfNumbers$3 = pair(number, number);
48817
+ const pairOfPoints = pair(pairOfNumbers$3, pairOfNumbers$3);
48818
+ const parseGrapherWidget = parseWidget(constant("grapher"), object({
48819
+ availableTypes: array(enumeration("absolute_value", "exponential", "linear", "logarithm", "quadratic", "sinusoid", "tangent")),
48820
+ correct: union(object({
48821
+ type: constant("absolute_value"),
48822
+ coords: pairOfPoints
48823
+ })).or(object({
48824
+ type: constant("exponential"),
48825
+ asymptote: pairOfPoints,
48826
+ coords: pairOfPoints
48827
+ })).or(object({
48828
+ type: constant("linear"),
48829
+ coords: pairOfPoints
48830
+ })).or(object({
48831
+ type: constant("logarithm"),
48832
+ asymptote: pairOfPoints,
48833
+ coords: pairOfPoints
48834
+ })).or(object({
48835
+ type: constant("quadratic"),
48836
+ coords: pairOfPoints
48837
+ })).or(object({
48838
+ type: constant("sinusoid"),
48839
+ coords: pairOfPoints
48840
+ })).or(object({
48841
+ type: constant("tangent"),
48842
+ coords: pairOfPoints
48843
+ })).parser,
48844
+ graph: object({
48845
+ backgroundImage: object({
48846
+ bottom: optional(number),
48847
+ height: optional(number),
48848
+ left: optional(number),
48849
+ scale: optional(number),
48850
+ url: optional(nullable(string)),
48851
+ width: optional(number)
48852
+ }),
48853
+ box: optional(pairOfNumbers$3),
48854
+ editableSettings: optional(array(enumeration("graph", "snap", "image", "measure"))),
48855
+ gridStep: optional(pairOfNumbers$3),
48856
+ labels: pair(string, string),
48857
+ markings: enumeration("graph", "none", "grid"),
48858
+ range: pair(pairOfNumbers$3, pairOfNumbers$3),
48859
+ rulerLabel: constant(""),
48860
+ rulerTicks: number,
48861
+ showProtractor: optional(boolean),
48862
+ showRuler: optional(boolean),
48863
+ showTooltips: optional(boolean),
48864
+ snapStep: optional(pairOfNumbers$3),
48865
+ step: pairOfNumbers$3,
48866
+ valid: optional(union(boolean).or(string).parser)
48867
+ })
48868
+ }));
48869
+
48870
+ const parseGroupWidget = parseWidget(constant("group"),
48871
+ // This module has an import cycle with parsePerseusRenderer.
48872
+ // The anonymous function below ensures that we don't try to access
48873
+ // parsePerseusRenderer before it's defined.
48874
+ (rawVal, ctx) => parsePerseusRenderer(rawVal, ctx));
48875
+
48876
+ const parseIframeWidget = parseWidget(constant("iframe"), object({
48877
+ url: string,
48878
+ settings: array(object({
48879
+ name: string,
48880
+ value: string
48881
+ })),
48882
+ width: union(number).or(string).parser,
48883
+ height: union(number).or(string).parser,
48884
+ allowFullScreen: boolean,
48885
+ allowTopNavigation: optional(boolean),
48886
+ static: boolean
48887
+ }));
48888
+
48666
48889
  const parsePerseusImageBackground = object({
48667
48890
  url: optional(nullable(string)),
48668
48891
  width: optional(number),
@@ -48674,6 +48897,160 @@ const parsePerseusImageBackground = object({
48674
48897
  scale: optional(union(number).or(string).parser)
48675
48898
  });
48676
48899
 
48900
+ const pairOfNumbers$2 = pair(number, number);
48901
+ const parseImageWidget = parseWidget(constant("image"), object({
48902
+ title: optional(string),
48903
+ caption: optional(string),
48904
+ alt: optional(string),
48905
+ backgroundImage: parsePerseusImageBackground,
48906
+ static: optional(boolean),
48907
+ labels: optional(array(object({
48908
+ content: string,
48909
+ alignment: string,
48910
+ coordinates: array(number)
48911
+ }))),
48912
+ range: optional(pair(pairOfNumbers$2, pairOfNumbers$2)),
48913
+ box: optional(pairOfNumbers$2)
48914
+ }));
48915
+
48916
+ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
48917
+ answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
48918
+ inexact: optional(boolean),
48919
+ maxError: optional(union(number).or(string).parser),
48920
+ rightAlign: optional(boolean),
48921
+ simplify: enumeration("required", "optional", "enforced"),
48922
+ size: enumeration("normal", "small"),
48923
+ value: union(number).or(string).parser,
48924
+ customKeypad: optional(boolean)
48925
+ }));
48926
+
48927
+ const pairOfNumbers$1 = pair(number, number);
48928
+ const parseFunctionElement = object({
48929
+ type: constant("function"),
48930
+ key: string,
48931
+ options: object({
48932
+ value: string,
48933
+ funcName: string,
48934
+ rangeMin: string,
48935
+ rangeMax: string,
48936
+ color: string,
48937
+ strokeDasharray: string,
48938
+ strokeWidth: number
48939
+ })
48940
+ });
48941
+ const parseLabelElement = object({
48942
+ type: constant("label"),
48943
+ key: string,
48944
+ options: object({
48945
+ label: string,
48946
+ color: string,
48947
+ coordX: string,
48948
+ coordY: string
48949
+ })
48950
+ });
48951
+ const parseLineElement = object({
48952
+ type: constant("line"),
48953
+ key: string,
48954
+ options: object({
48955
+ color: string,
48956
+ startX: string,
48957
+ startY: string,
48958
+ endX: string,
48959
+ endY: string,
48960
+ strokeDasharray: string,
48961
+ strokeWidth: number,
48962
+ arrows: string
48963
+ })
48964
+ });
48965
+ const parseMovableLineElement = object({
48966
+ type: constant("movable-line"),
48967
+ key: string,
48968
+ options: object({
48969
+ startX: string,
48970
+ startY: string,
48971
+ startSubscript: number,
48972
+ endX: string,
48973
+ endY: string,
48974
+ endSubscript: number,
48975
+ constraint: string,
48976
+ snap: number,
48977
+ constraintFn: string,
48978
+ constraintXMin: string,
48979
+ constraintXMax: string,
48980
+ constraintYMin: string,
48981
+ constraintYMax: string
48982
+ })
48983
+ });
48984
+ const parseMovablePointElement = object({
48985
+ type: constant("movable-point"),
48986
+ key: string,
48987
+ options: object({
48988
+ startX: string,
48989
+ startY: string,
48990
+ varSubscript: number,
48991
+ constraint: string,
48992
+ snap: number,
48993
+ constraintFn: string,
48994
+ constraintXMin: string,
48995
+ constraintXMax: string,
48996
+ constraintYMin: string,
48997
+ constraintYMax: string
48998
+ })
48999
+ });
49000
+ const parseParametricElement = object({
49001
+ type: constant("parametric"),
49002
+ key: string,
49003
+ options: object({
49004
+ x: string,
49005
+ y: string,
49006
+ rangeMin: string,
49007
+ rangeMax: string,
49008
+ color: string,
49009
+ strokeDasharray: string,
49010
+ strokeWidth: number
49011
+ })
49012
+ });
49013
+ const parsePointElement = object({
49014
+ type: constant("point"),
49015
+ key: string,
49016
+ options: object({
49017
+ color: string,
49018
+ coordX: string,
49019
+ coordY: string
49020
+ })
49021
+ });
49022
+ const parseRectangleElement = object({
49023
+ type: constant("rectangle"),
49024
+ key: string,
49025
+ options: object({
49026
+ color: string,
49027
+ coordX: string,
49028
+ coordY: string,
49029
+ width: string,
49030
+ height: string
49031
+ })
49032
+ });
49033
+ const parseInteractionWidget = parseWidget(constant("interaction"), object({
49034
+ static: boolean,
49035
+ graph: object({
49036
+ editableSettings: optional(array(enumeration("canvas", "graph"))),
49037
+ box: pairOfNumbers$1,
49038
+ labels: array(string),
49039
+ range: pair(pairOfNumbers$1, pairOfNumbers$1),
49040
+ gridStep: pairOfNumbers$1,
49041
+ markings: enumeration("graph", "grid", "none"),
49042
+ snapStep: optional(pairOfNumbers$1),
49043
+ valid: optional(union(boolean).or(string).parser),
49044
+ backgroundImage: optional(parsePerseusImageBackground),
49045
+ showProtractor: optional(boolean),
49046
+ showRuler: optional(boolean),
49047
+ rulerLabel: optional(string),
49048
+ rulerTicks: optional(number),
49049
+ tickStep: pairOfNumbers$1
49050
+ }),
49051
+ elements: array(union(parseFunctionElement).or(parseLabelElement).or(parseLineElement).or(parseMovableLineElement).or(parseMovablePointElement).or(parseParametricElement).or(parsePointElement).or(parseRectangleElement).parser)
49052
+ }));
49053
+
48677
49054
  // Used to represent 2-D points and ranges
48678
49055
  const pairOfNumbers = pair(number, number);
48679
49056
  const parsePerseusGraphTypeAngle = object({
@@ -48860,6 +49237,195 @@ const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), o
48860
49237
  fullGraphAriaDescription: optional(string)
48861
49238
  }));
48862
49239
 
49240
+ const parseLabelImageWidget = parseWidget(constant("label-image"), object({
49241
+ choices: array(string),
49242
+ imageUrl: string,
49243
+ imageAlt: string,
49244
+ imageHeight: number,
49245
+ imageWidth: number,
49246
+ markers: array(object({
49247
+ answers: array(string),
49248
+ label: string,
49249
+ x: number,
49250
+ y: number
49251
+ })),
49252
+ hideChoicesFromInstructions: boolean,
49253
+ multipleAnswers: boolean,
49254
+ static: boolean
49255
+ }));
49256
+
49257
+ const parseMatcherWidget = parseWidget(constant("matcher"), object({
49258
+ labels: array(string),
49259
+ left: array(string),
49260
+ right: array(string),
49261
+ orderMatters: boolean,
49262
+ padding: boolean
49263
+ }));
49264
+
49265
+ const parseMatrixWidget = parseWidget(constant("matrix"), object({
49266
+ prefix: string,
49267
+ suffix: string,
49268
+ answers: array(array(number)),
49269
+ cursorPosition: array(number),
49270
+ matrixBoardSize: array(number),
49271
+ static: boolean
49272
+ }));
49273
+
49274
+ const parseMeasurerWidget = parseWidget(constant("measurer"), object({
49275
+ image: parsePerseusImageBackground,
49276
+ showProtractor: boolean,
49277
+ showRuler: boolean,
49278
+ rulerLabel: string,
49279
+ rulerTicks: number,
49280
+ rulerPixels: number,
49281
+ rulerLength: number,
49282
+ box: pair(number, number),
49283
+ static: boolean
49284
+ }));
49285
+
49286
+ const parseMoleculeRendererWidget = parseWidget(constant("molecule-renderer"), object({
49287
+ widgetId: string,
49288
+ rotationAngle: optional(number),
49289
+ smiles: optional(string)
49290
+ }));
49291
+
49292
+ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
49293
+ range: array(number),
49294
+ labelRange: array(nullable(number)),
49295
+ labelStyle: string,
49296
+ labelTicks: boolean,
49297
+ isTickCtrl: optional(nullable(boolean)),
49298
+ divisionRange: array(number),
49299
+ numDivisions: optional(nullable(number)),
49300
+ snapDivisions: number,
49301
+ tickStep: optional(nullable(number)),
49302
+ correctRel: optional(nullable(string)),
49303
+ correctX: number,
49304
+ initialX: optional(nullable(number)),
49305
+ showTooltip: optional(boolean),
49306
+ static: boolean
49307
+ }));
49308
+
49309
+ const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "decimal", "percent", "pi");
49310
+ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
49311
+ answers: array(object({
49312
+ message: string,
49313
+ value: number,
49314
+ status: string,
49315
+ answerForms: optional(array(parseMathFormat)),
49316
+ strict: boolean,
49317
+ maxError: optional(nullable(number)),
49318
+ simplify: optional(nullable(string))
49319
+ })),
49320
+ labelText: string,
49321
+ size: string,
49322
+ coefficient: boolean,
49323
+ rightAlign: optional(boolean),
49324
+ static: boolean,
49325
+ answerForms: optional(array(object({
49326
+ name: parseMathFormat,
49327
+ simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
49328
+ })))
49329
+ }));
49330
+
49331
+ // There is an import cycle between orderer-widget.ts and perseus-renderer.ts.
49332
+ // This wrapper ensures that we don't refer to parsePerseusRenderer before
49333
+ // it's defined.
49334
+ function parseRenderer(rawValue, ctx) {
49335
+ return parsePerseusRenderer(rawValue, ctx);
49336
+ }
49337
+ const parseOrdererWidget = parseWidget(constant("orderer"), object({
49338
+ options: array(parseRenderer),
49339
+ correctOptions: array(parseRenderer),
49340
+ otherOptions: array(parseRenderer),
49341
+ height: string,
49342
+ layout: string
49343
+ }));
49344
+
49345
+ const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
49346
+ passageNumber: number,
49347
+ referenceNumber: number,
49348
+ summaryText: string
49349
+ }));
49350
+
49351
+ const parsePassageWidget = parseWidget(constant("passage"), object({
49352
+ footnotes: string,
49353
+ passageText: string,
49354
+ passageTitle: string,
49355
+ showLineNumbers: boolean,
49356
+ static: boolean
49357
+ }));
49358
+
49359
+ const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
49360
+ url: string,
49361
+ description: string
49362
+ }));
49363
+
49364
+ const parsePlotterWidget = parseWidget(constant("plotter"), object({
49365
+ labels: array(string),
49366
+ categories: array(string),
49367
+ type: enumeration(...plotterPlotTypes),
49368
+ maxY: number,
49369
+ scaleY: number,
49370
+ labelInterval: optional(nullable(number)),
49371
+ snapsPerLine: number,
49372
+ starting: array(number),
49373
+ correct: array(number),
49374
+ picUrl: optional(nullable(string)),
49375
+ picSize: optional(nullable(number)),
49376
+ picBoxHeight: optional(nullable(number)),
49377
+ plotDimensions: array(number)
49378
+ }));
49379
+
49380
+ const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
49381
+ programID: string,
49382
+ height: number
49383
+ }));
49384
+
49385
+ const parseRadioWidget = parseWidget(constant("radio"), object({
49386
+ choices: array(object({
49387
+ content: string,
49388
+ clue: optional(string),
49389
+ correct: optional(boolean),
49390
+ isNoneOfTheAbove: optional(boolean),
49391
+ // deprecated
49392
+ // There is an import cycle between radio-widget.ts and
49393
+ // widgets-map.ts. The anonymous function below ensures that we
49394
+ // don't refer to parseWidgetsMap before it's defined.
49395
+ widgets: optional((rawVal, ctx) => parseWidgetsMap(rawVal, ctx))
49396
+ })),
49397
+ hasNoneOfTheAbove: optional(boolean),
49398
+ countChoices: optional(boolean),
49399
+ randomize: optional(boolean),
49400
+ multipleSelect: optional(boolean),
49401
+ deselectEnabled: optional(boolean),
49402
+ // deprecated
49403
+ onePerLine: optional(boolean),
49404
+ // deprecated
49405
+ displayCount: optional(any),
49406
+ // v0 props
49407
+ // `noneOfTheAbove` is still in use (but only set to `false`).
49408
+ noneOfTheAbove: constant(false)
49409
+ }));
49410
+
49411
+ const parseSorterWidget = parseWidget(constant("sorter"), object({
49412
+ correct: array(string),
49413
+ padding: boolean,
49414
+ layout: enumeration("horizontal", "vertical")
49415
+ }));
49416
+
49417
+ const parseTableWidget = parseWidget(constant("table"), object({
49418
+ headers: array(string),
49419
+ rows: number,
49420
+ columns: number,
49421
+ answers: array(array(string))
49422
+ }));
49423
+
49424
+ const parseVideoWidget = parseWidget(constant("video"), object({
49425
+ location: string,
49426
+ static: optional(boolean)
49427
+ }));
49428
+
48863
49429
  const parseWidgetsMap = (rawValue, ctx) => {
48864
49430
  if (!isObject(rawValue)) {
48865
49431
  return ctx.failure("PerseusWidgetsMap", rawValue);
@@ -48905,86 +49471,67 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
48905
49471
  case "expression":
48906
49472
  return parseAndAssign(`expression ${id}`, parseExpressionWidget);
48907
49473
  case "grapher":
48908
- // TODO(LEMS-2585): implement a real parser for this widget
48909
- return parseAndAssign(`grapher ${id}`, any);
49474
+ return parseAndAssign(`grapher ${id}`, parseGrapherWidget);
48910
49475
  case "group":
48911
- // TODO(LEMS-2585): implement a real parser for this widget
48912
- return parseAndAssign(`group ${id}`, any);
49476
+ return parseAndAssign(`group ${id}`, parseGroupWidget);
48913
49477
  case "graded-group":
48914
- // TODO(LEMS-2585): implement a real parser for this widget
48915
- return parseAndAssign(`graded-group ${id}`, any);
49478
+ return parseAndAssign(`graded-group ${id}`, parseGradedGroupWidget);
48916
49479
  case "graded-group-set":
48917
- // TODO(LEMS-2585): implement a real parser for this widget
48918
- return parseAndAssign(`graded-group-set ${id}`, any);
49480
+ return parseAndAssign(`graded-group-set ${id}`, parseGradedGroupSetWidget);
48919
49481
  case "iframe":
48920
- // TODO(LEMS-2585): implement a real parser for this widget
48921
- return parseAndAssign(`iframe ${id}`, any);
49482
+ return parseAndAssign(`iframe ${id}`, parseIframeWidget);
48922
49483
  case "image":
48923
- // TODO(LEMS-2585): implement a real parser for this widget
48924
- return parseAndAssign(`image ${id}`, any);
49484
+ return parseAndAssign(`image ${id}`, parseImageWidget);
48925
49485
  case "input-number":
48926
- // TODO(LEMS-2585): implement a real parser for this widget
48927
- return parseAndAssign(`input-number ${id}`, any);
49486
+ return parseAndAssign(`input-number ${id}`, parseInputNumberWidget);
48928
49487
  case "interaction":
48929
- // TODO(LEMS-2585): implement a real parser for this widget
48930
- return parseAndAssign(`interaction ${id}`, any);
49488
+ return parseAndAssign(`interaction ${id}`, parseInteractionWidget);
48931
49489
  case "interactive-graph":
48932
49490
  return parseAndAssign(`interactive-graph ${id}`, parseInteractiveGraphWidget);
48933
49491
  case "label-image":
48934
- // TODO(LEMS-2585): implement a real parser for this widget
48935
- return parseAndAssign(`label-image ${id}`, any);
49492
+ return parseAndAssign(`label-image ${id}`, parseLabelImageWidget);
48936
49493
  case "matcher":
48937
- // TODO(LEMS-2585): implement a real parser for this widget
48938
- return parseAndAssign(`matcher ${id}`, any);
49494
+ return parseAndAssign(`matcher ${id}`, parseMatcherWidget);
48939
49495
  case "matrix":
48940
- // TODO(LEMS-2585): implement a real parser for this widget
48941
- return parseAndAssign(`matrix ${id}`, any);
49496
+ return parseAndAssign(`matrix ${id}`, parseMatrixWidget);
48942
49497
  case "measurer":
48943
- // TODO(LEMS-2585): implement a real parser for this widget
48944
- return parseAndAssign(`measurer ${id}`, any);
49498
+ return parseAndAssign(`measurer ${id}`, parseMeasurerWidget);
48945
49499
  case "molecule-renderer":
48946
- // TODO(LEMS-2585): implement a real parser for this widget
48947
- return parseAndAssign(`molecule-renderer ${id}`, any);
49500
+ return parseAndAssign(`molecule-renderer ${id}`, parseMoleculeRendererWidget);
48948
49501
  case "number-line":
48949
- // TODO(LEMS-2585): implement a real parser for this widget
48950
- return parseAndAssign(`number-line ${id}`, any);
49502
+ return parseAndAssign(`number-line ${id}`, parseNumberLineWidget);
48951
49503
  case "numeric-input":
48952
- // TODO(LEMS-2585): implement a real parser for this widget
48953
- return parseAndAssign(`numeric-input ${id}`, any);
49504
+ return parseAndAssign(`numeric-input ${id}`, parseNumericInputWidget);
48954
49505
  case "orderer":
48955
- // TODO(LEMS-2585): implement a real parser for this widget
48956
- return parseAndAssign(`orderer ${id}`, any);
49506
+ return parseAndAssign(`orderer ${id}`, parseOrdererWidget);
48957
49507
  case "passage":
48958
- // TODO(LEMS-2585): implement a real parser for this widget
48959
- return parseAndAssign(`passage ${id}`, any);
49508
+ return parseAndAssign(`passage ${id}`, parsePassageWidget);
48960
49509
  case "passage-ref":
48961
- // TODO(LEMS-2585): implement a real parser for this widget
48962
- return parseAndAssign(`passage-ref ${id}`, any);
49510
+ return parseAndAssign(`passage-ref ${id}`, parsePassageRefWidget);
48963
49511
  case "passage-ref-target":
48964
- // TODO(LEMS-2585): implement a real parser for this widget
49512
+ // NOTE(benchristel): as of 2024-11-12, passage-ref-target is only
49513
+ // used in test content. See:
49514
+ // https://www.khanacademy.org/devadmin/content/search?query=widget:passage-ref-target
48965
49515
  return parseAndAssign(`passage-ref-target ${id}`, any);
48966
49516
  case "phet-simulation":
48967
- // TODO(LEMS-2585): implement a real parser for this widget
48968
- return parseAndAssign(`phet-simulation ${id}`, any);
49517
+ return parseAndAssign(`phet-simulation ${id}`, parsePhetSimulationWidget);
48969
49518
  case "plotter":
48970
- // TODO(LEMS-2585): implement a real parser for this widget
48971
- return parseAndAssign(`plotter ${id}`, any);
49519
+ return parseAndAssign(`plotter ${id}`, parsePlotterWidget);
48972
49520
  case "python-program":
48973
- // TODO(LEMS-2585): implement a real parser for this widget
48974
- return parseAndAssign(`python-program ${id}`, any);
49521
+ return parseAndAssign(`python-program ${id}`, parsePythonProgramWidget);
48975
49522
  case "radio":
48976
- // TODO(LEMS-2585): implement a real parser for this widget
48977
- return parseAndAssign(`radio ${id}`, any);
49523
+ return parseAndAssign(`radio ${id}`, parseRadioWidget);
48978
49524
  case "sorter":
48979
- // TODO(LEMS-2585): implement a real parser for this widget
48980
- return parseAndAssign(`sorter ${id}`, any);
49525
+ return parseAndAssign(`sorter ${id}`, parseSorterWidget);
48981
49526
  case "table":
48982
- // TODO(LEMS-2585): implement a real parser for this widget
48983
- return parseAndAssign(`table ${id}`, any);
49527
+ return parseAndAssign(`table ${id}`, parseTableWidget);
48984
49528
  case "video":
48985
- // TODO(LEMS-2585): implement a real parser for this widget
48986
- return parseAndAssign(`video ${id}`, any);
49529
+ return parseAndAssign(`video ${id}`, parseVideoWidget);
48987
49530
  default:
49531
+ if (getWidget(type)) {
49532
+ // @ts-expect-error - 'type' is not a valid widget type
49533
+ return parseAndAssign(`${type} ${id}`, any);
49534
+ }
48988
49535
  return ctx.failure("a valid widget type", type);
48989
49536
  }
48990
49537
  };
@@ -49007,16 +49554,6 @@ const parseHint = object({
49007
49554
  }))
49008
49555
  });
49009
49556
 
49010
- const parsePerseusRenderer = object({
49011
- content: string,
49012
- widgets: parseWidgetsMap,
49013
- metadata: optional(array(string)),
49014
- images: record(string, object({
49015
- width: number,
49016
- height: number
49017
- }))
49018
- });
49019
-
49020
49557
  object({
49021
49558
  question: parsePerseusRenderer,
49022
49559
  hints: array(parseHint),
@@ -49357,6 +49894,7 @@ exports.parsePerseusItem = parsePerseusItem;
49357
49894
  exports.plotterPlotTypes = plotterPlotTypes;
49358
49895
  exports.preprocessTex = preprocessTex;
49359
49896
  exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
49897
+ exports.scorePerseusItem = scorePerseusItem;
49360
49898
  exports.shapes = shapes;
49361
49899
  exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
49362
49900
  exports.traverse = traverse;