@khanacademy/perseus 41.5.0 → 42.0.1

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 (73) hide show
  1. package/dist/es/index.js +513 -268
  2. package/dist/es/index.js.map +1 -1
  3. package/dist/es/strings.js +3 -2
  4. package/dist/es/strings.js.map +1 -1
  5. package/dist/index.d.ts +4 -1
  6. package/dist/index.js +519 -263
  7. package/dist/index.js.map +1 -1
  8. package/dist/renderer-util.d.ts +3 -2
  9. package/dist/renderer.d.ts +7 -10
  10. package/dist/server-item-renderer.d.ts +33 -2
  11. package/dist/strings.d.ts +2 -1
  12. package/dist/strings.js +3 -2
  13. package/dist/strings.js.map +1 -1
  14. package/dist/types.d.ts +6 -1
  15. package/dist/util/deprecated-widgets/input-number.d.ts +9 -0
  16. package/dist/util/deprecated-widgets/modernize-widgets-utils.d.ts +4 -0
  17. package/dist/util/parse-perseus-json/exhaustive-test-tool/index.d.ts +2 -0
  18. package/dist/widget-ai-utils/graded-group/{prompt-utils.d.ts → graded-group-ai-utils.d.ts} +1 -1
  19. package/dist/widget-ai-utils/graded-group-set/{prompt-utils.d.ts → graded-group-set-ai-utils.d.ts} +1 -1
  20. package/dist/widget-ai-utils/group/{prompt-utils.d.ts → group-ai-utils.d.ts} +1 -1
  21. package/dist/widget-ai-utils/prompt-types.d.ts +33 -0
  22. package/dist/widget-ai-utils/unsupported-widget.d.ts +1 -1
  23. package/dist/widgets/categorizer/categorizer.d.ts +1 -1
  24. package/dist/widgets/definition/definition.d.ts +1 -1
  25. package/dist/widgets/dropdown/dropdown.d.ts +1 -1
  26. package/dist/widgets/explanation/explanation.d.ts +1 -1
  27. package/dist/widgets/expression/expression.d.ts +3 -3
  28. package/dist/widgets/graded-group/graded-group.d.ts +1 -1
  29. package/dist/widgets/graded-group-set/graded-group-set.d.ts +1 -1
  30. package/dist/widgets/grapher/grapher.d.ts +1 -1
  31. package/dist/widgets/group/group.d.ts +1 -1
  32. package/dist/widgets/image/image.d.ts +1 -1
  33. package/dist/widgets/input-number/input-number.d.ts +1 -1
  34. package/dist/widgets/interactive-graph.d.ts +1 -1
  35. package/dist/widgets/interactive-graphs/graphs/components/movable-point.d.ts +9 -0
  36. package/dist/widgets/interactive-graphs/graphs/components/use-control-point.d.ts +9 -0
  37. package/dist/widgets/label-image/label-image.d.ts +1 -1
  38. package/dist/widgets/matcher/matcher.d.ts +1 -1
  39. package/dist/widgets/matrix/matrix.d.ts +1 -1
  40. package/dist/widgets/number-line/number-line.d.ts +1 -1
  41. package/dist/widgets/orderer/orderer.d.ts +1 -1
  42. package/dist/widgets/passage/passage.d.ts +1 -1
  43. package/dist/widgets/passage-ref/passage-ref.d.ts +1 -1
  44. package/dist/widgets/radio/radio-component.d.ts +1 -1
  45. package/dist/widgets/sorter/sorter.d.ts +1 -1
  46. package/package.json +1 -1
  47. package/dist/prompt-types.d.ts +0 -33
  48. /package/dist/widget-ai-utils/categorizer/{prompt-utils.d.ts → categorizer-ai-utils.d.ts} +0 -0
  49. /package/dist/widget-ai-utils/cs-program/{prompt-utils.d.ts → cs-program-ai-utils.d.ts} +0 -0
  50. /package/dist/widget-ai-utils/definition/{prompt-utils.d.ts → definition-ai-utils.d.ts} +0 -0
  51. /package/dist/widget-ai-utils/dropdown/{prompt-utils.d.ts → dropdown-ai-utils.d.ts} +0 -0
  52. /package/dist/widget-ai-utils/explanation/{prompt-utils.d.ts → explanation-ai-utils.d.ts} +0 -0
  53. /package/dist/widget-ai-utils/expression/{prompt-utils.d.ts → expression-ai-utils.d.ts} +0 -0
  54. /package/dist/widget-ai-utils/grapher/{prompt-utils.d.ts → grapher-ai-utils.d.ts} +0 -0
  55. /package/dist/widget-ai-utils/iframe/{prompt-utils.d.ts → iframe-ai-utils.d.ts} +0 -0
  56. /package/dist/widget-ai-utils/image/{prompt-utils.d.ts → image-ai-utils.d.ts} +0 -0
  57. /package/dist/widget-ai-utils/input-number/{prompt-utils.d.ts → input-number-ai-utils.d.ts} +0 -0
  58. /package/dist/widget-ai-utils/interaction/{prompt-utils.d.ts → interaction-ai-utils.d.ts} +0 -0
  59. /package/dist/widget-ai-utils/interactive-graph/{prompt-utils.d.ts → interactive-graph-ai-utils.d.ts} +0 -0
  60. /package/dist/widget-ai-utils/label-image/{prompt-utils.d.ts → label-image-ai-utils.d.ts} +0 -0
  61. /package/dist/widget-ai-utils/matcher/{prompt-utils.d.ts → matcher-ai-utils.d.ts} +0 -0
  62. /package/dist/widget-ai-utils/matrix/{prompt-utils.d.ts → matrix-ai-utils.d.ts} +0 -0
  63. /package/dist/widget-ai-utils/measurer/{prompt-utils.d.ts → measurer-ai-utils.d.ts} +0 -0
  64. /package/dist/widget-ai-utils/number-line/{prompt-utils.d.ts → number-line-ai-utils.d.ts} +0 -0
  65. /package/dist/widget-ai-utils/orderer/{prompt-utils.d.ts → orderer-ai-utils.d.ts} +0 -0
  66. /package/dist/widget-ai-utils/passage/{prompt-utils.d.ts → passage-ai-utils.d.ts} +0 -0
  67. /package/dist/widget-ai-utils/passage-ref/{prompt-utils.d.ts → passage-ref-ai-utils.d.ts} +0 -0
  68. /package/dist/widget-ai-utils/phet-simulation/{prompt-utils.d.ts → phet-simulation-ai-utils.d.ts} +0 -0
  69. /package/dist/widget-ai-utils/plotter/{prompt-utils.d.ts → plotter-ai-utils.d.ts} +0 -0
  70. /package/dist/widget-ai-utils/python-program/{prompt-utils.d.ts → python-ai-utils.d.ts} +0 -0
  71. /package/dist/widget-ai-utils/radio/{prompt-utils.d.ts → radio-ai-utils.d.ts} +0 -0
  72. /package/dist/widget-ai-utils/sorter/{prompt-utils.d.ts → sorter-ai-utils.d.ts} +0 -0
  73. /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 {
@@ -18874,6 +18875,326 @@ class QuestionParagraph extends React__namespace.Component {
18874
18875
  }
18875
18876
  }
