@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/es/index.js CHANGED
@@ -241,9 +241,10 @@ const mockStrings = {
241
241
  removePoint: "Remove Point",
242
242
  graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
243
243
  srPointAtCoordinates: ({
244
+ num,
244
245
  x,
245
246
  y
246
- }) => `Point at ${x} comma ${y}`,
247
+ }) => `Point ${num} at ${x} comma ${y}`,
247
248
  srInteractiveElements: ({
248
249
  elements
249
250
  }) => `Interactive elements: ${elements}`,
@@ -18673,6 +18674,321 @@ class QuestionParagraph extends React.Component {
18673
18674
  }
18674
18675
  }
18675
18676
 
18677
+ // Methods to convert input-number widgets to numeric-input widgets
18678
+
18679
+ const inputNumberToNumericInput = json => {
18680
+ // First we need to create a map of the old input-number keys to the new numeric-input keys
18681
+ // so that we can ensure we update the content and widgets accordingly
18682
+ const renameMap = getInputNumberRenameMap(json);
18683
+
18684
+ // Then we can use this to update the JSON
18685
+ return convertInputNumberJson(json, renameMap);
18686
+ };
18687
+
18688
+ // We need to be able to run this code recursively in order to convert nested input-number widgets
18689
+ const convertInputNumberJson = (json, renameMap) => {
18690
+ const updatedContent = convertDeprecatedWidgetsInContent(json, renameMap);
18691
+ const updatedWidgets = convertInputNumberWidgetOptions(json, renameMap);
18692
+ const modernizedJson = _extends({}, json, {
18693
+ content: updatedContent,
18694
+ widgets: updatedWidgets
18695
+ });
18696
+ return modernizedJson;
18697
+ };
18698
+
18699
+ // Convert the input-number json in the widgets of a PerseusRenderer object
18700
+ const convertInputNumberWidgetOptions = (json, renameMap) => {
18701
+ const widgets = _extends({}, json.widgets);
18702
+ // The question.widgets is a dictionary map of widgets, so we need to loop through in order to convert input-number to numeric-input
18703
+ // which can exist as both the key or as a value on widget.type.
18704
+ for (const key of Object.keys(widgets)) {
18705
+ // Loop through the keys of the widgets dictionary
18706
+ if (widgets[key].options.widgets) {
18707
+ widgets[key].options = _extends({}, inputNumberToNumericInput(widgets[key].options));
18708
+ }
18709
+ // Check if the widget is an input-number
18710
+ if (widgets[key].type === "input-number") {
18711
+ // If the answerType is not number or percent, we need to provide
18712
+ // the answer form for the numeric-input widget
18713
+ const provideAnswerForm = widgets[key].options.answerType !== "number" && widgets[key].options.answerType !== "percent";
18714
+ // We need to determine the mathFormat for the numeric-input widget
18715
+ const mathFormat = widgets[key].options.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
18716
+ : widgets[key].options.answerType; // Otherwise, we can use the answerType directly
18717
+
18718
+ // We need to update the answers prop to match the numeric-input widget format
18719
+ const answers = [{
18720
+ value: widgets[key].options.value,
18721
+ simplify: widgets[key].options.simplify,
18722
+ // Input Number doesn't have a strict prop, so we default to false
18723
+ strict: false,
18724
+ // We only want to set maxError if the inexact prop is true
18725
+ maxError: widgets[key].options.inexact ? widgets[key].options.maxError : 0,
18726
+ status: "correct",
18727
+ // Input-number only allows correct answers
18728
+ message: ""
18729
+ }];
18730
+
18731
+ // Add the required answerForms if provided/applicable
18732
+ if (provideAnswerForm) {
18733
+ answers[0].answerForms = [mathFormat];
18734
+ }
18735
+
18736
+ // Update the options prop to match the numeric-input widget format
18737
+ const upgradedWidgetOptions = {
18738
+ answers,
18739
+ size: widgets[key].options.size,
18740
+ coefficient: false,
18741
+ // input-number doesn't have a coefficient prop
18742
+ labelText: "",
18743
+ // input-number doesn't have a labelText prop
18744
+ static: false,
18745
+ // static is always false for numeric-input
18746
+ rightAlign: widgets[key].options.rightAlign || false
18747
+ };
18748
+ const upgradedWidget = {
18749
+ options: upgradedWidgetOptions,
18750
+ type: "numeric-input"
18751
+ };
18752
+ const newWidgetName = renameMap[key];
18753
+ // Create the new key entry
18754
+ widgets[newWidgetName] = upgradedWidget;
18755
+
18756
+ // We need to delete the old widget key
18757
+ delete widgets[key];
18758
+ }
18759
+ }
18760
+ return widgets;
18761
+ };
18762
+
18763
+ // Convert the deprecated widget refs in the content string
18764
+ // of a PerseusRenderer object to their renamed equivalents
18765
+ const convertDeprecatedWidgetsInContent = (json, renameMap) => {
18766
+ return Object.keys(renameMap).reduce((newContent, oldKey) => {
18767
+ const newKey = renameMap[oldKey];
18768
+ return newKey ? newContent.replace(oldKey, newKey) : newContent;
18769
+ }, json.content);
18770
+ };
18771
+
18772
+ // Create a map of the old input-number keys to the new numeric-input keys
18773
+ const getInputNumberRenameMap = json => {
18774
+ const numericRegex = /(?<=\[\[\u2603 )(numeric-input \d+)(?=\]\])/g;
18775
+ const inputNumberRegex = /(?<=\[\[\u2603 )(input-number \d+)(?=\]\])/g;
18776
+
18777
+ // Get all the content strings within the json, which might be nested within widgets
18778
+ const allContentStrings = json.content;
18779
+
18780
+ // Loop through the content strings to get all the input-number widgets
18781
+ const renameMap = {};
18782
+ const inputNumberMatches = [...(allContentStrings.match(inputNumberRegex) || [])];
18783
+
18784
+ // We want to count any pre-existing numeric-input widgets
18785
+ // so that we can start the new ids at the next number
18786
+ const numericMatches = [...(allContentStrings.match(numericRegex) || [])];
18787
+ let currentNumericCount = numericMatches.reduce((count, match) => {
18788
+ const id = parseInt(match.split(" ")[1], 10);
18789
+ return id >= count ? id + 1 : count; // We want to start at the next highest number
18790
+ }, 1);
18791
+
18792
+ // Now that we have all the necessary information, we can create the renameMap
18793
+ for (const match of inputNumberMatches) {
18794
+ const oldKey = match;
18795
+ const newKey = `numeric-input ${currentNumericCount}`;
18796
+ renameMap[oldKey] = newKey;
18797
+ currentNumericCount++;
18798
+ }
18799
+ return renameMap;
18800
+ };
18801
+
18802
+ // Convert the user input data keys from input-number to numeric-input
18803
+ const convertUserInputNumberData = userInputMap => {
18804
+ const updatedUserInputMap = _extends({}, userInputMap);
18805
+ for (const key of Object.keys(userInputMap)) {
18806
+ if (key.includes("input-number")) {
18807
+ const updatedKey = key.replace("input-number", "numeric-input");
18808
+ updatedUserInputMap[updatedKey] = userInputMap[key];
18809
+ delete updatedUserInputMap[key];
18810
+ }
18811
+ }
18812
+ return updatedUserInputMap;
18813
+ };
18814
+
18815
+ const widgetRegExes = [/input-number \d+/]; // We can add more regexes here in the future
18816
+
18817
+ // This utils in this file are used to modernize our Perseus JSON structure,
18818
+ // so that we can convert deprecated widgets to their modern equivalents when
18819
+ // content creators use the Editor Page to update content containing these widgets.
18820
+ // Currently, we're only converting input-number to numeric-input,
18821
+ // but we can add more conversions here in the future.
18822
+ // Modernize the json content of a PerseusRenderer object
18823
+ // by converting deprecated widgets to their modern equivalents
18824
+ const convertDeprecatedWidgets = json => {
18825
+ // Currently we're only converting input-number to numeric-input,
18826
+ // But we can add more conversions here in the future
18827
+ return inputNumberToNumericInput(json);
18828
+ };
18829
+ const conversionRequired = json => {
18830
+ // If there's no content, then there's no conversion required
18831
+ if (!json.content) {
18832
+ return false;
18833
+ }
18834
+ // Check the content string for any top-level input-number widgets
18835
+ if (widgetRegExes.some(regex => regex.test(json.content))) {
18836
+ return true;
18837
+ }
18838
+
18839
+ // If there's no deprecated widget in the top-level, then check for any nested widgets
18840
+ for (const key of Object.keys(json.widgets)) {
18841
+ if (json.widgets[key].options.widgets) {
18842
+ const nestedJson = json.widgets[key].options;
18843
+ if (conversionRequired(nestedJson)) {
18844
+ return true;
18845
+ }
18846
+ }
18847
+ }
18848
+ return false;
18849
+ };
18850
+
18851
+ // Convert the user input data keys for deprecated widgets to their modern equivalents
18852
+ const convertUserInputData = userInputMap => {
18853
+ // Currently we're only converting input-number to numeric-input,
18854
+ // But we can add more conversions here in the future
18855
+ return convertUserInputNumberData(userInputMap);
18856
+ };
18857
+
18858
+ // These are all the current widgets that require user-input and are supported for automatic scoring
18859
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
18860
+
18861
+ /**
18862
+ * Regex for widget placeholders in a string.
18863
+ *
18864
+ * First capture group is the widget ID (ex. 'radio 1')
18865
+ * Second capture group is the widget type (ex. "radio)
18866
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
18867
+ */
18868
+ function getWidgetRegex() {
18869
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
18870
+ }
18871
+
18872
+ /**
18873
+ * Add a widget placeholder using the widget ID.
18874
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
18875
+ *
18876
+ * @param {string} id
18877
+ * @returns {string}
18878
+ */
18879
+ function addWidget(id) {
18880
+ return `[[☃ ${id}]]`;
18881
+ }
18882
+
18883
+ /**
18884
+ * Get a widget type by a widget's ID
18885
+ * (please don't derive type from ID)
18886
+ *
18887
+ * @param {string} widgetId the ID of the widget
18888
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18889
+ * @returns {string} the widget type (ie "radio")
18890
+ */
18891
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
18892
+ var _widget$type;
18893
+ const widget = widgetMap[WidgetId];
18894
+ return (_widget$type = widget == null ? void 0 : widget.type) != null ? _widget$type : null;
18895
+ }
18896
+
18897
+ /**
18898
+ * Does the content have a specific type of widget?
18899
+ *
18900
+ * @param {string} type the type of the widget in question (ie "radio")
18901
+ * @param {string} content the string to search through
18902
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18903
+ * @returns {boolean} if the content includes the widget type
18904
+ */
18905
+ function contentHasWidgetType(type, content, widgetMap) {
18906
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
18907
+ }
18908
+
18909
+ /**
18910
+ * Extract all widget IDs, which includes the widget type and instance number.
18911
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
18912
+ *
18913
+ * Content should contain Perseus widget placeholders,
18914
+ * which look like: '[[☃ radio 1]]'.
18915
+ *
18916
+ * @param {string} content
18917
+ * @returns {ReadonlyArray<string>} widgetIds
18918
+ */
18919
+ function getWidgetIdsFromContent(content) {
18920
+ const widgets = [];
18921
+ const localWidgetRegex = getWidgetRegex();
18922
+ let match = localWidgetRegex.exec(content);
18923
+ while (match !== null) {
18924
+ widgets.push(match[1]);
18925
+ match = localWidgetRegex.exec(content);
18926
+ }
18927
+ return widgets;
18928
+ }
18929
+
18930
+ /**
18931
+ * Get a list of widget IDs from content,
18932
+ * but only for specific widget types
18933
+ *
18934
+ * @param {string} type the type of widget (ie "radio")
18935
+ * @param {string} content the string to parse
18936
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18937
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
18938
+ */
18939
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
18940
+ const rv = [];
18941
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
18942
+ widgetIdsInContent.forEach(widgetId => {
18943
+ const widget = widgetMap[widgetId];
18944
+ if ((widget == null ? void 0 : widget.type) === type) {
18945
+ rv.push(widgetId);
18946
+ }
18947
+ });
18948
+ return rv;
18949
+ }
18950
+
18951
+ /**
18952
+ * Pull the widget map out of ItemData
18953
+ *
18954
+ * @param {PerseusItem} PerseusItem containing a widgetMap
18955
+ * @returns {WidgetMap} the widget map in the PerseusItem
18956
+ */
18957
+ function getWidgetsMapFromItemData(itemData) {
18958
+ return itemData.question.widgets;
18959
+ }
18960
+
18961
+ /**
18962
+ * Get the widget information from a WidgetMap
18963
+ *
18964
+ * @param {string} widgetId the ID of the widget
18965
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
18966
+ */
18967
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
18968
+ var _widgetMap$widgetId;
18969
+ return (_widgetMap$widgetId = widgetMap[widgetId]) != null ? _widgetMap$widgetId : null;
18970
+ }
18971
+
18972
+ /**
18973
+ * Get select widgets from a widget map.
18974
+ * Useful for multi-items that needs to split
18975
+ * one PerseusItem into several
18976
+ *
18977
+ * @param {ReadonlyArray<string>} widgetIds to extract
18978
+ * @param {PerseusWidgetsMap} widgetMap to extract from
18979
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
18980
+ */
18981
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
18982
+ const widgets = {};
18983
+ widgetIds.forEach(widgetId => {
18984
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
18985
+ if (widget) {
18986
+ widgets[widgetId] = widget;
18987
+ }
18988
+ });
18989
+ return widgets;
18990
+ }
18991
+
18676
18992
  const DEFAULT_ALIGNMENT$1 = "block";