18876
18877
 
18878
+ // Methods to convert input-number widgets to numeric-input widgets
18879
+
18880
+ const inputNumberToNumericInput = json => {
18881
+ // First we need to create a map of the old input-number keys to the new numeric-input keys
18882
+ // so that we can ensure we update the content and widgets accordingly
18883
+ const renameMap = getInputNumberRenameMap(json);
18884
+
18885
+ // Then we can use this to update the JSON
18886
+ return convertInputNumberJson(json, renameMap);
18887
+ };
18888
+
18889
+ // We need to be able to run this code recursively in order to convert nested input-number widgets
18890
+ const convertInputNumberJson = (json, renameMap) => {
18891
+ const updatedContent = convertDeprecatedWidgetsInContent(json, renameMap);
18892
+ const updatedWidgets = convertInputNumberWidgetOptions(json, renameMap);
18893
+ const modernizedJson = {
18894
+ ...json,
18895
+ content: updatedContent,
18896
+ widgets: updatedWidgets
18897
+ };
18898
+ return modernizedJson;
18899
+ };
18900
+
18901
+ // Convert the input-number json in the widgets of a PerseusRenderer object
18902
+ const convertInputNumberWidgetOptions = (json, renameMap) => {
18903
+ const widgets = {
18904
+ ...json.widgets
18905
+ };
18906
+ // The question.widgets is a dictionary map of widgets, so we need to loop through in order to convert input-number to numeric-input
18907
+ // which can exist as both the key or as a value on widget.type.
18908
+ for (const key of Object.keys(widgets)) {
18909
+ // Loop through the keys of the widgets dictionary
18910
+ if (widgets[key].options.widgets) {
18911
+ widgets[key].options = {
18912
+ ...inputNumberToNumericInput(widgets[key].options)
18913
+ };
18914
+ }
18915
+ // Check if the widget is an input-number
18916
+ if (widgets[key].type === "input-number") {
18917
+ // If the answerType is not number or percent, we need to provide
18918
+ // the answer form for the numeric-input widget
18919
+ const provideAnswerForm = widgets[key].options.answerType !== "number" && widgets[key].options.answerType !== "percent";
18920
+ // We need to determine the mathFormat for the numeric-input widget
18921
+ const mathFormat = widgets[key].options.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
18922
+ : widgets[key].options.answerType; // Otherwise, we can use the answerType directly
18923
+
18924
+ // We need to update the answers prop to match the numeric-input widget format
18925
+ const answers = [{
18926
+ value: widgets[key].options.value,
18927
+ simplify: widgets[key].options.simplify,
18928
+ // Input Number doesn't have a strict prop, so we default to false
18929
+ strict: false,
18930
+ // We only want to set maxError if the inexact prop is true
18931
+ maxError: widgets[key].options.inexact ? widgets[key].options.maxError : 0,
18932
+ status: "correct",
18933
+ // Input-number only allows correct answers
18934
+ message: ""
18935
+ }];
18936
+
18937
+ // Add the required answerForms if provided/applicable
18938
+ if (provideAnswerForm) {
18939
+ answers[0].answerForms = [mathFormat];
18940
+ }
18941
+
18942
+ // Update the options prop to match the numeric-input widget format
18943
+ const upgradedWidgetOptions = {
18944
+ answers,
18945
+ size: widgets[key].options.size,
18946
+ coefficient: false,
18947
+ // input-number doesn't have a coefficient prop
18948
+ labelText: "",
18949
+ // input-number doesn't have a labelText prop
18950
+ static: false,
18951
+ // static is always false for numeric-input
18952
+ rightAlign: widgets[key].options.rightAlign || false
18953
+ };
18954
+ const upgradedWidget = {
18955
+ options: upgradedWidgetOptions,
18956
+ type: "numeric-input"
18957
+ };
18958
+ const newWidgetName = renameMap[key];
18959
+ // Create the new key entry
18960
+ widgets[newWidgetName] = upgradedWidget;
18961
+
18962
+ // We need to delete the old widget key
18963
+ delete widgets[key];
18964
+ }
18965
+ }
18966
+ return widgets;
18967
+ };
18968
+
18969
+ // Convert the deprecated widget refs in the content string
18970
+ // of a PerseusRenderer object to their renamed equivalents
18971
+ const convertDeprecatedWidgetsInContent = (json, renameMap) => {
18972
+ return Object.keys(renameMap).reduce((newContent, oldKey) => {
18973
+ const newKey = renameMap[oldKey];
18974
+ return newKey ? newContent.replace(oldKey, newKey) : newContent;
18975
+ }, json.content);
18976
+ };
18977
+
18978
+ // Create a map of the old input-number keys to the new numeric-input keys
18979
+ const getInputNumberRenameMap = json => {
18980
+ const numericRegex = /(?<=\[\[\u2603 )(numeric-input \d+)(?=\]\])/g;
18981
+ const inputNumberRegex = /(?<=\[\[\u2603 )(input-number \d+)(?=\]\])/g;
18982
+
18983
+ // Get all the content strings within the json, which might be nested within widgets
18984
+ const allContentStrings = json.content;
18985
+
18986
+ // Loop through the content strings to get all the input-number widgets
18987
+ const renameMap = {};
18988
+ const inputNumberMatches = [...(allContentStrings.match(inputNumberRegex) || [])];
18989
+
18990
+ // We want to count any pre-existing numeric-input widgets
18991
+ // so that we can start the new ids at the next number
18992
+ const numericMatches = [...(allContentStrings.match(numericRegex) || [])];
18993
+ let currentNumericCount = numericMatches.reduce((count, match) => {
18994
+ const id = parseInt(match.split(" ")[1], 10);
18995
+ return id >= count ? id + 1 : count; // We want to start at the next highest number
18996
+ }, 1);
18997
+
18998
+ // Now that we have all the necessary information, we can create the renameMap
18999
+ for (const match of inputNumberMatches) {
19000
+ const oldKey = match;
19001
+ const newKey = `numeric-input ${currentNumericCount}`;
19002
+ renameMap[oldKey] = newKey;
19003
+ currentNumericCount++;
19004
+ }
19005
+ return renameMap;
19006
+ };
19007
+
19008
+ // Convert the user input data keys from input-number to numeric-input
19009
+ const convertUserInputNumberData = userInputMap => {
19010
+ const updatedUserInputMap = {
19011
+ ...userInputMap
19012
+ };
19013
+ for (const key of Object.keys(userInputMap)) {
19014
+ if (key.includes("input-number")) {
19015
+ const updatedKey = key.replace("input-number", "numeric-input");
19016
+ updatedUserInputMap[updatedKey] = userInputMap[key];
19017
+ delete updatedUserInputMap[key];
19018
+ }
19019
+ }
19020
+ return updatedUserInputMap;
19021
+ };
19022
+
19023
+ const widgetRegExes = [/input-number \d+/]; // We can add more regexes here in the future
19024
+
19025
+ // This utils in this file are used to modernize our Perseus JSON structure,
19026
+ // so that we can convert deprecated widgets to their modern equivalents when
19027
+ // content creators use the Editor Page to update content containing these widgets.
19028
+ // Currently, we're only converting input-number to numeric-input,
19029
+ // but we can add more conversions here in the future.
19030
+ // Modernize the json content of a PerseusRenderer object
19031
+ // by converting deprecated widgets to their modern equivalents
19032
+ const convertDeprecatedWidgets = json => {
19033
+ // Currently we're only converting input-number to numeric-input,
19034
+ // But we can add more conversions here in the future
19035
+ return inputNumberToNumericInput(json);
19036
+ };
19037
+ const conversionRequired = json => {
19038
+ // If there's no content, then there's no conversion required
19039
+ if (!json.content) {
19040
+ return false;
19041
+ }
19042
+ // Check the content string for any top-level input-number widgets
19043
+ if (widgetRegExes.some(regex => regex.test(json.content))) {
19044
+ return true;
19045
+ }
19046
+
19047
+ // If there's no deprecated widget in the top-level, then check for any nested widgets
19048
+ for (const key of Object.keys(json.widgets)) {
19049
+ if (json.widgets[key].options.widgets) {
19050
+ const nestedJson = json.widgets[key].options;
19051
+ if (conversionRequired(nestedJson)) {
19052
+ return true;
19053
+ }
19054
+ }
19055
+ }
19056
+ return false;
19057
+ };
19058
+
19059
+ // Convert the user input data keys for deprecated widgets to their modern equivalents
19060
+ const convertUserInputData = userInputMap => {
19061
+ // Currently we're only converting input-number to numeric-input,
19062
+ // But we can add more conversions here in the future
19063
+ return convertUserInputNumberData(userInputMap);
19064
+ };
19065
+
19066
+ // These are all the current widgets that require user-input and are supported for automatic scoring
19067
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
19068
+
19069
+ /**
19070
+ * Regex for widget placeholders in a string.
19071
+ *
19072
+ * First capture group is the widget ID (ex. 'radio 1')
19073
+ * Second capture group is the widget type (ex. "radio)
19074
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
19075
+ */
19076
+ function getWidgetRegex() {
19077
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
19078
+ }
19079
+
19080
+ /**
19081
+ * Add a widget placeholder using the widget ID.
19082
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
19083
+ *
19084
+ * @param {string} id
19085
+ * @returns {string}
19086
+ */
19087
+ function addWidget(id) {
19088
+ return `[[☃ ${id}]]`;
19089
+ }
19090
+
19091
+ /**
19092
+ * Get a widget type by a widget's ID
19093
+ * (please don't derive type from ID)
19094
+ *
19095
+ * @param {string} widgetId the ID of the widget
19096
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
19097
+ * @returns {string} the widget type (ie "radio")
19098
+ */
19099
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
19100
+ const widget = widgetMap[WidgetId];
19101
+ return widget?.type ?? null;
19102
+ }
19103
+
19104
+ /**
19105
+ * Does the content have a specific type of widget?
19106
+ *
19107
+ * @param {string} type the type of the widget in question (ie "radio")
19108
+ * @param {string} content the string to search through
19109
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
19110
+ * @returns {boolean} if the content includes the widget type
19111
+ */
19112
+ function contentHasWidgetType(type, content, widgetMap) {
19113
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
19114
+ }
19115
+
19116
+ /**
19117
+ * Extract all widget IDs, which includes the widget type and instance number.
19118
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
19119
+ *
19120
+ * Content should contain Perseus widget placeholders,
19121
+ * which look like: '[[☃ radio 1]]'.
19122
+ *
19123
+ * @param {string} content
19124
+ * @returns {ReadonlyArray<string>} widgetIds
19125
+ */
19126
+ function getWidgetIdsFromContent(content) {
19127
+ const widgets = [];
19128
+ const localWidgetRegex = getWidgetRegex();
19129
+ let match = localWidgetRegex.exec(content);
19130
+ while (match !== null) {
19131
+ widgets.push(match[1]);
19132
+ match = localWidgetRegex.exec(content);
19133
+ }
19134
+ return widgets;
19135
+ }
19136
+
19137
+ /**
19138
+ * Get a list of widget IDs from content,
19139
+ * but only for specific widget types
19140
+ *
19141
+ * @param {string} type the type of widget (ie "radio")
19142
+ * @param {string} content the string to parse
19143
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
19144
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
19145
+ */
19146
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
19147
+ const rv = [];
19148
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
19149
+ widgetIdsInContent.forEach(widgetId => {
19150
+ const widget = widgetMap[widgetId];
19151
+ if (widget?.type === type) {
19152
+ rv.push(widgetId);
19153
+ }
19154
+ });
19155
+ return rv;
19156
+ }
19157
+
19158
+ /**
19159
+ * Pull the widget map out of ItemData
19160
+ *
19161
+ * @param {PerseusItem} PerseusItem containing a widgetMap
19162
+ * @returns {WidgetMap} the widget map in the PerseusItem
19163
+ */
19164
+ function getWidgetsMapFromItemData(itemData) {
19165
+ return itemData.question.widgets;
19166
+ }
19167
+
19168
+ /**
19169
+ * Get the widget information from a WidgetMap
19170
+ *
19171
+ * @param {string} widgetId the ID of the widget
19172
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
19173
+ */
19174
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
19175
+ return widgetMap[widgetId] ?? null;
19176
+ }
19177
+
19178
+ /**
19179
+ * Get select widgets from a widget map.
19180
+ * Useful for multi-items that needs to split
19181
+ * one PerseusItem into several
19182
+ *
19183
+ * @param {ReadonlyArray<string>} widgetIds to extract
19184
+ * @param {PerseusWidgetsMap} widgetMap to extract from
19185
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
19186
+ */
19187
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
19188
+ const widgets = {};
19189
+ widgetIds.forEach(widgetId => {
19190
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
19191
+ if (widget) {
19192
+ widgets[widgetId] = widget;
19193
+ }
19194
+ });
19195
+ return widgets;
19196
+ }
19197
+
18877
19198
  const DEFAULT_ALIGNMENT$1 = "block";
18878
19199
  // NOTE(kevinb): "default" is not one in `validAlignments`.
18879
19200
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -19334,6 +19655,27 @@ widgetIds, userInputMap, strings, locale) {
19334
19655
  }
19335
19656
  });
19336
19657
  }
19658
+
19659
+ // TODO: combine scorePerseusItem with scoreWidgetsFunctional
19660
+ // once scorePerseusItem is the only one calling scoreWidgetsFunctional
19661
+ function scorePerseusItem(perseusRenderData, userInputMap,
19662
+ // TODO(LEMS-2461,LEMS-2391): these probably
19663
+ // need to be removed before we move this to the server
19664
+ strings, locale) {
19665
+ // Check if the PerseusRenderer object contains any deprecated widgets that need to be converted
19666
+ const mustConvertData = conversionRequired(perseusRenderData);
19667
+ const convertedRenderData = mustConvertData ? convertDeprecatedWidgets(perseusRenderData) // Convert deprecated widgets to their modern equivalents
19668
+ : perseusRenderData;
19669
+ const convertedUserInputMap = mustConvertData ? convertUserInputData(userInputMap) // Convert deprecated user input data keys to their modern equivalents
19670
+ : userInputMap;
19671
+
19672
+ // There seems to be a chance that PerseusRenderer.widgets might include
19673
+ // widget data for widgets that are not in PerseusRenderer.content,
19674
+ // so this checks that the widgets are being used before scoring them
19675
+ const usedWidgetIds = getWidgetIdsFromContent(convertedRenderData.content);
19676
+ const scores = scoreWidgetsFunctional(convertedRenderData.widgets, usedWidgetIds, convertedUserInputMap, strings, locale);
19677
+ return Util.flattenScores(scores);
19678
+ }
19337
19679
  function scoreWidgetsFunctional(widgets,
19338
19680
  // This is a port of old code, I'm not sure why
19339
19681
  // we need widgetIds vs the keys of the widgets object
@@ -19354,7 +19696,7 @@ widgetIds, userInputMap, strings, locale) {
19354
19696
  const userInput = userInputMap[id];
19355
19697
  const scorer = getWidgetScorer(widget.type);
19356
19698
  if (widget.type === "group") {
19357
- const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19699
+ const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
19358
19700
  widgetScores[id] = Util.flattenScores(scores);
19359
19701
  } else if (scorer) {
19360
19702
  widgetScores[id] = scorer(userInput, widget.options, strings, locale);
@@ -20403,7 +20745,7 @@ class Renderer extends React__namespace.Component {
20403
20745
  // Instead, we center them with css magic in styles.less
20404
20746
  // /cry(aria)
20405
20747
  this._foundTextNodes = true;
20406
- if (___default["default"].contains(this.widgetIds, node.id)) {
20748
+ if (this.widgetIds.includes(node.id)) {
20407
20749
  // We don't want to render a duplicate widget key/ref,
20408
20750
  // as this causes problems with react (for obvious
20409
20751
  // reasons). Instead we just notify the
@@ -20728,7 +21070,7 @@ class Renderer extends React__namespace.Component {
20728
21070
  };
20729
21071
  getInputPaths = () => {
20730
21072
  const inputPaths = [];
20731
- ___default["default"].each(this.widgetIds, widgetId => {
21073
+ this.widgetIds.forEach(widgetId => {
20732
21074
  const widget = this.getWidgetInstance(widgetId);
20733
21075
  if (widget && widget.getInputPaths) {
20734
21076
  // Grab all input paths and add widgetID to the front
@@ -20736,7 +21078,7 @@ class Renderer extends React__namespace.Component {
20736
21078
  // Prefix paths with their widgetID and add to collective
20737
21079
  // list of paths.
20738
21080
  // @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
20739
- ___default["default"].each(widgetInputPaths, inputPath => {
21081
+ widgetInputPaths.forEach(inputPath => {
20740
21082
  const relativeInputPath = [widgetId].concat(inputPath);
20741
21083
  inputPaths.push(relativeInputPath);
20742
21084
  });
@@ -20929,33 +21271,30 @@ class Renderer extends React__namespace.Component {
20929
21271
  }
20930
21272
 
20931
21273
  /**
20932
- * Returns an object mapping from widget ID to perseus-style score.
20933
- * The keys of this object are the values of the array returned
20934
- * from `getWidgetIds`.
20935
- */
20936
- scoreWidgets() {
20937
- return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20938
- }
20939
-
20940
- /**
20941
- * Grades the content.
21274
+ * Scores the content.
21275
+ *
21276
+ * @deprecated use scorePerseusItem
20942
21277
  */
20943
21278
  score() {
20944
- const scores = this.scoreWidgets();
21279
+ const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20945
21280
  const combinedScore = Util.flattenScores(scores);
20946
21281
  return combinedScore;
20947
21282
  }
20948
- guessAndScore() {
21283
+
21284
+ /**
21285
+ * @deprecated use scorePerseusItem
21286
+ */
21287
+ guessAndScore = () => {
20949
21288
  const totalGuess = this.getUserInput();
20950
21289
  const totalScore = this.score();
20951
21290
  return [totalGuess, totalScore];
20952
- }
21291
+ };
20953
21292
  examples = () => {
20954
21293
  const widgetIds = this.widgetIds;
20955
- const examples = ___default["default"].compact(___default["default"].map(widgetIds, widgetId => {
21294
+ const examples = widgetIds.map(widgetId => {
20956
21295
  const widget = this.getWidgetInstance(widgetId);
20957
21296
  return widget != null && widget.examples ? widget.examples() : null;
20958
- }));
21297
+ }).filter(Boolean);
20959
21298
 
20960
21299
  // no widgets with examples
20961
21300
  if (!examples.length) {
@@ -22463,7 +22802,9 @@ const propUpgrades$2 = {
22463
22802
  // input-number to numeric-input. In this case, we need to upgrade
22464
22803
  // the widget options accordingly.
22465
22804
  if (initialProps.value) {
22466
- const provideAnswerForm = initialProps.answerType !== "number";
22805
+ // If the answerType is not number or percent, we need to provide
22806
+ // the answer form for the numeric-input widget
22807
+ const provideAnswerForm = initialProps.answerType !== "number" && initialProps.answerType !== "percent";
22467
22808
 
22468
22809
  // We need to determine the mathFormat for the numeric-input widget
22469
22810
  const mathFormat = initialProps.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
@@ -29528,6 +29869,9 @@ class Group extends React__namespace.Component {
29528
29869
  this.change("widgets", this.rendererRef.props.widgets);
29529
29870
  }
29530
29871
  };
29872
+
29873
+ // TODO(LEMS-2391): replace this when there's a separate check
29874
+ // for valid/invalid state
29531
29875
  const score = this.rendererRef?.score();
29532
29876
  const isValid = score && score.type !== "invalid";
29533
29877
  const isInvalid = score && score.type === "invalid";
@@ -31371,6 +31715,108 @@ function AxisArrows() {
31371
31715
  })));
31372
31716
  }
31373
31717
 
31718
+ /**
31719
+ * 44 is touch best practice and AAA compliant for WCAG
31720
+ * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
31721
+ */
31722
+ const TARGET_SIZE = 44;
31723
+ const REMOVE_BUTTON_ID = "perseus_mafs_remove_button";
31724
+
31725
+ // same as pointsFromNormalized in interactive-graph.tsx
31726
+ const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
31727
+ // maps over [x, y]
31728
+ coords.map((coord, i) => {
31729
+ const axisRange = range[i];
31730
+ if (noSnap) {
31731
+ return axisRange[MIN] + size(axisRange) * coord;
31732
+ }
31733
+ const axisStep = step[i];
31734
+ const nSteps = Math.floor(size(axisRange) / axisStep);
31735
+ const tick = Math.round(coord * nSteps);
31736
+ return axisRange[MIN] + axisStep * tick;
31737
+ }));
31738
+
31739
+ // same as normalizeCoords in interactive-graph.tsx
31740
+ const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
31741
+ return (coord + ranges[i][1]) / size(ranges[i]);
31742
+ }));
31743
+
31744
+ // Returns the closest point to the given `point` that is within the graph
31745
+ // bounds given in `state`.
31746
+ function bound$1(_ref) {
31747
+ let {
31748
+ snapStep,
31749
+ range,
31750
+ point
31751
+ } = _ref;
31752
+ const boundingBox = inset(snapStep, range);
31753
+ return clampToBox(boundingBox, point);
31754
+ }
31755
+ function isUnlimitedGraphState(state) {
31756
+ return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
31757
+ }
31758
+
31759
+ /**
31760
+ * Replace all text outside of the $ TeX blocks with `\\text{...}`
31761
+ * This way, the entire resulting string can be rendered within <TeX>
31762
+ * and the text outside of the $ blocks will be non-TeX text.
31763
+ */
31764
+ function replaceOutsideTeX(mathString) {
31765
+ // All the information we need is in the first section,
31766
+ // whether it's typed as "blockmath" or "paragraph"
31767
+ const firstSection = SimpleMarkdown__namespace.parse(mathString)[0];
31768
+
31769
+ // If it's blockMath, the outer level has the full math content.
31770
+ if (firstSection.type === "blockMath") {
31771
+ return firstSection.content;
31772
+ }
31773
+
31774
+ // If it's a paragraph, we need to iterate through the sections
31775
+ // to look for individual math blocks.
31776
+ const condensedNodes = condenseTextNodes(firstSection.content);
31777
+ let result = "";
31778
+ for (const piece of condensedNodes) {
31779
+ piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
31780
+ }
31781
+ return result;
31782
+ }
31783
+ // Helper function for replaceOutsideTeX()
31784
+ // Condense adjacent text nodes into a single text node
31785
+ function condenseTextNodes(nodes) {
31786
+ const result = [];
31787
+ if (!nodes) {
31788
+ return result;
31789
+ }
31790
+ let currentText = "";
31791
+ for (const node of nodes) {
31792
+ if (node.type === "math") {
31793
+ if (currentText) {
31794
+ result.push({
31795
+ type: "text",
31796
+ content: currentText
31797
+ });
31798
+ currentText = "";
31799
+ }
31800
+ result.push(node);
31801
+ } else {
31802
+ currentText += node.content;
31803
+ }
31804
+ }
31805
+ if (currentText) {
31806
+ result.push({
31807
+ type: "text",
31808
+ content: currentText
31809
+ });
31810
+ }
31811
+ return result;
31812
+ }
31813
+
31814
+ // Helper function for replaceOutsideTeX()
31815
+ function escapeSpecialChars(str) {
31816
+ // Escape $, \, {, and } characters
31817
+ return str.replace(/([$\\{}])/g, "\\$1");
31818
+ }
31819
+
31374
31820
  function AxisLabels() {
31375
31821
  const {
31376
31822
  range,
@@ -31402,7 +31848,7 @@ function AxisLabels() {
31402
31848
  fontSize: "14px",
31403
31849
  transform: "translate(7px, -50%)"
31404
31850
  }
31405
- }, /*#__PURE__*/React__default["default"].createElement(TeX, null, xAxisLabelText)), /*#__PURE__*/React__default["default"].createElement("span", {
31851
+ }, /*#__PURE__*/React__default["default"].createElement(TeX, null, replaceOutsideTeX(xAxisLabelText))), /*#__PURE__*/React__default["default"].createElement("span", {
31406
31852
  style: {
31407
31853
  position: "absolute",
31408
31854
  left: x2,
@@ -31410,7 +31856,7 @@ function AxisLabels() {
31410
31856
  fontSize: "14px",
31411
31857
  transform: "translate(-50%, 0px)"
31412
31858
  }
31413
- }, /*#__PURE__*/React__default["default"].createElement(TeX, null, yAxisLabelText)));
31859
+ }, /*#__PURE__*/React__default["default"].createElement(TeX, null, replaceOutsideTeX(yAxisLabelText))));
31414
31860
  }
31415
31861
 
31416
31862
  // The size of the ticks and labels in pixels
@@ -32348,108 +32794,6 @@ const lockedFigureFillStyles = {
32348
32794
 
32349
32795
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
32350
32796
 
32351
- /**
32352
- * 44 is touch best practice and AAA compliant for WCAG
32353
- * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
32354
- */
32355
- const TARGET_SIZE = 44;
32356
- const REMOVE_BUTTON_ID = "perseus_mafs_remove_button";
32357
-
32358
- // same as pointsFromNormalized in interactive-graph.tsx
32359
- const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
32360
- // maps over [x, y]
32361
- coords.map((coord, i) => {
32362
- const axisRange = range[i];
32363
- if (noSnap) {
32364
- return axisRange[MIN] + size(axisRange) * coord;
32365
- }
32366
- const axisStep = step[i];
32367
- const nSteps = Math.floor(size(axisRange) / axisStep);
32368
- const tick = Math.round(coord * nSteps);
32369
- return axisRange[MIN] + axisStep * tick;
32370
- }));
32371
-
32372
- // same as normalizeCoords in interactive-graph.tsx
32373
- const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
32374
- return (coord + ranges[i][1]) / size(ranges[i]);
32375
- }));
32376
-
32377
- // Returns the closest point to the given `point` that is within the graph
32378
- // bounds given in `state`.
32379
- function bound$1(_ref) {
32380
- let {
32381
- snapStep,
32382
- range,
32383
- point
32384
- } = _ref;
32385
- const boundingBox = inset(snapStep, range);
32386
- return clampToBox(boundingBox, point);
32387
- }
32388
- function isUnlimitedGraphState(state) {
32389
- return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
32390
- }
32391
-
32392
- /**
32393
- * Replace all text outside of the $ TeX blocks with `\\text{...}`
32394
- * This way, the entire resulting string can be rendered within <TeX>
32395
- * and the text outside of the $ blocks will be non-TeX text.
32396
- */
32397
- function replaceOutsideTeX(mathString) {
32398
- // All the information we need is in the first section,
32399
- // whether it's typed as "blockmath" or "paragraph"
32400
- const firstSection = SimpleMarkdown__namespace.parse(mathString)[0];
32401
-
32402
- // If it's blockMath, the outer level has the full math content.
32403
- if (firstSection.type === "blockMath") {
32404
- return firstSection.content;
32405
- }
32406
-
32407
- // If it's a paragraph, we need to iterate through the sections
32408
- // to look for individual math blocks.
32409
- const condensedNodes = condenseTextNodes(firstSection.content);
32410
- let result = "";
32411
- for (const piece of condensedNodes) {
32412
- piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
32413
- }
32414
- return result;
32415
- }
32416
- // Helper function for replaceOutsideTeX()
32417
- // Condense adjacent text nodes into a single text node
32418
- function condenseTextNodes(nodes) {
32419
- const result = [];
32420
- if (!nodes) {
32421
- return result;
32422
- }
32423
- let currentText = "";
32424
- for (const node of nodes) {
32425
- if (node.type === "math") {
32426
- if (currentText) {
32427
- result.push({
32428
- type: "text",
32429
- content: currentText
32430
- });
32431
- currentText = "";
32432
- }
32433
- result.push(node);
32434
- } else {
32435
- currentText += node.content;
32436
- }
32437
- }
32438
- if (currentText) {
32439
- result.push({
32440
- type: "text",
32441
- content: currentText
32442
- });
32443
- }
32444
- return result;
32445
- }
32446
-
32447
- // Helper function for replaceOutsideTeX()
32448
- function escapeSpecialChars(str) {
32449
- // Escape $, \, {, and } characters
32450
- return str.replace(/([$\\{}])/g, "\\$1");
32451
- }
32452
-
32453
32797
  function LockedLabel(props) {
32454
32798
  const {
32455
32799
  coord,
@@ -33678,6 +34022,7 @@ function useControlPoint(params) {
33678
34022
  } = useGraphConfig();
33679
34023
  const {
33680
34024
  point,
34025
+ sequenceNumber,
33681
34026
  color,
33682
34027
  cursor,
33683
34028
  constrain = p => snap(snapStep, p),
@@ -33718,6 +34063,7 @@ function useControlPoint(params) {
33718
34063
  ref: focusableHandleRef,
33719
34064
  role: "button",
33720
34065
  "aria-label": strings.srPointAtCoordinates({
34066
+ num: sequenceNumber,
33721
34067
  x: srFormatNumber(point[X], locale),
33722
34068
  y: srFormatNumber(point[Y], locale)
33723
34069
  })
@@ -33795,6 +34141,7 @@ const MovableLine = props => {
33795
34141
  focusableHandle: focusableHandle1
33796
34142
  } = useControlPoint({
33797
34143
  point: start,
34144
+ sequenceNumber: 1,
33798
34145
  color,
33799
34146
  onMove: p => onMovePoint(0, p)
33800
34147
  });
@@ -33803,6 +34150,7 @@ const MovableLine = props => {
33803
34150
  focusableHandle: focusableHandle2
33804
34151
  } = useControlPoint({
33805
34152
  point: end,
34153
+ sequenceNumber: 2,
33806
34154
  color,
33807
34155
  onMove: p => onMovePoint(1, p)
33808
34156
  });
@@ -33984,14 +34332,17 @@ function AngleGraph(props) {
33984
34332
  // Render the lines, angle, and then movable points
33985
34333
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, svgLines, /*#__PURE__*/React__namespace.createElement(Angle, angleParams), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33986
34334
  point: coords[1],
34335
+ sequenceNumber: 1,
33987
34336
  constrain: p => p,
33988
34337
  onMove: destination => dispatch(actions.angle.movePoint(1, destination))
33989
34338
  }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33990
34339
  point: coords[0],
34340
+ sequenceNumber: 2,
33991
34341
  constrain: getAngleSideConstraint(coords[0], coords[1], snapDegrees || 1),
33992
34342
  onMove: destination => dispatch(actions.angle.movePoint(0, destination))
33993
34343
  }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33994
34344
  point: coords[2],
34345
+ sequenceNumber: 3,
33995
34346
  constrain: getAngleSideConstraint(coords[2], coords[1], snapDegrees || 1),
33996
34347
  onMove: destination => dispatch(actions.angle.movePoint(2, destination))
33997
34348
  }));
@@ -34156,6 +34507,7 @@ function CircleGraph(props) {
34156
34507
  onMove: c => dispatch(actions.circle.moveCenter(c))
34157
34508
  }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34158
34509
  point: radiusPoint,
34510
+ sequenceNumber: 1,
34159
34511
  cursor: "ew-resize",
34160
34512
  onMove: newRadiusPoint => {
34161
34513
  dispatch(actions.circle.moveRadiusPoint(newRadiusPoint));
@@ -34331,6 +34683,7 @@ function LimitedPointGraph(props) {
34331
34683
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34332
34684
  key: i,
34333
34685
  point: point,
34686
+ sequenceNumber: i + 1,
34334
34687
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination))
34335
34688
  })));