18677
18993
  // NOTE(kevinb): "default" is not one in `validAlignments`.
18678
18994
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -19134,6 +19450,27 @@ widgetIds, userInputMap, strings, locale) {
19134
19450
  }
19135
19451
  });
19136
19452
  }
19453
+
19454
+ // TODO: combine scorePerseusItem with scoreWidgetsFunctional
19455
+ // once scorePerseusItem is the only one calling scoreWidgetsFunctional
19456
+ function scorePerseusItem(perseusRenderData, userInputMap,
19457
+ // TODO(LEMS-2461,LEMS-2391): these probably
19458
+ // need to be removed before we move this to the server
19459
+ strings, locale) {
19460
+ // Check if the PerseusRenderer object contains any deprecated widgets that need to be converted
19461
+ const mustConvertData = conversionRequired(perseusRenderData);
19462
+ const convertedRenderData = mustConvertData ? convertDeprecatedWidgets(perseusRenderData) // Convert deprecated widgets to their modern equivalents
19463
+ : perseusRenderData;
19464
+ const convertedUserInputMap = mustConvertData ? convertUserInputData(userInputMap) // Convert deprecated user input data keys to their modern equivalents
19465
+ : userInputMap;
19466
+
19467
+ // There seems to be a chance that PerseusRenderer.widgets might include
19468
+ // widget data for widgets that are not in PerseusRenderer.content,
19469
+ // so this checks that the widgets are being used before scoring them
19470
+ const usedWidgetIds = getWidgetIdsFromContent(convertedRenderData.content);
19471
+ const scores = scoreWidgetsFunctional(convertedRenderData.widgets, usedWidgetIds, convertedUserInputMap, strings, locale);
19472
+ return Util.flattenScores(scores);
19473
+ }
19137
19474
  function scoreWidgetsFunctional(widgets,
19138
19475
  // This is a port of old code, I'm not sure why
19139
19476
  // we need widgetIds vs the keys of the widgets object
@@ -19154,7 +19491,7 @@ widgetIds, userInputMap, strings, locale) {
19154
19491
  const userInput = userInputMap[id];
19155
19492
  const scorer = getWidgetScorer(widget.type);
19156
19493
  if (widget.type === "group") {
19157
- const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19494
+ const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
19158
19495
  widgetScores[id] = Util.flattenScores(scores);
19159
19496
  } else if (scorer) {
19160
19497
  widgetScores[id] = scorer(userInput, widget.options, strings, locale);
@@ -19986,7 +20323,7 @@ class Renderer extends React.Component {
19986
20323
  // Instead, we center them with css magic in styles.less
19987
20324
  // /cry(aria)
19988
20325
  this._foundTextNodes = true;
19989
- if (_.contains(this.widgetIds, node.id)) {
20326
+ if (this.widgetIds.includes(node.id)) {
19990
20327
  // We don't want to render a duplicate widget key/ref,
19991
20328
  // as this causes problems with react (for obvious
19992
20329
  // reasons). Instead we just notify the
@@ -20290,7 +20627,7 @@ class Renderer extends React.Component {
20290
20627
  };
20291
20628
  this.getInputPaths = () => {
20292
20629
  const inputPaths = [];
20293
- _.each(this.widgetIds, widgetId => {
20630
+ this.widgetIds.forEach(widgetId => {
20294
20631
  const widget = this.getWidgetInstance(widgetId);
20295
20632
  if (widget && widget.getInputPaths) {
20296
20633
  // Grab all input paths and add widgetID to the front
@@ -20298,7 +20635,7 @@ class Renderer extends React.Component {
20298
20635
  // Prefix paths with their widgetID and add to collective
20299
20636
  // list of paths.
20300
20637
  // @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
20301
- _.each(widgetInputPaths, inputPath => {
20638
+ widgetInputPaths.forEach(inputPath => {
20302
20639
  const relativeInputPath = [widgetId].concat(inputPath);
20303
20640
  inputPaths.push(relativeInputPath);
20304
20641
  });
@@ -20427,12 +20764,20 @@ class Renderer extends React.Component {
20427
20764
  this.getWidgetIds = () => {
20428
20765
  return this.widgetIds;
20429
20766
  };
20767
+ /**
20768
+ * @deprecated use scorePerseusItem
20769
+ */
20770
+ this.guessAndScore = () => {
20771
+ const totalGuess = this.getUserInput();
20772
+ const totalScore = this.score();
20773
+ return [totalGuess, totalScore];
20774
+ };
20430
20775
  this.examples = () => {
20431
20776
  const widgetIds = this.widgetIds;
20432
- const examples = _.compact(_.map(widgetIds, widgetId => {
20777
+ const examples = widgetIds.map(widgetId => {
20433
20778
  const widget = this.getWidgetInstance(widgetId);
20434
20779
  return widget != null && widget.examples ? widget.examples() : null;
20435
- }));
20780
+ }).filter(Boolean);
20436
20781
 
20437
20782
  // no widgets with examples
20438
20783
  if (!examples.length) {
@@ -20687,27 +21032,15 @@ class Renderer extends React.Component {
20687
21032
  }
20688
21033
 
20689
21034
  /**
20690
- * Returns an object mapping from widget ID to perseus-style score.
20691
- * The keys of this object are the values of the array returned
20692
- * from `getWidgetIds`.
20693
- */
20694
- scoreWidgets() {
20695
- return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20696
- }
20697
-
20698
- /**
20699
- * Grades the content.
21035
+ * Scores the content.
21036
+ *
21037
+ * @deprecated use scorePerseusItem
20700
21038
  */
20701
21039
  score() {
20702
- const scores = this.scoreWidgets();
21040
+ const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20703
21041
  const combinedScore = Util.flattenScores(scores);
20704
21042
  return combinedScore;
20705
21043
  }
20706
- guessAndScore() {
20707
- const totalGuess = this.getUserInput();
20708
- const totalScore = this.score();
20709
- return [totalGuess, totalScore];
20710
- }
20711
21044
  render() {
20712
21045
  const apiOptions = this.getApiOptions();
20713
21046
  if (this.reuseMarkdown) {
@@ -22247,7 +22580,9 @@ const propUpgrades$2 = {
22247
22580
  // input-number to numeric-input. In this case, we need to upgrade
22248
22581
  // the widget options accordingly.
22249
22582
  if (initialProps.value) {
22250
- const provideAnswerForm = initialProps.answerType !== "number";
22583
+ // If the answerType is not number or percent, we need to provide
22584
+ // the answer form for the numeric-input widget
22585
+ const provideAnswerForm = initialProps.answerType !== "number" && initialProps.answerType !== "percent";
22251
22586
 
22252
22587
  // We need to determine the mathFormat for the numeric-input widget
22253
22588
  const mathFormat = initialProps.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
@@ -29255,6 +29590,9 @@ class Group extends React.Component {
29255
29590
  this.change("widgets", this.rendererRef.props.widgets);
29256
29591
  }
29257
29592
  };
29593
+
29594
+ // TODO(LEMS-2391): replace this when there's a separate check
29595
+ // for valid/invalid state
29258
29596
  const score = (_this$rendererRef12 = this.rendererRef) == null ? void 0 : _this$rendererRef12.score();
29259
29597
  const isValid = score && score.type !== "invalid";
29260
29598
  const isInvalid = score && score.type === "invalid";
@@ -31073,6 +31411,107 @@ function AxisArrows() {
31073
31411
  })));
31074
31412
  }
31075
31413
 
31414
+ /**
31415
+ * 44 is touch best practice and AAA compliant for WCAG
31416
+ * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
31417
+ */
31418
+ const TARGET_SIZE = 44;
31419
+ const REMOVE_BUTTON_ID = "perseus_mafs_remove_button";
31420
+
31421
+ // same as pointsFromNormalized in interactive-graph.tsx
31422
+ const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
31423
+ // maps over [x, y]
31424
+ coords.map((coord, i) => {
31425
+ const axisRange = range[i];
31426
+ if (noSnap) {
31427
+ return axisRange[MIN] + size(axisRange) * coord;
31428
+ }
31429
+ const axisStep = step[i];
31430
+ const nSteps = Math.floor(size(axisRange) / axisStep);
31431
+ const tick = Math.round(coord * nSteps);
31432
+ return axisRange[MIN] + axisStep * tick;
31433
+ }));
31434
+
31435
+ // same as normalizeCoords in interactive-graph.tsx
31436
+ const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
31437
+ return (coord + ranges[i][1]) / size(ranges[i]);
31438
+ }));
31439
+
31440
+ // Returns the closest point to the given `point` that is within the graph
31441
+ // bounds given in `state`.
31442
+ function bound$1({
31443
+ snapStep,
31444
+ range,
31445
+ point
31446
+ }) {
31447
+ const boundingBox = inset(snapStep, range);
31448
+ return clampToBox(boundingBox, point);
31449
+ }
31450
+ function isUnlimitedGraphState(state) {
31451
+ return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
31452
+ }
31453
+
31454
+ /**
31455
+ * Replace all text outside of the $ TeX blocks with `\\text{...}`
31456
+ * This way, the entire resulting string can be rendered within <TeX>
31457
+ * and the text outside of the $ blocks will be non-TeX text.
31458
+ */
31459
+ function replaceOutsideTeX(mathString) {
31460
+ // All the information we need is in the first section,
31461
+ // whether it's typed as "blockmath" or "paragraph"
31462
+ const firstSection = SimpleMarkdown.parse(mathString)[0];
31463
+
31464
+ // If it's blockMath, the outer level has the full math content.
31465
+ if (firstSection.type === "blockMath") {
31466
+ return firstSection.content;
31467
+ }
31468
+
31469
+ // If it's a paragraph, we need to iterate through the sections
31470
+ // to look for individual math blocks.
31471
+ const condensedNodes = condenseTextNodes(firstSection.content);
31472
+ let result = "";
31473
+ for (const piece of condensedNodes) {
31474
+ piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
31475
+ }
31476
+ return result;
31477
+ }
31478
+ // Helper function for replaceOutsideTeX()
31479
+ // Condense adjacent text nodes into a single text node
31480
+ function condenseTextNodes(nodes) {
31481
+ const result = [];
31482
+ if (!nodes) {
31483
+ return result;
31484
+ }
31485
+ let currentText = "";
31486
+ for (const node of nodes) {
31487
+ if (node.type === "math") {
31488
+ if (currentText) {
31489
+ result.push({
31490
+ type: "text",
31491
+ content: currentText
31492
+ });
31493
+ currentText = "";
31494
+ }
31495
+ result.push(node);
31496
+ } else {
31497
+ currentText += node.content;
31498
+ }
31499
+ }
31500
+ if (currentText) {
31501
+ result.push({
31502
+ type: "text",
31503
+ content: currentText
31504
+ });
31505
+ }
31506
+ return result;
31507
+ }
31508
+
31509
+ // Helper function for replaceOutsideTeX()
31510
+ function escapeSpecialChars(str) {
31511
+ // Escape $, \, {, and } characters
31512
+ return str.replace(/([$\\{}])/g, "\\$1");
31513
+ }
31514
+
31076
31515
  function AxisLabels() {
31077
31516
  const {
31078
31517
  range,
@@ -31104,7 +31543,7 @@ function AxisLabels() {
31104
31543
  fontSize: "14px",
31105
31544
  transform: "translate(7px, -50%)"
31106
31545
  }
31107
- }, /*#__PURE__*/React__default.createElement(TeX, null, xAxisLabelText)), /*#__PURE__*/React__default.createElement("span", {
31546
+ }, /*#__PURE__*/React__default.createElement(TeX, null, replaceOutsideTeX(xAxisLabelText))), /*#__PURE__*/React__default.createElement("span", {
31108
31547
  style: {
31109
31548
  position: "absolute",
31110
31549
  left: x2,
@@ -31112,7 +31551,7 @@ function AxisLabels() {
31112
31551
  fontSize: "14px",
31113
31552
  transform: "translate(-50%, 0px)"
31114
31553
  }
31115
- }, /*#__PURE__*/React__default.createElement(TeX, null, yAxisLabelText)));
31554
+ }, /*#__PURE__*/React__default.createElement(TeX, null, replaceOutsideTeX(yAxisLabelText))));
31116
31555
  }
31117
31556
 
31118
31557
  // The size of the ticks and labels in pixels
@@ -32046,107 +32485,6 @@ const lockedFigureFillStyles = {
32046
32485
 
32047
32486
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
32048
32487
 
32049
- /**
32050
- * 44 is touch best practice and AAA compliant for WCAG
32051
- * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
32052
- */
32053
- const TARGET_SIZE = 44;
32054
- const REMOVE_BUTTON_ID = "perseus_mafs_remove_button";
32055
-
32056
- // same as pointsFromNormalized in interactive-graph.tsx
32057
- const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
32058
- // maps over [x, y]
32059
- coords.map((coord, i) => {
32060
- const axisRange = range[i];
32061
- if (noSnap) {
32062
- return axisRange[MIN] + size(axisRange) * coord;
32063
- }
32064
- const axisStep = step[i];
32065
- const nSteps = Math.floor(size(axisRange) / axisStep);
32066
- const tick = Math.round(coord * nSteps);
32067
- return axisRange[MIN] + axisStep * tick;
32068
- }));
32069
-
32070
- // same as normalizeCoords in interactive-graph.tsx
32071
- const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
32072
- return (coord + ranges[i][1]) / size(ranges[i]);
32073
- }));
32074
-
32075
- // Returns the closest point to the given `point` that is within the graph
32076
- // bounds given in `state`.
32077
- function bound$1({
32078
- snapStep,
32079
- range,
32080
- point
32081
- }) {
32082
- const boundingBox = inset(snapStep, range);
32083
- return clampToBox(boundingBox, point);
32084
- }
32085
- function isUnlimitedGraphState(state) {
32086
- return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
32087
- }
32088
-
32089
- /**
32090
- * Replace all text outside of the $ TeX blocks with `\\text{...}`
32091
- * This way, the entire resulting string can be rendered within <TeX>
32092
- * and the text outside of the $ blocks will be non-TeX text.
32093
- */
32094
- function replaceOutsideTeX(mathString) {
32095
- // All the information we need is in the first section,
32096
- // whether it's typed as "blockmath" or "paragraph"
32097
- const firstSection = SimpleMarkdown.parse(mathString)[0];
32098
-
32099
- // If it's blockMath, the outer level has the full math content.
32100
- if (firstSection.type === "blockMath") {
32101
- return firstSection.content;
32102
- }
32103
-
32104
- // If it's a paragraph, we need to iterate through the sections
32105
- // to look for individual math blocks.
32106
- const condensedNodes = condenseTextNodes(firstSection.content);
32107
- let result = "";
32108
- for (const piece of condensedNodes) {
32109
- piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
32110
- }
32111
- return result;
32112
- }
32113
- // Helper function for replaceOutsideTeX()
32114
- // Condense adjacent text nodes into a single text node
32115
- function condenseTextNodes(nodes) {
32116
- const result = [];
32117
- if (!nodes) {
32118
- return result;
32119
- }
32120
- let currentText = "";
32121
- for (const node of nodes) {
32122
- if (node.type === "math") {
32123
- if (currentText) {
32124
- result.push({
32125
- type: "text",
32126
- content: currentText
32127
- });
32128
- currentText = "";
32129
- }
32130
- result.push(node);
32131
- } else {
32132
- currentText += node.content;
32133
- }
32134
- }
32135
- if (currentText) {
32136
- result.push({
32137
- type: "text",
32138
- content: currentText
32139
- });
32140
- }
32141
- return result;
32142
- }
32143
-
32144
- // Helper function for replaceOutsideTeX()
32145
- function escapeSpecialChars(str) {
32146
- // Escape $, \, {, and } characters
32147
- return str.replace(/([$\\{}])/g, "\\$1");
32148
- }
32149
-
32150
32488
  function LockedLabel(props) {
32151
32489
  const {
32152
32490
  coord,
@@ -33369,6 +33707,7 @@ function useControlPoint(params) {
33369
33707
  } = useGraphConfig();
33370
33708
  const {
33371
33709
  point,
33710
+ sequenceNumber,
33372
33711
  color,
33373
33712
  cursor,
33374
33713
  constrain = p => snap(snapStep, p),
@@ -33409,6 +33748,7 @@ function useControlPoint(params) {
33409
33748
  ref: focusableHandleRef,
33410
33749
  role: "button",
33411
33750
  "aria-label": strings.srPointAtCoordinates({
33751
+ num: sequenceNumber,
33412
33752
  x: srFormatNumber(point[X], locale),
33413
33753
  y: srFormatNumber(point[Y], locale)
33414
33754
  })
@@ -33487,6 +33827,7 @@ const MovableLine = props => {
33487
33827
  focusableHandle: focusableHandle1
33488
33828
  } = useControlPoint({
33489
33829
  point: start,
33830
+ sequenceNumber: 1,
33490
33831
  color,
33491
33832
  onMove: p => onMovePoint(0, p)
33492
33833
  });
@@ -33495,6 +33836,7 @@ const MovableLine = props => {
33495
33836
  focusableHandle: focusableHandle2
33496
33837
  } = useControlPoint({
33497
33838
  point: end,
33839
+ sequenceNumber: 2,
33498
33840
  color,
33499
33841
  onMove: p => onMovePoint(1, p)
33500
33842
  });
@@ -33674,14 +34016,17 @@ function AngleGraph(props) {
33674
34016
  // Render the lines, angle, and then movable points
33675
34017
  return /*#__PURE__*/React.createElement(React.Fragment, null, svgLines, /*#__PURE__*/React.createElement(Angle, angleParams), /*#__PURE__*/React.createElement(MovablePoint$1, {
33676
34018
  point: coords[1],
34019
+ sequenceNumber: 1,
33677
34020
  constrain: p => p,
33678
34021
  onMove: destination => dispatch(actions.angle.movePoint(1, destination))
33679
34022
  }), /*#__PURE__*/React.createElement(MovablePoint$1, {
33680
34023
  point: coords[0],
34024
+ sequenceNumber: 2,
33681
34025
  constrain: getAngleSideConstraint(coords[0], coords[1], snapDegrees || 1),
33682
34026
  onMove: destination => dispatch(actions.angle.movePoint(0, destination))
33683
34027
  }), /*#__PURE__*/React.createElement(MovablePoint$1, {
33684
34028
  point: coords[2],
34029
+ sequenceNumber: 3,
33685
34030
  constrain: getAngleSideConstraint(coords[2], coords[1], snapDegrees || 1),
33686
34031
  onMove: destination => dispatch(actions.angle.movePoint(2, destination))
33687
34032
  }));
@@ -33835,6 +34180,7 @@ function CircleGraph(props) {
33835
34180
  onMove: c => dispatch(actions.circle.moveCenter(c))
33836
34181
  }), /*#__PURE__*/React.createElement(MovablePoint$1, {
33837
34182
  point: radiusPoint,
34183
+ sequenceNumber: 1,
33838
34184
  cursor: "ew-resize",
33839
34185
  onMove: newRadiusPoint => {
33840
34186
  dispatch(actions.circle.moveRadiusPoint(newRadiusPoint));
@@ -34010,6 +34356,7 @@ function LimitedPointGraph(props) {
34010
34356
  return /*#__PURE__*/React.createElement(React.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34011
34357
  key: i,
34012
34358
  point: point,
34359
+ sequenceNumber: i + 1,
34013
34360
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination))
34014
34361
  })));
34015
34362
  }
@@ -34052,6 +34399,7 @@ function UnlimitedPointGraph(props) {
34052
34399
  }), props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34053
34400
  key: i,
34054
34401
  point: point,
34402
+ sequenceNumber: i + 1,
34055
34403
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
34056
34404
  ref: ref => {
34057
34405
  itemsRef.current[i] = ref;
@@ -34082,7 +34430,8 @@ function describePointGraph(state, i18n) {
34082
34430
  if (state.coords.length === 0) {
34083
34431
  return strings.srNoInteractiveElements;
34084
34432
  }
34085
- const pointDescriptions = state.coords.map(([x, y]) => strings.srPointAtCoordinates({
34433
+ const pointDescriptions = state.coords.map(([x, y], index) => strings.srPointAtCoordinates({
34434
+ num: index + 1,
34086
34435
  x: srFormatNumber(x, locale),
34087
34436
  y: srFormatNumber(y, locale)
34088
34437
  }));
@@ -34200,6 +34549,7 @@ const LimitedPolygonGraph = props => {
34200
34549
  key: "point-" + i,
34201
34550
  constrain: constrain,
34202
34551
  point: point,
34552
+ sequenceNumber: i + 1,
34203
34553
  onMove: destination => {
34204
34554
  const now = Date.now();
34205
34555
  const targetFPS = 40;
@@ -34346,6 +34696,7 @@ const UnlimitedPolygonGraph = props => {
34346
34696
  }), props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34347
34697
  key: i,
34348
34698
  point: point,
34699
+ sequenceNumber: i + 1,
34349
34700
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
34350
34701
  ref: ref => {
34351
34702
  pointRef.current[i] = ref;
@@ -34421,6 +34772,7 @@ function QuadraticGraph(props) {
34421
34772
  }), coords.map((coord, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34422
34773
  key: "point-" + i,
34423
34774
  point: coord,
34775
+ sequenceNumber: i + 1,
34424
34776
  onMove: destination => dispatch(actions.quadratic.movePoint(i, destination))
34425
34777
  })));
34426
34778
  }
@@ -34549,6 +34901,7 @@ function SinusoidGraph(props) {
34549
34901
  }), coords.map((coord, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34550
34902
  key: "point-" + i,
34551
34903
  point: coord,
34904
+ sequenceNumber: i + 1,
34552
34905
  onMove: destination => dispatch(actions.sinusoid.movePoint(i, destination))
34553
34906
  })));
34554
34907
  }
@@ -44677,7 +45030,7 @@ const init = function init() {
44677
45030
 
44678
45031
  // This file is processed by a Rollup plugin (replace) to inject the production
44679
45032
  const libName = "@khanacademy/perseus";
44680
- const libVersion = "41.5.0";
45033
+ const libVersion = "42.0.1";
44681
45034
  addLibraryVersionToPerseusDebug(libName, libVersion);
44682
45035
 
44683
45036
  const apiVersion = {
@@ -45301,6 +45654,13 @@ class ServerItemRenderer extends React.Component {
45301
45654
  this.props.onRendered(true);
45302
45655
  }
45303
45656
  }
45657
+ if (this.props.score && this.props.score !== prevProps.score) {
45658
+ const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45659
+ this.setState({
45660
+ questionCompleted: this.props.score.correct,
45661
+ questionHighlightedWidgets: emptyQuestionAreaWidgets
45662
+ });
45663
+ }
45304
45664
  }
45305
45665
  componentWillUnmount() {
45306
45666
  if (this.blurTimeoutID != null) {
@@ -45418,18 +45778,37 @@ class ServerItemRenderer extends React.Component {
45418
45778
  return this.questionRenderer.getPromptJSON();
45419
45779
  }
45420
45780
 
45781
+ /**
45782
+ * Returns an array of the widget `.getUserInput()` results
45783
+ *
45784
+ * TODO: can we remove this? Seems to be just for backwards
45785
+ * compatibility with old Perseus Chrome logging
45786
+ * @deprecated use getUserInput
45787
+ */
45788
+ getUserInputLegacy() {
45789
+ return this.questionRenderer.getUserInput();
45790
+ }
45791
+
45792
+ /**
45793
+ * Returns an object of the widget `.getUserInput()` results
45794
+ */
45795
+ getUserInput() {
45796
+ return this.questionRenderer.getUserInputMap();
45797
+ }
45798
+
45421
45799
  /**
45422
45800
  * Grades the item.
45801
+ *
45802
+ * @deprecated use scorePerseusItem
45423
45803
  */
45424
45804
  scoreInput() {
45425
- const guessAndScore = this.questionRenderer.guessAndScore();
45426
- const guess = guessAndScore[0];
45427
- const score = guessAndScore[1];
45805
+ const guess = this.getUserInput();
45806
+ const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
45428
45807
 
45429
45808
  // Continue to include an empty guess for the now defunct answer area.
45430
45809
  // TODO(alex): Check whether we rely on the format here for
45431
45810
  // analyzing ProblemLogs. If not, remove this layer.
45432
- const maxCompatGuess = [guess, []];
45811
+ const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
45433
45812
  const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
45434
45813
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45435
45814
  this.setState({
@@ -47277,140 +47656,6 @@ const registerAllWidgetsForTesting = () => {
47277
47656
  replaceDeprecatedWidgets();
47278
47657
  };
47279
47658
 
47280
- // These are all the current widgets that require user-input and are supported for automatic scoring
47281
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
47282
-
47283
- /**
47284
- * Regex for widget placeholders in a string.
47285
- *
47286
- * First capture group is the widget ID (ex. 'radio 1')
47287
- * Second capture group is the widget type (ex. "radio)
47288
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
47289
- */
47290
- function getWidgetRegex() {
47291
- return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
47292
- }
47293
-
47294
- /**
47295
- * Add a widget placeholder using the widget ID.
47296
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
47297
- *
47298
- * @param {string} id
47299
- * @returns {string}
47300
- */
47301
- function addWidget(id) {
47302
- return `[[☃ ${id}]]`;
47303
- }
47304
-
47305
- /**
47306
- * Get a widget type by a widget's ID
47307
- * (please don't derive type from ID)
47308
- *
47309
- * @param {string} widgetId the ID of the widget
47310
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47311
- * @returns {string} the widget type (ie "radio")
47312
- */
47313
- function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
47314
- var _widget$type;
47315
- const widget = widgetMap[WidgetId];
47316
- return (_widget$type = widget == null ? void 0 : widget.type) != null ? _widget$type : null;
47317
- }
47318
-
47319
- /**
47320
- * Does the content have a specific type of widget?
47321
- *
47322
- * @param {string} type the type of the widget in question (ie "radio")
47323
- * @param {string} content the string to search through
47324
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47325
- * @returns {boolean} if the content includes the widget type
47326
- */
47327
- function contentHasWidgetType(type, content, widgetMap) {
47328
- return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
47329
- }
47330
-
47331
- /**
47332
- * Extract all widget IDs, which includes the widget type and instance number.
47333
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
47334
- *
47335
- * Content should contain Perseus widget placeholders,
47336
- * which look like: '[[☃ radio 1]]'.
47337
- *
47338
- * @param {string} content
47339
- * @returns {ReadonlyArray<string>} widgetIds
47340
- */
47341
- function getWidgetIdsFromContent(content) {
47342
- const widgets = [];
47343
- const localWidgetRegex = getWidgetRegex();
47344
- let match = localWidgetRegex.exec(content);
47345
- while (match !== null) {
47346
- widgets.push(match[1]);
47347
- match = localWidgetRegex.exec(content);
47348
- }
47349
- return widgets;
47350
- }
47351
-
47352
- /**
47353
- * Get a list of widget IDs from content,
47354
- * but only for specific widget types
47355
- *
47356
- * @param {string} type the type of widget (ie "radio")
47357
- * @param {string} content the string to parse
47358
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47359
- * @returns {ReadonlyArray<string>} the widget type (ie "radio")
47360
- */
47361
- function getWidgetIdsFromContentByType(type, content, widgetMap) {
47362
- const rv = [];
47363
- const widgetIdsInContent = getWidgetIdsFromContent(content);
47364
- widgetIdsInContent.forEach(widgetId => {
47365
- const widget = widgetMap[widgetId];
47366
- if ((widget == null ? void 0 : widget.type) === type) {
47367
- rv.push(widgetId);
47368
- }
47369
- });
47370
- return rv;
47371
- }
47372
-
47373
- /**
47374
- * Pull the widget map out of ItemData
47375
- *
47376
- * @param {PerseusItem} PerseusItem containing a widgetMap
47377
- * @returns {WidgetMap} the widget map in the PerseusItem
47378
- */
47379
- function getWidgetsMapFromItemData(itemData) {
47380
- return itemData.question.widgets;
47381
- }
47382
-
47383
- /**
47384
- * Get the widget information from a WidgetMap
47385
- *
47386
- * @param {string} widgetId the ID of the widget
47387
- * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
47388
- */
47389
- function getWidgetFromWidgetMap(widgetId, widgetMap) {
47390
- var _widgetMap$widgetId;
47391
- return (_widgetMap$widgetId = widgetMap[widgetId]) != null ? _widgetMap$widgetId : null;
47392
- }
47393
-
47394
- /**
47395
- * Get select widgets from a widget map.
47396
- * Useful for multi-items that needs to split
47397
- * one PerseusItem into several
47398
- *
47399
- * @param {ReadonlyArray<string>} widgetIds to extract
47400
- * @param {PerseusWidgetsMap} widgetMap to extract from
47401
- * @return {PerseusWidgetsMap} a new widget map with requested widgets
47402
- */
47403
- function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
47404
- const widgets = {};
47405
- widgetIds.forEach(widgetId => {
47406
- const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
47407
- if (widget) {
47408
- widgets[widgetId] = widget;
47409
- }
47410
- });
47411
- return widgets;
47412
- }
47413
-
47414
47659
  /**
47415
47660
  * This function extracts the answers from the widgets.
47416
47661
  *
@@ -49355,5 +49600,5 @@ const GrapherUtil = {
49355
49600
  typeToButton
49356
49601
  };
49357
49602
 
49358
- export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusExpressionAnswerFormConsidered, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, addWidget, apiVersion, bodyXsmallBold, buildEmptyItemTreeForShape, buildMapper, components, containerSizeClass, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getClockwiseAngle, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, lockedFigureColors, lockedFigureFillStyles, makeSafeUrl, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
49603
+ export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusExpressionAnswerFormConsidered, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, addWidget, apiVersion, bodyXsmallBold, buildEmptyItemTreeForShape, buildMapper, components, containerSizeClass, contentHasWidgetType, conversionRequired, convertDeprecatedWidgets, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getClockwiseAngle, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, lockedFigureColors, lockedFigureFillStyles, makeSafeUrl, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, scorePerseusItem, shapes, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
49359
49604
  //# sourceMappingURL=index.js.map