34336
34689
  }
@@ -34372,6 +34725,7 @@ function UnlimitedPointGraph(props) {
34372
34725
  }), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34373
34726
  key: i,
34374
34727
  point: point,
34728
+ sequenceNumber: i + 1,
34375
34729
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
34376
34730
  ref: ref => {
34377
34731
  itemsRef.current[i] = ref;
@@ -34403,9 +34757,10 @@ function describePointGraph(state, i18n) {
34403
34757
  if (state.coords.length === 0) {
34404
34758
  return strings.srNoInteractiveElements;
34405
34759
  }
34406
- const pointDescriptions = state.coords.map(_ref2 => {
34760
+ const pointDescriptions = state.coords.map((_ref2, index) => {
34407
34761
  let [x, y] = _ref2;
34408
34762
  return strings.srPointAtCoordinates({
34763
+ num: index + 1,
34409
34764
  x: srFormatNumber(x, locale),
34410
34765
  y: srFormatNumber(y, locale)
34411
34766
  });
@@ -34525,6 +34880,7 @@ const LimitedPolygonGraph = props => {
34525
34880
  key: "point-" + i,
34526
34881
  constrain: constrain,
34527
34882
  point: point,
34883
+ sequenceNumber: i + 1,
34528
34884
  onMove: destination => {
34529
34885
  const now = Date.now();
34530
34886
  const targetFPS = 40;
@@ -34671,6 +35027,7 @@ const UnlimitedPolygonGraph = props => {
34671
35027
  }), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34672
35028
  key: i,
34673
35029
  point: point,
35030
+ sequenceNumber: i + 1,
34674
35031
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
34675
35032
  ref: ref => {
34676
35033
  pointRef.current[i] = ref;
@@ -34743,6 +35100,7 @@ function QuadraticGraph(props) {
34743
35100
  }), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34744
35101
  key: "point-" + i,
34745
35102
  point: coord,
35103
+ sequenceNumber: i + 1,
34746
35104
  onMove: destination => dispatch(actions.quadratic.movePoint(i, destination))
34747
35105
  })));
34748
35106
  }
@@ -34871,6 +35229,7 @@ function SinusoidGraph(props) {
34871
35229
  }), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34872
35230
  key: "point-" + i,
34873
35231
  point: coord,
35232
+ sequenceNumber: i + 1,
34874
35233
  onMove: destination => dispatch(actions.sinusoid.movePoint(i, destination))
34875
35234
  })));
34876
35235
  }
@@ -45039,7 +45398,7 @@ const init = function () {
45039
45398
 
45040
45399
  // This file is processed by a Rollup plugin (replace) to inject the production
45041
45400
  const libName = "@khanacademy/perseus";
45042
- const libVersion = "41.5.0";
45401
+ const libVersion = "42.0.1";
45043
45402
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45044
45403
 
45045
45404
  const apiVersion = {
@@ -45644,6 +46003,13 @@ class ServerItemRenderer extends React__namespace.Component {
45644
46003
  this.props.onRendered(true);
45645
46004
  }
45646
46005
  }
46006
+ if (this.props.score && this.props.score !== prevProps.score) {
46007
+ const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
46008
+ this.setState({
46009
+ questionCompleted: this.props.score.correct,
46010
+ questionHighlightedWidgets: emptyQuestionAreaWidgets
46011
+ });
46012
+ }
45647
46013
  }
45648
46014
  componentWillUnmount() {
45649
46015
  if (this.blurTimeoutID != null) {
@@ -45779,18 +46145,37 @@ class ServerItemRenderer extends React__namespace.Component {
45779
46145
  return this.questionRenderer.getPromptJSON();
45780
46146
  }
45781
46147
 
46148
+ /**
46149
+ * Returns an array of the widget `.getUserInput()` results
46150
+ *
46151
+ * TODO: can we remove this? Seems to be just for backwards
46152
+ * compatibility with old Perseus Chrome logging
46153
+ * @deprecated use getUserInput
46154
+ */
46155
+ getUserInputLegacy() {
46156
+ return this.questionRenderer.getUserInput();
46157
+ }
46158
+
46159
+ /**
46160
+ * Returns an object of the widget `.getUserInput()` results
46161
+ */
46162
+ getUserInput() {
46163
+ return this.questionRenderer.getUserInputMap();
46164
+ }
46165
+
45782
46166
  /**
45783
46167
  * Grades the item.
46168
+ *
46169
+ * @deprecated use scorePerseusItem
45784
46170
  */
45785
46171
  scoreInput() {
45786
- const guessAndScore = this.questionRenderer.guessAndScore();
45787
- const guess = guessAndScore[0];
45788
- const score = guessAndScore[1];
46172
+ const guess = this.getUserInput();
46173
+ const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
45789
46174
 
45790
46175
  // Continue to include an empty guess for the now defunct answer area.
45791
46176
  // TODO(alex): Check whether we rely on the format here for
45792
46177
  // analyzing ProblemLogs. If not, remove this layer.
45793
- const maxCompatGuess = [guess, []];
46178
+ const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
45794
46179
  const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
45795
46180
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45796
46181
  this.setState({
@@ -47652,138 +48037,6 @@ const registerAllWidgetsForTesting = () => {
47652
48037
  replaceDeprecatedWidgets();
47653
48038
  };
47654
48039
 
47655
- // These are all the current widgets that require user-input and are supported for automatic scoring
47656
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
47657
-
47658
- /**
47659
- * Regex for widget placeholders in a string.
47660
- *
47661
- * First capture group is the widget ID (ex. 'radio 1')
47662
- * Second capture group is the widget type (ex. "radio)
47663
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
47664
- */
47665
- function getWidgetRegex() {
47666
- return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
47667
- }
47668
-
47669
- /**
47670
- * Add a widget placeholder using the widget ID.
47671
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
47672
- *
47673
- * @param {string} id
47674
- * @returns {string}
47675
- */
47676
- function addWidget(id) {
47677
- return `[[☃ ${id}]]`;
47678
- }
47679
-
47680
- /**
47681
- * Get a widget type by a widget's ID
47682
- * (please don't derive type from ID)
47683
- *
47684
- * @param {string} widgetId the ID of the widget
47685
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47686
- * @returns {string} the widget type (ie "radio")
47687
- */
47688
- function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
47689
- const widget = widgetMap[WidgetId];
47690
- return widget?.type ?? null;
47691
- }
47692
-
47693
- /**
47694
- * Does the content have a specific type of widget?
47695
- *
47696
- * @param {string} type the type of the widget in question (ie "radio")
47697
- * @param {string} content the string to search through
47698
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47699
- * @returns {boolean} if the content includes the widget type
47700
- */
47701
- function contentHasWidgetType(type, content, widgetMap) {
47702
- return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
47703
- }
47704
-
47705
- /**
47706
- * Extract all widget IDs, which includes the widget type and instance number.
47707
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
47708
- *
47709
- * Content should contain Perseus widget placeholders,
47710
- * which look like: '[[☃ radio 1]]'.
47711
- *
47712
- * @param {string} content
47713
- * @returns {ReadonlyArray<string>} widgetIds
47714
- */
47715
- function getWidgetIdsFromContent(content) {
47716
- const widgets = [];
47717
- const localWidgetRegex = getWidgetRegex();
47718
- let match = localWidgetRegex.exec(content);
47719
- while (match !== null) {
47720
- widgets.push(match[1]);
47721
- match = localWidgetRegex.exec(content);
47722
- }
47723
- return widgets;
47724
- }
47725
-
47726
- /**
47727
- * Get a list of widget IDs from content,
47728
- * but only for specific widget types
47729
- *
47730
- * @param {string} type the type of widget (ie "radio")
47731
- * @param {string} content the string to parse
47732
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47733
- * @returns {ReadonlyArray<string>} the widget type (ie "radio")
47734
- */
47735
- function getWidgetIdsFromContentByType(type, content, widgetMap) {
47736
- const rv = [];
47737
- const widgetIdsInContent = getWidgetIdsFromContent(content);
47738
- widgetIdsInContent.forEach(widgetId => {
47739
- const widget = widgetMap[widgetId];
47740
- if (widget?.type === type) {
47741
- rv.push(widgetId);
47742
- }
47743
- });
47744
- return rv;
47745
- }
47746
-
47747
- /**
47748
- * Pull the widget map out of ItemData
47749
- *
47750
- * @param {PerseusItem} PerseusItem containing a widgetMap
47751
- * @returns {WidgetMap} the widget map in the PerseusItem
47752
- */
47753
- function getWidgetsMapFromItemData(itemData) {
47754
- return itemData.question.widgets;
47755
- }
47756
-
47757
- /**
47758
- * Get the widget information from a WidgetMap
47759
- *
47760
- * @param {string} widgetId the ID of the widget
47761
- * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
47762
- */
47763
- function getWidgetFromWidgetMap(widgetId, widgetMap) {
47764
- return widgetMap[widgetId] ?? null;
47765
- }
47766
-
47767
- /**
47768
- * Get select widgets from a widget map.
47769
- * Useful for multi-items that needs to split
47770
- * one PerseusItem into several
47771
- *
47772
- * @param {ReadonlyArray<string>} widgetIds to extract
47773
- * @param {PerseusWidgetsMap} widgetMap to extract from
47774
- * @return {PerseusWidgetsMap} a new widget map with requested widgets
47775
- */
47776
- function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
47777
- const widgets = {};
47778
- widgetIds.forEach(widgetId => {
47779
- const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
47780
- if (widget) {
47781
- widgets[widgetId] = widget;
47782
- }
47783
- });
47784
- return widgets;
47785
- }
47786
-
47787
48040
  /**
47788
48041
  * This function extracts the answers from the widgets.
47789
48042
  *
@@ -49785,6 +50038,8 @@ exports.buildMapper = buildMapper;
49785
50038
  exports.components = components;
49786
50039
  exports.containerSizeClass = containerSizeClass;
49787
50040
  exports.contentHasWidgetType = contentHasWidgetType;
50041
+ exports.conversionRequired = conversionRequired;
50042
+ exports.convertDeprecatedWidgets = convertDeprecatedWidgets;
49788
50043
  exports.convertWidgetNameToEnum = convertWidgetNameToEnum;
49789
50044
  exports.displaySigFigs = displaySigFigs;
49790
50045
  exports.generateTestCategorizerWidget = generateTestCategorizerWidget;
@@ -49836,6 +50091,7 @@ exports.parsePerseusItem = parsePerseusItem;
49836
50091
  exports.plotterPlotTypes = plotterPlotTypes;
49837
50092
  exports.preprocessTex = preprocessTex;
49838
50093
  exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
50094
+ exports.scorePerseusItem = scorePerseusItem;
49839
50095
  exports.shapes = shapes;
49840
50096
  exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
49841
50097
  exports.traverse = traverse;