@khanacademy/perseus 42.0.3 → 43.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.
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ var kmath = require('@khanacademy/kmath');
24
24
  var wonderStuffCore = require('@khanacademy/wonder-stuff-core');
25
25
  var wonderBlocksProgressSpinner = require('@khanacademy/wonder-blocks-progress-spinner');
26
26
  var SimpleMarkdown = require('@khanacademy/simple-markdown');
27
- var SimpleMarkdown$1 = require('@khanacademy/pure-markdown');
27
+ var pureMarkdown = require('@khanacademy/pure-markdown');
28
28
  var wonderBlocksForm = require('@khanacademy/wonder-blocks-form');
29
29
  var Button = require('@khanacademy/wonder-blocks-button');
30
30
  var wonderBlocksLayout = require('@khanacademy/wonder-blocks-layout');
@@ -77,7 +77,6 @@ var Clickable__default = /*#__PURE__*/_interopDefaultLegacy(Clickable);
77
77
  var $__default = /*#__PURE__*/_interopDefaultLegacy($);
78
78
  var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
79
79
  var SimpleMarkdown__default = /*#__PURE__*/_interopDefaultLegacy(SimpleMarkdown);
80
- var SimpleMarkdown__namespace = /*#__PURE__*/_interopNamespace(SimpleMarkdown$1);
81
80
  var Button__default = /*#__PURE__*/_interopDefaultLegacy(Button);
82
81
  var Banner__default = /*#__PURE__*/_interopDefaultLegacy(Banner);
83
82
  var caretDown__default = /*#__PURE__*/_interopDefaultLegacy(caretDown);
@@ -369,6 +368,8 @@ const mockStrings = {
369
368
  addPoint: "Add Point",
370
369
  removePoint: "Remove Point",
371
370
  graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
371
+ closePolygon: "Close shape",
372
+ openPolygon: "Re-open shape",
372
373
  srPointAtCoordinates: _ref27 => {
373
374
  let {
374
375
  num,
@@ -530,7 +531,10 @@ class InnerMathInput extends React__namespace.Component {
530
531
  locale
531
532
  } = this.context;
532
533
  // Initialize MathQuill.MathField instance
533
- this.__mathField = mathInput.createMathField(this.__mathFieldWrapperRef, locale, this.props.mathInputStrings, baseConfig => ({
534
+ this.__mathField = mathInput.createMathField(this.__mathFieldWrapperRef, locale, this.props.mathInputStrings,
535
+ // TODO(LEMS-2656): remove TS suppression
536
+ // @ts-expect-error: Type 'EditableMathQuill' is not assignable to type 'MathFieldInterface'.
537
+ baseConfig => ({
534
538
  ...baseConfig,
535
539
  handlers: {
536
540
  edit: debounce(mathField => {
@@ -2259,6 +2263,24 @@ const KhanAnswerTypes = {
2259
2263
  }
2260
2264
  };
2261
2265
 
2266
+ /**
2267
+ * Checks user input from the expression widget to see if it is scorable.
2268
+ *
2269
+ * Note: Most of the expression widget's validation requires the Rubric because
2270
+ * of its use of KhanAnswerTypes as a core part of scoring.
2271
+ *
2272
+ * @see `scoreExpression()` for more details.
2273
+ */
2274
+ function validateExpression(userInput) {
2275
+ if (userInput === "") {
2276
+ return {
2277
+ type: "invalid",
2278
+ message: null
2279
+ };
2280
+ }
2281
+ return null;
2282
+ }
2283
+
2262
2284
  /* Content creators input a list of answers which are matched from top to
2263
2285
  * bottom. The intent is that they can include spcific solutions which should
2264
2286
  * be graded as correct or incorrect (or ungraded!) first, then get more
@@ -2278,6 +2300,10 @@ const KhanAnswerTypes = {
2278
2300
  * - Otherwise, pass through the resulting points and message.
2279
2301
  */
2280
2302
  function scoreExpression(userInput, rubric, strings, locale) {
2303
+ const validationError = validateExpression(userInput);
2304
+ if (validationError) {
2305
+ return validationError;
2306
+ }
2281
2307
  const options = ___default["default"].clone(rubric);
2282
2308
  ___default["default"].extend(options, {
2283
2309
  decimal_separator: getDecimalSeparator(locale)
@@ -2535,11 +2561,17 @@ class Expression extends React__namespace.Component {
2535
2561
  }
2536
2562
  return true;
2537
2563
  };
2564
+
2565
+ // TODO(LEMS-2656): remove TS suppression
2566
+ // @ts-expect-error: Type 'FocusPath' is not assignable to type 'InputPath'.
2538
2567
  focusInputPath(inputPath) {
2539
2568
  // eslint-disable-next-line react/no-string-refs
2540
2569
  // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
2541
2570
  this.refs.input.focus();
2542
2571
  }
2572
+
2573
+ // TODO(LEMS-2656): remove TS suppression
2574
+ // @ts-expect-error: Type 'FocusPath' is not assignable to type 'InputPath'.
2543
2575
  blurInputPath(inputPath) {
2544
2576
  // eslint-disable-next-line react/no-string-refs
2545
2577
  // @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'ReactInstance'.
@@ -2713,7 +2745,7 @@ const keypadConfigurationForProps = widgetOptions => {
2713
2745
  times: widgetOptions.times
2714
2746
  };
2715
2747
  };
2716
- const propUpgrades$3 = {
2748
+ const propUpgrades$2 = {
2717
2749
  /* c8 ignore next */
2718
2750
  "1": v0props => ({
2719
2751
  times: v0props.times,
@@ -2773,10 +2805,14 @@ var Expression$1 = {
2773
2805
  major: 1,
2774
2806
  minor: 0
2775
2807
  },
2776
- propUpgrades: propUpgrades$3,
2808
+ propUpgrades: propUpgrades$2,
2777
2809
  // For use by the editor
2778
2810
  isLintable: true,
2811
+ // TODO(LEMS-2656): remove TS suppression
2812
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'.
2779
2813
  scorer: scoreExpression,
2814
+ // TODO(LEMS-2656): remove TS suppression
2815
+ // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusExpressionRubric'.
2780
2816
  getOneCorrectAnswerFromRubric(rubric) {
2781
2817
  const correctAnswers = (rubric.answerForms || []).filter(answerForm => answerForm.considered === "correct");
2782
2818
  if (correctAnswers.length === 0) {
@@ -2859,6 +2895,8 @@ const nestedMap$2 = function (children, func, context) {
2859
2895
  return nestedMap$2(child, func);
2860
2896
  });
2861
2897
  }
2898
+ // TODO(LEMS-2656): remove TS suppression
2899
+ // @ts-expect-error: T | ReadonlyArray<T> is not assignable to T
2862
2900
  return func.call(context, children);
2863
2901
  };
2864
2902
 
@@ -14637,7 +14675,9 @@ const constraints$2 = {
14637
14675
  });
14638
14676
 
14639
14677
  // bound the delta by the calculated bounds
14640
- const boundedDelta = ___default["default"].reduce(deltaBounds, function (delta, bound) {
14678
+ const boundedDelta = ___default["default"].reduce(deltaBounds,
14679
+ // @ts-expect-error: Type 'number[]' is not assignable to type 'Coord'.
14680
+ function (delta, bound) {
14641
14681
  const lower = bound[0];
14642
14682
  const upper = bound[1];
14643
14683
  const deltaX = Math.max(lower[0], Math.min(upper[0], delta[0]));
@@ -15172,6 +15212,7 @@ const constraints$1 = {
15172
15212
  };
15173
15213
  },
15174
15214
  snap: function (snap) {
15215
+ // @ts-expect-error: Type 'boolean | Coord' is not assignable to type 'Coord'.
15175
15216
  return function (coord) {
15176
15217
  if (snap === null) {
15177
15218
  // TODO(benchristel), NOTE(kevinb): this should probably return
@@ -15194,6 +15235,8 @@ const constraints$1 = {
15194
15235
  return optionalPaddingPx;
15195
15236
  }
15196
15237
  })();
15238
+
15239
+ // @ts-expect-error: Type 'boolean | Coord' is not assignable to type 'Coord'.
15197
15240
  return function (coord, prev, options) {
15198
15241
  const graphie = this.graphie;
15199
15242
  range = range || graphie.range;
@@ -16097,15 +16140,15 @@ const constraints = {
16097
16140
  });
16098
16141
 
16099
16142
  // bound the delta by the calculated bounds
16100
- const boundedDelta = ___default["default"].reduce(deltaBounds, function (delta, bound) {
16143
+ const boundedDelta = ___default["default"].reduce(deltaBounds,
16144
+ // @ts-expect-error - TS2345 - Type 'number[]' is not assignable to type 'Coord'.
16145
+ function (delta, bound) {
16101
16146
  const lower = bound[0];
16102
16147
  const upper = bound[1];
16103
16148
  const deltaX = Math.max(lower[0], Math.min(upper[0], delta[0]));
16104
16149
  const deltaY = Math.max(lower[1], Math.min(upper[1], delta[1]));
16105
16150
  return [deltaX, deltaY];
16106
16151
  }, delta);
16107
-
16108
- // @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'Coord'.
16109
16152
  return kmath.vector.add(prevCoord, boundedDelta);
16110
16153
  };
16111
16154
  }
@@ -17022,6 +17065,7 @@ class ImageLoader extends React__namespace.Component {
17022
17065
 
17023
17066
  this.img = new Image();
17024
17067
  this.img.onload = this.handleLoad;
17068
+ // @ts-expect-error: Type 'string | Event' is not assignable to type 'Event'.
17025
17069
  this.img.onerror = this.handleError;
17026
17070
  this.img.src = this.props.src;
17027
17071
  };
@@ -18080,6 +18124,7 @@ class InteractionTracker {
18080
18124
  this.widgetType = widgetType;
18081
18125
  this.widgetID = widgetID;
18082
18126
  this.setting = setting;
18127
+ // @ts-expect-error: Type 'T | undefined' is not assignable to type 'T'.
18083
18128
  this.track = this._track;
18084
18129
  }
18085
18130
  }
@@ -18514,10 +18559,10 @@ const styles$C = aphrodite.StyleSheet.create({
18514
18559
 
18515
18560
  /* eslint-disable no-useless-escape, no-prototype-builtins */
18516
18561
  const rules$1 = {
18517
- ...SimpleMarkdown$1.pureMarkdownRules,
18562
+ ...pureMarkdown.pureMarkdownRules,
18518
18563
  // NOTE: basically ignored by JIPT. wraps everything at the outer layer
18519
18564
  columns: {
18520
- ...SimpleMarkdown$1.pureMarkdownRules.columns,
18565
+ ...pureMarkdown.pureMarkdownRules.columns,
18521
18566
  react: (node, output, state) => {
18522
18567
  return /*#__PURE__*/React__namespace.createElement("div", {
18523
18568
  className: "perseus-two-columns",
@@ -18545,7 +18590,7 @@ const rules$1 = {
18545
18590
  // for exercises the renderer just renders the crowdin id to the
18546
18591
  // renderer div.
18547
18592
  crowdinId: {
18548
- ...SimpleMarkdown$1.pureMarkdownRules.crowdinId,
18593
+ ...pureMarkdown.pureMarkdownRules.crowdinId,
18549
18594
  react: (node, output, state) => node.id
18550
18595
  },
18551
18596
  // Ensure that the table is focusable in sequential keyboard navigation
@@ -18554,7 +18599,7 @@ const rules$1 = {
18554
18599
  // left/right gesture. Note, setting `tabindex` doesn't appear required for
18555
18600
  // iOS 14.4, in order for VoiceOver to work.
18556
18601
  table: {
18557
- ...SimpleMarkdown$1.pureMarkdownRules.table,
18602
+ ...pureMarkdown.pureMarkdownRules.table,
18558
18603
  react: function (node, output, state) {
18559
18604
  const table = SimpleMarkdown__default["default"].defaultRules.table.react(node, output, state);
18560
18605
  if (!state.isMobile) {
@@ -18572,7 +18617,7 @@ const rules$1 = {
18572
18617
  // our table regex into tableCapture[1..], and we pass tableCapture to
18573
18618
  // our nptable regex
18574
18619
  titledTable: {
18575
- ...SimpleMarkdown$1.pureMarkdownRules.titledTable,
18620
+ ...pureMarkdown.pureMarkdownRules.titledTable,
18576
18621
  react: (node, output, state) => {
18577
18622
  let contents;
18578
18623
  if (!node.table) {
@@ -18632,7 +18677,7 @@ const rules$1 = {
18632
18677
  }
18633
18678
  },
18634
18679
  widget: {
18635
- ...SimpleMarkdown$1.pureMarkdownRules.widget,
18680
+ ...pureMarkdown.pureMarkdownRules.widget,
18636
18681
  react: (node, output, state) => {
18637
18682
  // The actual output is handled in the renderer, where
18638
18683
  // we know the current widget props/state. This is
@@ -18643,7 +18688,7 @@ const rules$1 = {
18643
18688
  }
18644
18689
  },
18645
18690
  blockMath: {
18646
- ...SimpleMarkdown$1.pureMarkdownRules.blockMath,
18691
+ ...pureMarkdown.pureMarkdownRules.blockMath,
18647
18692
  react: (node, output, state) => {
18648
18693
  const {
18649
18694
  TeX
@@ -18657,7 +18702,7 @@ const rules$1 = {
18657
18702
  }
18658
18703
  },
18659
18704
  math: {
18660
- ...SimpleMarkdown$1.pureMarkdownRules.math,
18705
+ ...pureMarkdown.pureMarkdownRules.math,
18661
18706
  react: (node, output, state) => {
18662
18707
  const {
18663
18708
  TeX
@@ -18671,7 +18716,7 @@ const rules$1 = {
18671
18716
  }
18672
18717
  },
18673
18718
  unescapedDollar: {
18674
- ...SimpleMarkdown$1.pureMarkdownRules.unescapedDollar,
18719
+ ...pureMarkdown.pureMarkdownRules.unescapedDollar,
18675
18720
  react: (node, output, state) => {
18676
18721
  // Unescaped dollar signs render correctly, but result in
18677
18722
  // untranslatable text after the i18n python linter flags it
@@ -18681,7 +18726,7 @@ const rules$1 = {
18681
18726
  // Extend the SimpleMarkdown link parser to make the link open in a new
18682
18727
  // window and handle reverse tabnapping phishing attacks
18683
18728
  link: {
18684
- ...SimpleMarkdown$1.pureMarkdownRules.link,
18729
+ ...pureMarkdown.pureMarkdownRules.link,
18685
18730
  react: function (node, output, state) {
18686
18731
  const link = SimpleMarkdown__default["default"].defaultRules.link.react(node, output, state);
18687
18732
  const href = link.props.href;
@@ -18708,7 +18753,7 @@ const rules$1 = {
18708
18753
  }
18709
18754
  },
18710
18755
  codeBlock: {
18711
- ...SimpleMarkdown$1.pureMarkdownRules.codeBlock,
18756
+ ...pureMarkdown.pureMarkdownRules.codeBlock,
18712
18757
  react: (node, output, state) => {
18713
18758
  // ideally this should be a different rule, with only an
18714
18759
  // output function, but right now that breaks the parser.
@@ -18726,7 +18771,7 @@ const rules$1 = {
18726
18771
  // transform the tree to add lint nodes. This rule is here
18727
18772
  // just for the react() function
18728
18773
  lint: {
18729
- ...SimpleMarkdown$1.pureMarkdownRules.lint,
18774
+ ...pureMarkdown.pureMarkdownRules.lint,
18730
18775
  react: (node, output, state) => {
18731
18776
  return /*#__PURE__*/React__namespace.createElement(Lint, {
18732
18777
  message: node.message,
@@ -18875,342 +18920,6 @@ class QuestionParagraph extends React__namespace.Component {
18875
18920
  }
18876
18921
  }
18877
18922
 
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, renameMap) => {
19010
- const updatedUserInputMap = {
19011
- ...userInputMap
19012
- };
19013
- for (const key of Object.keys(userInputMap)) {
19014
- if (key.includes("input-number")) {
19015
- const updatedKey = renameMap[key];
19016
- updatedUserInputMap[updatedKey] = userInputMap[key];
19017
- delete updatedUserInputMap[key];
19018
- }
19019
- }
19020
- return updatedUserInputMap;
19021
- };
19022
-
19023
- // Used to convert InputNumber widgets to NumericInput widgets for scoring
19024
- const convertInputNumberForScoring = (rubric, userInputMap) => {
19025
- // First we need to create a map of the old input-number keys to the new numeric-input keys
19026
- // so that we can ensure we update the content, widgets, AND userInput accordingly
19027
- const renameMap = getInputNumberRenameMap(rubric);
19028
- const convertedRubric = convertInputNumberJson(rubric, renameMap);
19029
- const convertedUserData = convertUserInputNumberData(userInputMap, renameMap);
19030
- return {
19031
- convertedRubric,
19032
- convertedUserData
19033
- };
19034
- };
19035
-
19036
- const widgetRegExes = [/input-number \d+/]; // We can add more regexes here in the future
19037
-
19038
- // This utils in this file are used to modernize our Perseus JSON structure,
19039
- // so that we can convert deprecated widgets to their modern equivalents when
19040
- // content creators use the Editor Page to update content containing these widgets.
19041
- // Currently, we're only converting input-number to numeric-input,
19042
- // but we can add more conversions here in the future.
19043
-
19044
- // Modernize the json content of a PerseusRenderer object
19045
- // by converting deprecated widgets to their modern equivalents
19046
- const convertDeprecatedWidgets = json => {
19047
- // Currently we're only converting input-number to numeric-input,
19048
- // But we can add more conversions here in the future
19049
- return inputNumberToNumericInput(json);
19050
- };
19051
- const conversionRequired = json => {
19052
- // If there's no content, then there's no conversion required
19053
- if (!json.content) {
19054
- return false;
19055
- }
19056
- // Check the content string for any top-level input-number widgets
19057
- if (widgetRegExes.some(regex => regex.test(json.content))) {
19058
- return true;
19059
- }
19060
-
19061
- // If there's no deprecated widget in the top-level, then check for any nested widgets
19062
- for (const key of Object.keys(json.widgets)) {
19063
- if (json.widgets[key].options.widgets) {
19064
- const nestedJson = json.widgets[key].options;
19065
- if (conversionRequired(nestedJson)) {
19066
- return true;
19067
- }
19068
- }
19069
- }
19070
- return false;
19071
- };
19072
-
19073
- /**
19074
- * Convert the deprecated widgets in the rubric and user data to their
19075
- * modern equivalents for scoring. These need to be updated together
19076
- * in order to ensure that the widgetKeys match between the rubric and user data.
19077
- */
19078
- const convertDeprecatedWidgetsForScoring = (rubric, userInputMap) => {
19079
- return convertInputNumberForScoring(rubric, userInputMap);
19080
- };
19081
-
19082
- // These are all the current widgets that require user-input and are supported for automatic scoring
19083
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
19084
-
19085
- /**
19086
- * Regex for widget placeholders in a string.
19087
- *
19088
- * First capture group is the widget ID (ex. 'radio 1')
19089
- * Second capture group is the widget type (ex. "radio)
19090
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
19091
- */
19092
- function getWidgetRegex() {
19093
- return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
19094
- }
19095
-
19096
- /**
19097
- * Add a widget placeholder using the widget ID.
19098
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
19099
- *
19100
- * @param {string} id
19101
- * @returns {string}
19102
- */
19103
- function addWidget(id) {
19104
- return `[[☃ ${id}]]`;
19105
- }
19106
-
19107
- /**
19108
- * Get a widget type by a widget's ID
19109
- * (please don't derive type from ID)
19110
- *
19111
- * @param {string} widgetId the ID of the widget
19112
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
19113
- * @returns {string} the widget type (ie "radio")
19114
- */
19115
- function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
19116
- const widget = widgetMap[WidgetId];
19117
- return widget?.type ?? null;
19118
- }
19119
-
19120
- /**
19121
- * Does the content have a specific type of widget?
19122
- *
19123
- * @param {string} type the type of the widget in question (ie "radio")
19124
- * @param {string} content the string to search through
19125
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
19126
- * @returns {boolean} if the content includes the widget type
19127
- */
19128
- function contentHasWidgetType(type, content, widgetMap) {
19129
- return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
19130
- }
19131
-
19132
- /**
19133
- * Extract all widget IDs, which includes the widget type and instance number.
19134
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
19135
- *
19136
- * Content should contain Perseus widget placeholders,
19137
- * which look like: '[[☃ radio 1]]'.
19138
- *
19139
- * @param {string} content
19140
- * @returns {ReadonlyArray<string>} widgetIds
19141
- */
19142
- function getWidgetIdsFromContent(content) {
19143
- const widgets = [];
19144
- const localWidgetRegex = getWidgetRegex();
19145
- let match = localWidgetRegex.exec(content);
19146
- while (match !== null) {
19147
- widgets.push(match[1]);
19148
- match = localWidgetRegex.exec(content);
19149
- }
19150
- return widgets;
19151
- }
19152
-
19153
- /**
19154
- * Get a list of widget IDs from content,
19155
- * but only for specific widget types
19156
- *
19157
- * @param {string} type the type of widget (ie "radio")
19158
- * @param {string} content the string to parse
19159
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
19160
- * @returns {ReadonlyArray<string>} the widget type (ie "radio")
19161
- */
19162
- function getWidgetIdsFromContentByType(type, content, widgetMap) {
19163
- const rv = [];
19164
- const widgetIdsInContent = getWidgetIdsFromContent(content);
19165
- widgetIdsInContent.forEach(widgetId => {
19166
- const widget = widgetMap[widgetId];
19167
- if (widget?.type === type) {
19168
- rv.push(widgetId);
19169
- }
19170
- });
19171
- return rv;
19172
- }
19173
-
19174
- /**
19175
- * Pull the widget map out of ItemData
19176
- *
19177
- * @param {PerseusItem} PerseusItem containing a widgetMap
19178
- * @returns {WidgetMap} the widget map in the PerseusItem
19179
- */
19180
- function getWidgetsMapFromItemData(itemData) {
19181
- return itemData.question.widgets;
19182
- }
19183
-
19184
- /**
19185
- * Get the widget information from a WidgetMap
19186
- *
19187
- * @param {string} widgetId the ID of the widget
19188
- * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
19189
- */
19190
- function getWidgetFromWidgetMap(widgetId, widgetMap) {
19191
- return widgetMap[widgetId] ?? null;
19192
- }
19193
-
19194
- /**
19195
- * Get select widgets from a widget map.
19196
- * Useful for multi-items that needs to split
19197
- * one PerseusItem into several
19198
- *
19199
- * @param {ReadonlyArray<string>} widgetIds to extract
19200
- * @param {PerseusWidgetsMap} widgetMap to extract from
19201
- * @return {PerseusWidgetsMap} a new widget map with requested widgets
19202
- */
19203
- function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
19204
- const widgets = {};
19205
- widgetIds.forEach(widgetId => {
19206
- const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
19207
- if (widget) {
19208
- widgets[widgetId] = widget;
19209
- }
19210
- });
19211
- return widgets;
19212
- }
19213
-
19214
18923
  const DEFAULT_ALIGNMENT$1 = "block";
19215
18924
  // NOTE(kevinb): "default" is not one in `validAlignments`.
19216
18925
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -19261,9 +18970,6 @@ const replaceDeprecatedWidgets = () => {
19261
18970
  replaceWidget("sequence", "deprecated-standin");
19262
18971
  replaceWidget("simulator", "deprecated-standin");
19263
18972
  replaceWidget("unit-input", "deprecated-standin");
19264
-
19265
- // Input-Number is a special case as it is being replaced by Numeric-Input
19266
- replaceWidget("input-number", "numeric-input");
19267
18973
  };
19268
18974
  const registerEditors = editorsToRegister => {
19269
18975
  editorsToRegister.forEach(editor => {
@@ -19298,11 +19004,6 @@ const replaceDeprecatedEditors = () => {
19298
19004
  replaceEditor("sequence", "deprecated-standin");
19299
19005
  replaceEditor("simulator", "deprecated-standin");
19300
19006
  replaceEditor("unit-input", "deprecated-standin");
19301
-
19302
- // We're replacing the input-number editor with the numeric-input editor here,
19303
- // but we're also modifying the JSON content to convert input-number into the
19304
- // numeric-input format over in editor-page.tsx on line 92.
19305
- replaceEditor("input-number", "numeric-input");
19306
19007
  };
19307
19008
  const getWidget = name => {
19308
19009
  // TODO(alex): Consider referring to these as renderers to avoid
@@ -19349,8 +19050,10 @@ const getVersionVector = () => {
19349
19050
  const getPublicWidgets = () => {
19350
19051
  // TODO(alex): Update underscore.js so that _.pick can take a function.
19351
19052
  // @ts-expect-error - TS2740 - Type 'Pick<{ [key: string]: Readonly<{ name: string; displayName: string; getWidget?: (() => ComponentType<any>) | undefined; accessible?: boolean | ((props: any) => boolean) | undefined; hidden?: boolean | undefined; ... 10 more ...; widget: ComponentType<...>; }>; }, string>' is missing the following properties from type 'readonly Readonly<{ name: string; displayName: string; getWidget?: (() => ComponentType<any>) | undefined; accessible?: boolean | ((props: any) => boolean) | undefined; hidden?: boolean | undefined; ... 10 more ...; widget: ComponentType<...>; }>[]': length, concat, join, slice, and 18 more.
19352
- return ___default["default"].pick(widgets, ___default["default"].reject(___default["default"].keys(widgets), function (name) {
19353
- return widgets[name].hidden || name === "input-number";
19053
+ return ___default["default"].pick(widgets,
19054
+ // @ts-expect-error - TS2345 - Argument of type '(name: string) => boolean | undefined' is not assignable to parameter of type 'Iteratee<string[], boolean, string>'.
19055
+ ___default["default"].reject(___default["default"].keys(widgets), function (name) {
19056
+ return widgets[name].hidden;
19354
19057
  }));
19355
19058
  };
19356
19059
  const isAccessible = widgetInfo => {
@@ -19671,34 +19374,6 @@ widgetIds, userInputMap, strings, locale) {
19671
19374
  }
19672
19375
  });
19673
19376
  }
19674
-
19675
- // TODO: combine scorePerseusItem with scoreWidgetsFunctional
19676
- // once scorePerseusItem is the only one calling scoreWidgetsFunctional
19677
- function scorePerseusItem(perseusRenderData, userInputMap,
19678
- // TODO(LEMS-2461,LEMS-2391): these probably
19679
- // need to be removed before we move this to the server
19680
- strings, locale) {
19681
- let convertedRenderData = perseusRenderData;
19682
- let convertedUserInputMap = userInputMap;
19683
-
19684
- // Check if the PerseusRenderer object contains any deprecated widgets that need to be converted
19685
- const mustConvertData = conversionRequired(perseusRenderData);
19686
- if (mustConvertData) {
19687
- const {
19688
- convertedRubric,
19689
- convertedUserData
19690
- } = convertDeprecatedWidgetsForScoring(perseusRenderData, userInputMap);
19691
- convertedRenderData = convertedRubric;
19692
- convertedUserInputMap = convertedUserData;
19693
- }
19694
-
19695
- // There seems to be a chance that PerseusRenderer.widgets might include
19696
- // widget data for widgets that are not in PerseusRenderer.content,
19697
- // so this checks that the widgets are being used before scoring them
19698
- const usedWidgetIds = getWidgetIdsFromContent(convertedRenderData.content);
19699
- const scores = scoreWidgetsFunctional(convertedRenderData.widgets, usedWidgetIds, convertedUserInputMap, strings, locale);
19700
- return Util.flattenScores(scores);
19701
- }
19702
19377
  function scoreWidgetsFunctional(widgets,
19703
19378
  // This is a port of old code, I'm not sure why
19704
19379
  // we need widgetIds vs the keys of the widgets object
@@ -19719,7 +19394,7 @@ widgetIds, userInputMap, strings, locale) {
19719
19394
  const userInput = userInputMap[id];
19720
19395
  const scorer = getWidgetScorer(widget.type);
19721
19396
  if (widget.type === "group") {
19722
- const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
19397
+ const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19723
19398
  widgetScores[id] = Util.flattenScores(scores);
19724
19399
  } else if (scorer) {
19725
19400
  widgetScores[id] = scorer(userInput, widget.options, strings, locale);
@@ -20768,7 +20443,7 @@ class Renderer extends React__namespace.Component {
20768
20443
  // Instead, we center them with css magic in styles.less
20769
20444
  // /cry(aria)
20770
20445
  this._foundTextNodes = true;
20771
- if (this.widgetIds.includes(node.id)) {
20446
+ if (___default["default"].contains(this.widgetIds, node.id)) {
20772
20447
  // We don't want to render a duplicate widget key/ref,
20773
20448
  // as this causes problems with react (for obvious
20774
20449
  // reasons). Instead we just notify the
@@ -21093,7 +20768,7 @@ class Renderer extends React__namespace.Component {
21093
20768
  };
21094
20769
  getInputPaths = () => {
21095
20770
  const inputPaths = [];
21096
- this.widgetIds.forEach(widgetId => {
20771
+ ___default["default"].each(this.widgetIds, widgetId => {
21097
20772
  const widget = this.getWidgetInstance(widgetId);
21098
20773
  if (widget && widget.getInputPaths) {
21099
20774
  // Grab all input paths and add widgetID to the front
@@ -21101,7 +20776,7 @@ class Renderer extends React__namespace.Component {
21101
20776
  // Prefix paths with their widgetID and add to collective
21102
20777
  // list of paths.
21103
20778
  // @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
21104
- widgetInputPaths.forEach(inputPath => {
20779
+ ___default["default"].each(widgetInputPaths, inputPath => {
21105
20780
  const relativeInputPath = [widgetId].concat(inputPath);
21106
20781
  inputPaths.push(relativeInputPath);
21107
20782
  });
@@ -21294,30 +20969,33 @@ class Renderer extends React__namespace.Component {
21294
20969
  }
21295
20970
 
21296
20971
  /**
21297
- * Scores the content.
21298
- *
21299
- * @deprecated use scorePerseusItem
20972
+ * Returns an object mapping from widget ID to perseus-style score.
20973
+ * The keys of this object are the values of the array returned
20974
+ * from `getWidgetIds`.
21300
20975
  */
21301
- score() {
21302
- const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
21303
- const combinedScore = Util.flattenScores(scores);
21304
- return combinedScore;
20976
+ scoreWidgets() {
20977
+ return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
21305
20978
  }
21306
20979
 
21307
20980
  /**
21308
- * @deprecated use scorePerseusItem
20981
+ * Grades the content.
21309
20982
  */
21310
- guessAndScore = () => {
20983
+ score() {
20984
+ const scores = this.scoreWidgets();
20985
+ const combinedScore = Util.flattenScores(scores);
20986
+ return combinedScore;
20987
+ }
20988
+ guessAndScore() {
21311
20989
  const totalGuess = this.getUserInput();
21312
20990
  const totalScore = this.score();
21313
20991
  return [totalGuess, totalScore];
21314
- };
20992
+ }
21315
20993
  examples = () => {
21316
20994
  const widgetIds = this.widgetIds;
21317
- const examples = widgetIds.map(widgetId => {
20995
+ const examples = ___default["default"].compact(___default["default"].map(widgetIds, widgetId => {
21318
20996
  const widget = this.getWidgetInstance(widgetId);
21319
20997
  return widget != null && widget.examples ? widget.examples() : null;
21320
- }).filter(Boolean);
20998
+ }));
21321
20999
 
21322
21000
  // no widgets with examples
21323
21001
  if (!examples.length) {
@@ -22322,11 +22000,17 @@ class InputNumber extends React__namespace.Component {
22322
22000
  this.refs.input.focus();
22323
22001
  return true;
22324
22002
  };
22003
+
22004
+ // TODO(LEMS-2656): remove TS suppression
22005
+ // @ts-expect-error: Type 'FocusPath' is not assignable to type 'Path'.
22325
22006
  focusInputPath = inputPath => {
22326
22007
  // eslint-disable-next-line react/no-string-refs
22327
22008
  // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
22328
22009
  this.refs.input.focus();
22329
22010
  };
22011
+
22012
+ // TODO(LEMS-2656): remove TS suppression
22013
+ // @ts-expect-error: Type 'FocusPath' is not assignable to type 'Path'.
22330
22014
  blurInputPath = inputPath => {
22331
22015
  // eslint-disable-next-line react/no-string-refs
22332
22016
  // @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'ReactInstance'.
@@ -22342,6 +22026,9 @@ class InputNumber extends React__namespace.Component {
22342
22026
  /* c8 ignore next */
22343
22027
  return [[]];
22344
22028
  };
22029
+
22030
+ // TODO(LEMS-2656): remove TS suppression
22031
+ // @ts-expect-error: Type 'FocusPath' is not assignable to type 'Path'.
22345
22032
  setInputValue = (path, newValue, cb) => {
22346
22033
  this.props.onChange({
22347
22034
  currentValue: newValue
@@ -22452,6 +22139,8 @@ var InputNumber$1 = {
22452
22139
  widget: InputNumber,
22453
22140
  transform: propTransform$3,
22454
22141
  isLintable: true,
22142
+ // TODO(LEMS-2656): remove TS suppression
22143
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInputNumberUserInput'.
22455
22144
  scorer: scoreInputNumber,
22456
22145
  getOneCorrectAnswerFromRubric(rubric) {
22457
22146
  if (rubric.value == null) {
@@ -22745,7 +22434,7 @@ class NumericInput extends React__namespace.Component {
22745
22434
  height: "auto"
22746
22435
  }
22747
22436
  });
22748
- return /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
22437
+ return /*#__PURE__*/React__namespace.createElement("div", null, /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
22749
22438
  ref: ref => this.inputRef = ref,
22750
22439
  value: this.props.currentValue,
22751
22440
  onChange: this.handleChange,
@@ -22757,7 +22446,7 @@ class NumericInput extends React__namespace.Component {
22757
22446
  id: this.props.widgetId,
22758
22447
  disabled: this.props.apiOptions.readOnly,
22759
22448
  style: styles.input
22760
- });
22449
+ }));
22761
22450
  }
22762
22451
  }
22763
22452
 
@@ -22815,69 +22504,19 @@ const propsTransform = function (widgetOptions) {
22815
22504
  });
22816
22505
  return rendererProps;
22817
22506
  };
22818
-
22819
- // This function is being used to replace the input-number widget
22820
- // with the numeric-input widget
22821
- const propUpgrades$2 = {
22822
- /* c8 ignore next */
22823
- "1": initialProps => {
22824
- // If the initialProps has simplify, it means we're upgrading from
22825
- // input-number to numeric-input. In this case, we need to upgrade
22826
- // the widget options accordingly.
22827
- if (initialProps.simplify !== undefined) {
22828
- // If the answerType is not number or percent, we need to provide
22829
- // the answer form for the numeric-input widget
22830
- const provideAnswerForm = initialProps.answerType !== "number" && initialProps.answerType !== "percent";
22831
-
22832
- // We need to determine the mathFormat for the numeric-input widget
22833
- const mathFormat = initialProps.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
22834
- : initialProps.answerType; // Otherwise, we can use the answerType directly
22835
-
22836
- // If adjusting this logic, also adjust the logic in the convertInputNumberWidgetOptions
22837
- // function in input-number.ts in the Perseus Editor package's util folder
22838
- const answers = [{
22839
- value: initialProps.value,
22840
- simplify: initialProps.simplify,
22841
- answerForms: provideAnswerForm ? [mathFormat] : undefined,
22842
- strict: initialProps.inexact,
22843
- // We only want to set maxError if the inexact prop is true
22844
- maxError: initialProps.inexact ? initialProps.maxError : 0,
22845
- status: "correct",
22846
- // Input-number only allows correct answers
22847
- message: ""
22848
- }];
22849
- return {
22850
- answers,
22851
- size: initialProps.size,
22852
- coefficient: false,
22853
- // input-number doesn't have a coefficient prop
22854
- labelText: "",
22855
- // input-number doesn't have a labelText prop
22856
- static: false,
22857
- // static is always false for numeric-input
22858
- rightAlign: initialProps.rightAlign || false
22859
- };
22860
- } else {
22861
- // Otherwise simply return the initialProps as there's no differences
22862
- // between v0 and v1 for numeric-input
22863
- return initialProps;
22864
- }
22865
- }
22866
- };
22867
22507
  var NumericInput$1 = {
22868
22508
  name: "numeric-input",
22869
22509
  displayName: "Numeric input",
22870
22510
  defaultAlignment: "inline-block",
22871
22511
  accessible: true,
22872
22512
  widget: NumericInput,
22873
- version: {
22874
- major: 1,
22875
- minor: 0
22876
- },
22877
22513
  transform: propsTransform,
22878
- propUpgrades: propUpgrades$2,
22879
22514
  isLintable: true,
22515
+ // TODO(LEMS-2656): remove TS suppression
22516
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumericInputUserInput'.
22880
22517
  scorer: scoreNumericInput,
22518
+ // TODO(LEMS-2656): remove TS suppression
22519
+ // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusNumericInputRubric'
22881
22520
  getOneCorrectAnswerFromRubric(rubric) {
22882
22521
  const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
22883
22522
  const answerStrings = correctAnswers.map(answer => {
@@ -24696,6 +24335,9 @@ const CIRCLE_LABEL_STYLE = {
24696
24335
  borderRadius: "50%",
24697
24336
  textAlign: "center"
24698
24337
  };
24338
+
24339
+ // TODO(LEMS-2656): remove TS suppression
24340
+ // @ts-expect-error: State is not assignable to ParseState
24699
24341
  const builtParser = SimpleMarkdown__default["default"].parserFor(rules);
24700
24342
  const parse$2 = (source, state) => {
24701
24343
  // @ts-expect-error - TS2322 - Type 'ParseState | {}' is not assignable to type 'ParseState | null | undefined'.
@@ -27151,6 +26793,8 @@ var Radio = {
27151
26793
  },
27152
26794
  propUpgrades: propUpgrades$1,
27153
26795
  isLintable: true,
26796
+ // TODO(LEMS-2656): remove TS suppression
26797
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput
27154
26798
  scorer: scoreRadio
27155
26799
  };
27156
26800
 
@@ -27374,6 +27018,8 @@ var Categorizer$1 = {
27374
27018
  return ___default["default"].pick(editorProps, "items", "categories", "values", "randomizeItems");
27375
27019
  },
27376
27020
  isLintable: true,
27021
+ // TODO(LEMS-2656): remove TS suppression
27022
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
27377
27023
  scorer: scoreCategorizer
27378
27024
  };
27379
27025
 
@@ -27611,6 +27257,8 @@ var CSProgram$1 = {
27611
27257
  supportedAlignments: ["block", "full-width"],
27612
27258
  widget: CSProgram,
27613
27259
  hidden: true,
27260
+ // TODO(LEMS-2656): remove TS suppression
27261
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
27614
27262
  scorer: scoreCSProgram
27615
27263
  };
27616
27264
 
@@ -27826,6 +27474,8 @@ var Dropdown$1 = {
27826
27474
  accessible: true,
27827
27475
  widget: Dropdown,
27828
27476
  transform: optionsTransform,
27477
+ // TODO(LEMS-2656): remove TS suppression
27478
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'.
27829
27479
  scorer: scoreDropdown
27830
27480
  };
27831
27481
 
@@ -29412,6 +29062,8 @@ class FunctionGrapher extends React__namespace.Component {
29412
29062
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
29413
29063
  args[_key] = arguments[_key];
29414
29064
  }
29065
+ // TODO(LEMS-2656): remove TS suppression
29066
+ // @ts-expect-error: Argument of type 'any[]' is not assignable to parameter of type '[newPropsOrSinglePropName: string | { [key: string]: any; }, propValue?: any, callback?: (() => unknown) | undefined]'. Target requires 1 element(s) but source may have fewer.
29415
29067
  return change.apply(_this, args);
29416
29068
  };
29417
29069
  })();
@@ -29769,6 +29421,8 @@ var Grapher$1 = {
29769
29421
  widget: Grapher,
29770
29422
  transform: propTransform$2,
29771
29423
  staticTransform: staticTransform$4,
29424
+ // TODO(LEMS-2656): remove TS suppression
29425
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGrapherUserInput'.
29772
29426
  scorer: scoreGrapher
29773
29427
  };
29774
29428
 
@@ -29892,9 +29546,6 @@ class Group extends React__namespace.Component {
29892
29546
  this.change("widgets", this.rendererRef.props.widgets);
29893
29547
  }
29894
29548
  };
29895
-
29896
- // TODO(LEMS-2391): replace this when there's a separate check
29897
- // for valid/invalid state
29898
29549
  const score = this.rendererRef?.score();
29899
29550
  const isValid = score && score.type !== "invalid";
29900
29551
  const isInvalid = score && score.type === "invalid";
@@ -30102,6 +29753,8 @@ var Iframe$1 = {
30102
29753
  widget: Iframe,
30103
29754
  // Let's not expose it to all content creators yet
30104
29755
  hidden: true,
29756
+ // TODO(LEMS-2656): remove TS suppression
29757
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusIframeUserInput'.
30105
29758
  scorer: scoreIframe
30106
29759
  };
30107
29760
 
@@ -31779,6 +31432,38 @@ function isUnlimitedGraphState(state) {
31779
31432
  return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
31780
31433
  }
31781
31434
 
31435
+ /**
31436
+ * Parse a string of text and math into a list of objects with type and content
31437
+ *
31438
+ * Example: "Pi is about $\frac{22}{7}$" ==>
31439
+ * [
31440
+ * {type: "text", content: "Pi is about "},
31441
+ * {type: "math", content: "\\frac{22}{7}"},
31442
+ * ]
31443
+ */
31444
+ const mathOnlyParser = SimpleMarkdown__default["default"].parserFor({
31445
+ math: {
31446
+ ...pureMarkdown.pureMarkdownRules.math,
31447
+ order: 0
31448
+ },
31449
+ text: {
31450
+ order: 1,
31451
+ match: SimpleMarkdown__default["default"].anyScopeRegex(/^([^$\\{}]+)/),
31452
+ parse: capture => ({
31453
+ content: capture[0]
31454
+ })
31455
+ },
31456
+ specialCharacter: {
31457
+ order: 2,
31458
+ match: SimpleMarkdown__default["default"].anyScopeRegex(/^(\\[\S\s]|\$|\\$|{|})/),
31459
+ parse: capture => ({
31460
+ content: capture[0]
31461
+ })
31462
+ }
31463
+ }, {
31464
+ inline: true
31465
+ });
31466
+
31782
31467
  /**
31783
31468
  * Replace all text outside of the $ TeX blocks with `\\text{...}`
31784
31469
  * This way, the entire resulting string can be rendered within <TeX>
@@ -31787,57 +31472,19 @@ function isUnlimitedGraphState(state) {
31787
31472
  function replaceOutsideTeX(mathString) {
31788
31473
  // All the information we need is in the first section,
31789
31474
  // whether it's typed as "blockmath" or "paragraph"
31790
- const firstSection = SimpleMarkdown__namespace.parse(mathString)[0];
31791
-
31792
- // If it's blockMath, the outer level has the full math content.
31793
- if (firstSection.type === "blockMath") {
31794
- return firstSection.content;
31795
- }
31796
-
31797
- // If it's a paragraph, we need to iterate through the sections
31798
- // to look for individual math blocks.
31799
- const condensedNodes = condenseTextNodes(firstSection.content);
31475
+ const parsed = mathOnlyParser(mathString);
31800
31476
  let result = "";
31801
- for (const piece of condensedNodes) {
31802
- piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
31477
+ for (const piece of parsed) {
31478
+ piece.type === "math" ? result += "$" + piece.content + "$" : piece.type === "specialCharacter" ? result += escapeIfUnescaped(piece.content) : result += piece.content;
31803
31479
  }
31804
- return result;
31480
+ return `\\text{${result}}`;
31805
31481
  }
31806
- // Helper function for replaceOutsideTeX()
31807
- // Condense adjacent text nodes into a single text node
31808
- function condenseTextNodes(nodes) {
31809
- const result = [];
31810
- if (!nodes) {
31811
- return result;
31812
- }
31813
- let currentText = "";
31814
- for (const node of nodes) {
31815
- if (node.type === "math") {
31816
- if (currentText) {
31817
- result.push({
31818
- type: "text",
31819
- content: currentText
31820
- });
31821
- currentText = "";
31822
- }
31823
- result.push(node);
31824
- } else {
31825
- currentText += node.content;
31826
- }
31827
- }
31828
- if (currentText) {
31829
- result.push({
31830
- type: "text",
31831
- content: currentText
31832
- });
31482
+ function escapeIfUnescaped(character) {
31483
+ if (character.length === 1) {
31484
+ return "\\" + character;
31485
+ } else {
31486
+ return character;
31833
31487
  }
31834
- return result;
31835
- }
31836
-
31837
- // Helper function for replaceOutsideTeX()
31838
- function escapeSpecialChars(str) {
31839
- // Escape $, \, {, and } characters
31840
- return str.replace(/([$\\{}])/g, "\\$1");
31841
31488
  }
31842
31489
 
31843
31490
  function AxisLabels() {
@@ -32786,12 +32433,17 @@ const ItemExtras = [
32786
32433
  // prettier-ignore
32787
32434
 
32788
32435
  // prettier-ignore
32789
- // While this widget is deprecated, we still need the type for conversion purposes
32436
+
32790
32437
  // prettier-ignore
32438
+
32791
32439
  // prettier-ignore
32440
+
32792
32441
  // prettier-ignore
32442
+
32793
32443
  //prettier-ignore
32444
+
32794
32445
  // A background image applied to various widgets.
32446
+
32795
32447
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
32796
32448
 
32797
32449
  // 2D range: xMin, xMax, yMin, yMax
@@ -33293,7 +32945,9 @@ const actions = {
33293
32945
  removePoint,
33294
32946
  focusPoint,
33295
32947
  blurPoint,
33296
- clickPoint
32948
+ clickPoint,
32949
+ closePolygon,
32950
+ openPolygon
33297
32951
  },
33298
32952
  quadratic: {
33299
32953
  movePoint
@@ -33372,6 +33026,18 @@ function changeKeyboardInvitationVisibility(shouldShow) {
33372
33026
  shouldShow
33373
33027
  };
33374
33028
  }
33029
+ const CLOSE_POLYGON = "close-polygon";
33030
+ function closePolygon() {
33031
+ return {
33032
+ type: CLOSE_POLYGON
33033
+ };
33034
+ }
33035
+ const OPEN_POLYGON = "open-polygon";
33036
+ function openPolygon() {
33037
+ return {
33038
+ type: OPEN_POLYGON
33039
+ };
33040
+ }
33375
33041
  const MOVE_ALL = "move-all";
33376
33042
  function moveAll(delta) {
33377
33043
  return {
@@ -34915,33 +34581,17 @@ const LimitedPolygonGraph = props => {
34915
34581
  }
34916
34582
  })));
34917
34583
  };
34918
-
34919
- // TODO(catjohnson): reduce redundancy between LimitedPolygonGraph and UnlimitedPolygonGraph
34920
- // both components are vary similar, however more implementation is needed to be added before
34921
- // it is clear what can and can't be shared between components.
34922
34584
  const UnlimitedPolygonGraph = props => {
34923
- const [hovered, setHovered] = React__namespace.useState(false);
34924
- // This is more so required for the re-rendering that occurs when state
34925
- // updates; specifically with regard to line weighting and polygon focus.
34926
- const [focusVisible, setFocusVisible] = React__namespace.useState(false);
34927
34585
  const {
34928
34586
  dispatch
34929
34587
  } = props;
34930
34588
  const {
34931
34589
  coords,
34932
- showAngles,
34933
- showSides,
34934
- range,
34935
- snapStep,
34936
- snapTo = "grid"
34590
+ closedPolygon
34937
34591
  } = props.graphState;
34938
34592
  const graphConfig = useGraphConfig();
34939
-
34940
- // TODO(catjohnson): Explore abstracting this code as it is similar to point.tsx
34941
- // and hopefully we can cut down ont the unlimited graph redundancy.
34942
34593
  const {
34943
34594
  range: [x, y],
34944
- disableKeyboardInteraction,
34945
34595
  graphDimensionsInPixels
34946
34596
  } = graphConfig;
34947
34597
  const widthPx = graphDimensionsInPixels[0];
@@ -34952,116 +34602,66 @@ const UnlimitedPolygonGraph = props => {
34952
34602
  // TODO(benchristel): can the default set of points be removed here? I don't
34953
34603
  // think coords can be null.
34954
34604
  const points = coords ?? [[0, 0]];
34955
- const polygonRef = React__namespace.useRef(null);
34956
- const dragReferencePoint = points[0];
34957
- const constrain = ["angles", "sides"].includes(snapTo) ? p => p : p => snap(snapStep, p);
34958
- const {
34959
- dragging
34960
- } = useDraggable({
34961
- gestureTarget: polygonRef,
34962
- point: dragReferencePoint,
34963
- onMove: newPoint => {
34964
- const delta = mafs.vec.sub(newPoint, dragReferencePoint);
34965
- dispatch(actions.polygon.moveAll(delta));
34966
- },
34967
- constrainKeyboardMovement: constrain
34968
- });
34969
- const lines = getLines(points);
34970
34605
  React__namespace.useEffect(() => {
34971
34606
  const focusedIndex = props.graphState.focusedPointIndex;
34972
34607
  if (focusedIndex != null) {
34973
34608
  pointRef.current[focusedIndex]?.focus();
34974
34609
  }
34975
34610
  }, [props.graphState.focusedPointIndex, pointRef]);
34976
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
34977
- style: {
34978
- fill: "rgba(0,0,0,0)",
34979
- cursor: "crosshair"
34980
- },
34981
- width: widthPx,
34982
- height: heightPx,
34983
- x: left,
34984
- y: top,
34985
- onClick: event => {
34986
- const elementRect = event.currentTarget.getBoundingClientRect();
34987
- const x = event.clientX - elementRect.x;
34988
- const y = event.clientY - elementRect.y;
34989
- const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
34990
- dispatch(actions.polygon.addPoint(graphCoordinates[0]));
34991
- }
34992
- }), /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
34993
- points: [...points],
34994
- color: "var(--movable-line-stroke-color)",
34995
- svgPolygonProps: {
34996
- strokeWidth: focusVisible ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)",
34611
+ if (closedPolygon) {
34612
+ const closedPolygonProps = {
34613
+ ...props,
34614
+ numSides: coords.length
34615
+ };
34616
+ return /*#__PURE__*/React__namespace.createElement(LimitedPolygonGraph, closedPolygonProps);
34617
+ } else {
34618
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
34997
34619
  style: {
34998
- fill: "transparent"
34620
+ fill: "rgba(0,0,0,0)",
34621
+ cursor: "crosshair"
34622
+ },
34623
+ width: widthPx,
34624
+ height: heightPx,
34625
+ x: left,
34626
+ y: top,
34627
+ onClick: event => {
34628
+ const elementRect = event.currentTarget.getBoundingClientRect();
34629
+ const x = event.clientX - elementRect.x;
34630
+ const y = event.clientY - elementRect.y;
34631
+ const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
34632
+ dispatch(actions.polygon.addPoint(graphCoordinates[0]));
34633
+ }
34634
+ }), /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
34635
+ points: [...points],
34636
+ color: "var(--movable-line-stroke-color)",
34637
+ svgPolylineProps: {
34638
+ strokeWidth: "var(--movable-line-stroke-weight)",
34639
+ style: {
34640
+ fill: "transparent"
34641
+ }
34999
34642
  }
35000
- }
35001
- }), props.graphState.coords.map((point, i) => {
35002
- const pt1 = points.at(i - 1);
35003
- const pt2 = points[(i + 1) % points.length];
35004
- if (!pt1 || !pt2) {
35005
- return null;
35006
- }
35007
- return /*#__PURE__*/React__namespace.createElement(PolygonAngle, {
35008
- key: "angle-" + i,
35009
- centerPoint: point,
35010
- endPoints: [pt1, pt2],
35011
- range: range,
35012
- polygonLines: lines,
35013
- showAngles: !!showAngles,
35014
- snapTo: snapTo
35015
- });
35016
- }), showSides && lines.map((_ref2, i) => {
35017
- let [start, end] = _ref2;
35018
- const [x, y] = mafs.vec.midpoint(start, end);
35019
- const length = parseFloat(mafs.vec.dist(start, end).toFixed(snapTo === "sides" ? 0 : 1));
35020
- return /*#__PURE__*/React__namespace.createElement(TextLabel, {
35021
- key: "side-" + i,
35022
- x: x,
35023
- y: y
35024
- }, !Number.isInteger(length) && "≈ ", length);
35025
- }), /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
35026
- points: [...points],
35027
- color: "transparent",
35028
- svgPolygonProps: {
35029
- ref: polygonRef,
35030
- tabIndex: disableKeyboardInteraction ? -1 : 0,
35031
- strokeWidth: TARGET_SIZE,
35032
- style: {
35033
- cursor: dragging ? "grabbing" : "grab",
35034
- fill: hovered ? "var(--mafs-blue)" : "transparent"
34643
+ }), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34644
+ key: i,
34645
+ point: point,
34646
+ sequenceNumber: i + 1,
34647
+ onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
34648
+ ref: ref => {
34649
+ pointRef.current[i] = ref;
35035
34650
  },
35036
- onMouseEnter: () => setHovered(true),
35037
- onMouseLeave: () => setHovered(false),
35038
- // Required to remove line weighting when user clicks away
35039
- // from the focused polygon
35040
- onKeyDownCapture: () => {
35041
- setFocusVisible(hasFocusVisible(polygonRef.current));
34651
+ onFocus: () => {
34652
+ dispatch(actions.polygon.focusPoint(i));
35042
34653
  },
35043
- // Required for lines to darken on focus
35044
- onFocus: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
35045
- // Required for line weighting to update on blur. Without this,
35046
- // the user has to hover over the shape for it to update
35047
- onBlur: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
35048
- className: "movable-polygon"
35049
- }
35050
- }), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
35051
- key: i,
35052
- point: point,
35053
- sequenceNumber: i + 1,
35054
- onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
35055
- ref: ref => {
35056
- pointRef.current[i] = ref;
35057
- },
35058
- onFocus: () => {
35059
- dispatch(actions.pointGraph.focusPoint(i));
35060
- },
35061
- onClick: () => {
35062
- dispatch(actions.pointGraph.clickPoint(i));
35063
- }
35064
- })));
34654
+ onClick: () => {
34655
+ // If the point being clicked is the first point and
34656
+ // there's enough points to form a polygon (3 or more)
34657
+ // Close the shape before setting focus.
34658
+ if (i === 0 && props.graphState.coords.length >= 3) {
34659
+ dispatch(actions.polygon.closePolygon());
34660
+ }
34661
+ dispatch(actions.polygon.clickPoint(i));
34662
+ }
34663
+ })));
34664
+ }
35065
34665
  };
35066
34666
  function getLines(points) {
35067
34667
  return points.map((point, i) => {
@@ -35457,6 +35057,7 @@ const MafsGraph = props => {
35457
35057
  const uniqueId = React__namespace.useId();
35458
35058
  const descriptionId = `interactive-graph-description-${uniqueId}`;
35459
35059
  const interactiveElementsDescriptionId = `interactive-graph-interactive-elements-description-${uniqueId}`;
35060
+ const unlimitedGraphKeyboardPromptId = `unlimited-graph-keyboard-prompt-${uniqueId}`;
35460
35061
  const graphRef = React__namespace.useRef(null);
35461
35062
  const {
35462
35063
  analytics
@@ -35529,7 +35130,7 @@ const MafsGraph = props => {
35529
35130
  handleKeyboardEvent(event, state, dispatch);
35530
35131
  },
35531
35132
  "aria-label": fullGraphAriaLabel,
35532
- "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId),
35133
+ "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && "unlimited-graph-keyboard-prompt"),
35533
35134
  ref: graphRef,
35534
35135
  tabIndex: 0,
35535
35136
  onFocus: event => {
@@ -35599,6 +35200,7 @@ const MafsGraph = props => {
35599
35200
  height: height
35600
35201
  }, /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, props.showProtractor && /*#__PURE__*/React__namespace.createElement(Protractor, null), graph)))), interactionPrompt && /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
35601
35202
  style: {
35203
+ display: interactionPrompt ? undefined : "hidden",
35602
35204
  textAlign: "center",
35603
35205
  backgroundColor: "white",
35604
35206
  border: "1px solid #21242C52",
@@ -35610,7 +35212,9 @@ const MafsGraph = props => {
35610
35212
  top: "50%",
35611
35213
  transform: "translateY(-50%)"
35612
35214
  }
35613
- }, /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelMedium, null, strings.graphKeyboardPrompt))), renderGraphControls({
35215
+ }, /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelMedium, {
35216
+ id: unlimitedGraphKeyboardPromptId
35217
+ }, strings.graphKeyboardPrompt))), renderGraphControls({
35614
35218
  state,
35615
35219
  dispatch,
35616
35220
  width,
@@ -35660,23 +35264,51 @@ const renderPointGraphControls = props => {
35660
35264
  }
35661
35265
  }, perseusStrings.removePoint));
35662
35266
  };
35663
-
35664
- /**
35665
- * TODO(catjohnson): The polygon controls will be vastly different from the point controls.
35666
- * Will be keeping this function as is for the time being, even if it's a copy of the point graph
35667
- * controls.
35668
- */
35669
35267
  const renderPolygonGraphControls = props => {
35670
35268
  const {
35671
35269
  interactionMode,
35672
35270
  showRemovePointButton,
35673
- focusedPointIndex
35271
+ focusedPointIndex,
35272
+ closedPolygon,
35273
+ coords
35674
35274
  } = props.state;
35675
35275
  const {
35676
35276
  perseusStrings
35677
35277
  } = props;
35678
35278
  const shouldShowRemoveButton = showRemovePointButton && focusedPointIndex !== null;
35679
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
35279
+
35280
+ // We want to disable the closePolygon button when
35281
+ // there are not enough coords to make a polygon
35282
+ const disableCloseButton = coords.length < 3;
35283
+
35284
+ // If polygon is closed, show open button.
35285
+ // If polygon is open, show close button.
35286
+ const polygonButton = closedPolygon ? /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
35287
+ kind: "secondary",
35288
+ style: {
35289
+ width: "100%",
35290
+ marginLeft: "20px"
35291
+ },
35292
+ tabIndex: 0,
35293
+ onClick: () => {
35294
+ props.dispatch(actions.polygon.openPolygon());
35295
+ }
35296
+ }, perseusStrings.openPolygon) : /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
35297
+ kind: "secondary"
35298
+ // Conditional disable when there are less than 3 points in
35299
+ // the graph
35300
+ ,
35301
+ disabled: disableCloseButton,
35302
+ style: {
35303
+ width: "100%",
35304
+ marginLeft: "20px"
35305
+ },
35306
+ tabIndex: disableCloseButton ? -1 : 0,
35307
+ onClick: () => {
35308
+ props.dispatch(actions.polygon.closePolygon());
35309
+ }
35310
+ }, perseusStrings.closePolygon);
35311
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
35680
35312
  style: {
35681
35313
  flexDirection: "row",
35682
35314
  width: props.width
@@ -35686,8 +35318,13 @@ const renderPolygonGraphControls = props => {
35686
35318
  style: {
35687
35319
  width: "100%",
35688
35320
  marginLeft: "20px"
35689
- },
35690
- tabIndex: 0,
35321
+ }
35322
+ // Disable button when polygon is closed.
35323
+ ,
35324
+ disabled: closedPolygon
35325
+ // Do not make the button tabbable when it is disabled.
35326
+ ,
35327
+ tabIndex: closedPolygon ? -1 : 0,
35691
35328
  onClick: () => {
35692
35329
  props.dispatch(actions.polygon.addPoint([0, 0]));
35693
35330
  }
@@ -35695,19 +35332,21 @@ const renderPolygonGraphControls = props => {
35695
35332
  id: REMOVE_BUTTON_ID,
35696
35333
  kind: "secondary",
35697
35334
  color: "destructive"
35335
+ // Disable button when polygon is closed.
35336
+ ,
35337
+ disabled: closedPolygon || !shouldShowRemoveButton
35698
35338
  // This button is meant to be interacted with by the mouse only
35699
35339
  // Never allow learners to tab to this button
35700
35340
  ,
35701
35341
  tabIndex: -1,
35702
35342
  style: {
35703
35343
  width: "100%",
35704
- marginLeft: "20px",
35705
- visibility: shouldShowRemoveButton ? "visible" : "hidden"
35344
+ marginLeft: "20px"
35706
35345
  },
35707
35346
  onClick: _event => {
35708
35347
  props.dispatch(actions.polygon.removePoint(props.state.focusedPointIndex));
35709
35348
  }
35710
- }, perseusStrings.removePoint));
35349
+ }, perseusStrings.removePoint), polygonButton));
35711
35350
  };
35712
35351
  const renderGraphControls = props => {
35713
35352
  const {
@@ -35758,14 +35397,17 @@ function handleBlurEvent(_event, state, dispatch) {
35758
35397
  }
35759
35398
  function handleKeyboardEvent(event, state, dispatch) {
35760
35399
  if (isUnlimitedGraphState(state)) {
35761
- if (event.key === "Backspace") {
35400
+ if (event.key === "Backspace" || event.key === "Delete") {
35762
35401
  // TODO(benchristel): Checking classList here is a hack to prevent
35763
35402
  // points from being deleted if the user presses the backspace key
35764
35403
  // while the whole graph is focused. Instead of doing this, we
35765
35404
  // should move the keyboard event handler to the movable point
35766
35405
  // handle element.
35767
35406
  if (document.activeElement?.classList.contains("movable-point__focusable-handle")) {
35768
- dispatch(actions.global.deleteIntent());
35407
+ // Only allow delete if type is point or a polygon that is open.
35408
+ if (state.type === "point" || state.type === "polygon" && !state.closedPolygon) {
35409
+ dispatch(actions.global.deleteIntent());
35410
+ }
35769
35411
  }
35770
35412
 
35771
35413
  // After removing a point blur
@@ -36015,7 +35657,8 @@ function initializeGraphState(params) {
36015
35657
  focusedPointIndex: null,
36016
35658
  showRemovePointButton: false,
36017
35659
  interactionMode: "mouse",
36018
- showKeyboardInteractionInvitation: false
35660
+ showKeyboardInteractionInvitation: false,
35661
+ closedPolygon: false
36019
35662
  };
36020
35663
  case "point":
36021
35664
  return {
@@ -36299,6 +35942,10 @@ function interactiveGraphReducer(state, action) {
36299
35942
  return doDeleteIntent(state);
36300
35943
  case CLICK_POINT:
36301
35944
  return doClickPoint(state, action);
35945
+ case CLOSE_POLYGON:
35946
+ return doClosePolygon(state);
35947
+ case OPEN_POLYGON:
35948
+ return doOpenPolygon(state);
36302
35949
  case CHANGE_INTERACTION_MODE:
36303
35950
  return doChangeInteractionMode(state, action);
36304
35951
  case CHANGE_KEYBOARD_INVITATION_VISIBILITY:
@@ -36355,6 +36002,24 @@ function doClickPoint(state, action) {
36355
36002
  }
36356
36003
  return state;
36357
36004
  }
36005
+ function doClosePolygon(state) {
36006
+ if (isUnlimitedGraphState(state) && state.type === "polygon") {
36007
+ return {
36008
+ ...state,
36009
+ closedPolygon: true
36010
+ };
36011
+ }
36012
+ return state;
36013
+ }
36014
+ function doOpenPolygon(state) {
36015
+ if (isUnlimitedGraphState(state) && state.type === "polygon") {
36016
+ return {
36017
+ ...state,
36018
+ closedPolygon: false
36019
+ };
36020
+ }
36021
+ return state;
36022
+ }
36358
36023
  function doChangeInteractionMode(state, action) {
36359
36024
  if (isUnlimitedGraphState(state)) {
36360
36025
  const nextKeyboardInvitation = action.mode === "keyboard" ? false : state.showKeyboardInteractionInvitation;
@@ -36562,8 +36227,12 @@ function doMovePoint(state, action) {
36562
36227
  newValue: newValue
36563
36228
  });
36564
36229
 
36565
- // Reject the move if it would cause the sides of the polygon to cross
36566
- if (polygonSidesIntersect(newCoords)) {
36230
+ // Boolean value to track whether we can let the polygon sides interact.
36231
+ // They can interact if it's an unlimited polygon that is open.
36232
+ const polygonSidesCanIntersect = state.numSides === "unlimited" && !state.closedPolygon;
36233
+
36234
+ // Reject the move if it would cause the sides of the polygon to cross.
36235
+ if (!polygonSidesCanIntersect && polygonSidesIntersect(newCoords)) {
36567
36236
  return state;
36568
36237
  }
36569
36238
  return {
@@ -36728,14 +36397,15 @@ function doAddPoint(state, action) {
36728
36397
  return state;
36729
36398
  }
36730
36399
  }
36400
+ const newCoords = [...state.coords, snappedPoint];
36731
36401
 
36732
36402
  // If there's no point in spot where we want the new point to go we add it there
36733
36403
  return {
36734
36404
  ...state,
36735
36405
  hasBeenInteractedWith: true,
36736
- coords: [...state.coords, snappedPoint],
36406
+ coords: newCoords,
36737
36407
  showRemovePointButton: false,
36738
- focusedPointIndex: state.coords.length
36408
+ focusedPointIndex: newCoords.length - 1
36739
36409
  };
36740
36410
  }
36741
36411
  function doRemovePoint(state, action) {
@@ -39031,6 +38701,8 @@ var InteractiveGraph$1 = {
39031
38701
  displayName: "Interactive graph (Assessments only)",
39032
38702
  widget: InteractiveGraph,
39033
38703
  staticTransform: staticTransform$3,
38704
+ // TODO(LEMS-2656): remove TS suppression
38705
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInteractiveGraphUserInput'.
39034
38706
  scorer: scoreInteractiveGraph
39035
38707
  };
39036
38708
 
@@ -40040,6 +39712,8 @@ var LabelImage$1 = {
40040
39712
  widget: LabelImageWithDependencies,
40041
39713
  accessible: true,
40042
39714
  isLintable: true,
39715
+ // TODO(LEMS-2656): remove TS suppression
39716
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusLabelImageUserInput'.
40043
39717
  scorer: scoreLabelImage
40044
39718
  };
40045
39719
 
@@ -40979,6 +40653,8 @@ var Matcher$1 = {
40979
40653
  displayName: "Matcher (two column)",
40980
40654
  widget: Matcher,
40981
40655
  isLintable: true,
40656
+ // TODO(LEMS-2656): remove TS suppression
40657
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatcherUserInput'.
40982
40658
  scorer: scoreMatcher
40983
40659
  };
40984
40660
 
@@ -40995,7 +40671,35 @@ const getPromptJSON$8 = (renderProps, userInput) => {
40995
40671
  };
40996
40672
  };
40997
40673
 
40674
+ /**
40675
+ * Checks user input from the matrix widget to see if it is scorable.
40676
+ *
40677
+ * Note: The matrix widget cannot do much validation without the Scoring
40678
+ * Data because of its use of KhanAnswerTypes as a core part of scoring.
40679
+ *
40680
+ * @see `scoreMatrix()` for more details.
40681
+ */
40682
+ function validateMatrix(userInput, validationData, strings) {
40683
+ const supplied = userInput.answers;
40684
+ const suppliedSize = getMatrixSize(supplied);
40685
+ for (let row = 0; row < suppliedSize[0]; row++) {
40686
+ for (let col = 0; col < suppliedSize[1]; col++) {
40687
+ if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
40688
+ return {
40689
+ type: "invalid",
40690
+ message: strings.fillAllCells
40691
+ };
40692
+ }
40693
+ }
40694
+ }
40695
+ return null;
40696
+ }
40697
+
40998
40698
  function scoreMatrix(userInput, rubric, strings) {
40699
+ const validationResult = validateMatrix(userInput, rubric, strings);
40700
+ if (validationResult != null) {
40701
+ return validationResult;
40702
+ }
40999
40703
  const solution = rubric.answers;
41000
40704
  const supplied = userInput.answers;
41001
40705
  const solutionSize = getMatrixSize(solution);
@@ -41003,13 +40707,9 @@ function scoreMatrix(userInput, rubric, strings) {
41003
40707
  const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
41004
40708
  const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
41005
40709
  let message = null;
41006
- let hasEmptyCell = false;
41007
40710
  let incorrect = false;
41008
40711
  ___default["default"](suppliedSize[0]).times(row => {
41009
40712
  ___default["default"](suppliedSize[1]).times(col => {
41010
- if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
41011
- hasEmptyCell = true;
41012
- }
41013
40713
  if (!incorrectSize) {
41014
40714
  const validator = createValidator(
41015
40715
  // @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
@@ -41027,12 +40727,6 @@ function scoreMatrix(userInput, rubric, strings) {
41027
40727
  }
41028
40728
  });
41029
40729
  });
41030
- if (hasEmptyCell) {
41031
- return {
41032
- type: "invalid",
41033
- message: strings.fillAllCells
41034
- };
41035
- }
41036
40730
  if (incorrectSize) {
41037
40731
  return {
41038
40732
  type: "points",
@@ -41460,6 +41154,8 @@ var Matrix$1 = {
41460
41154
  transform: propTransform$1,
41461
41155
  staticTransform: staticTransform$2,
41462
41156
  isLintable: true,
41157
+ // TODO(LEMS-2656): remove TS suppression
41158
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
41463
41159
  scorer: scoreMatrix
41464
41160
  };
41465
41161
 
@@ -43095,6 +42791,8 @@ var NumberLine$1 = {
43095
42791
  widget: NumberLine,
43096
42792
  transform: numberLineTransform,
43097
42793
  staticTransform: staticTransform$1,
42794
+ // TODO(LEMS-2656): remove TS suppression
42795
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
43098
42796
  scorer: scoreNumberLine
43099
42797
  };
43100
42798
 
@@ -43669,6 +43367,8 @@ var Orderer$1 = {
43669
43367
  hidden: true,
43670
43368
  widget: Orderer,
43671
43369
  isLintable: true,
43370
+ // TODO(LEMS-2656): remove TS suppression
43371
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
43672
43372
  scorer: scoreOrderer
43673
43373
  };
43674
43374
 
@@ -44761,6 +44461,8 @@ var Plotter$1 = {
44761
44461
  hidden: true,
44762
44462
  widget: Plotter,
44763
44463
  staticTransform: staticTransform,
44464
+ // TODO(LEMS-2656): remove TS suppression
44465
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
44764
44466
  scorer: scorePlotter
44765
44467
  };
44766
44468
 
@@ -44944,6 +44646,8 @@ var Sorter$1 = {
44944
44646
  displayName: "Sorter",
44945
44647
  widget: Sorter,
44946
44648
  isLintable: true,
44649
+ // TODO(LEMS-2656): remove TS suppression
44650
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
44947
44651
  scorer: scoreSorter
44948
44652
  };
44949
44653
 
@@ -45236,6 +44940,8 @@ var Table$1 = {
45236
44940
  transform: propTransform,
45237
44941
  hidden: true,
45238
44942
  isLintable: true,
44943
+ // TODO(LEMS-2656): remove TS suppression
44944
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
45239
44945
  scorer: scoreTable
45240
44946
  };
45241
44947
 
@@ -45421,7 +45127,7 @@ const init = function () {
45421
45127
 
45422
45128
  // This file is processed by a Rollup plugin (replace) to inject the production
45423
45129
  const libName = "@khanacademy/perseus";
45424
- const libVersion = "42.0.3";
45130
+ const libVersion = "43.0.1";
45425
45131
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45426
45132
 
45427
45133
  const apiVersion = {
@@ -46026,13 +45732,6 @@ class ServerItemRenderer extends React__namespace.Component {
46026
45732
  this.props.onRendered(true);
46027
45733
  }
46028
45734
  }
46029
- if (this.props.score && this.props.score !== prevProps.score) {
46030
- const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
46031
- this.setState({
46032
- questionCompleted: this.props.score.correct,
46033
- questionHighlightedWidgets: emptyQuestionAreaWidgets
46034
- });
46035
- }
46036
45735
  }
46037
45736
  componentWillUnmount() {
46038
45737
  if (this.blurTimeoutID != null) {
@@ -46121,7 +45820,7 @@ class ServerItemRenderer extends React__namespace.Component {
46121
45820
 
46122
45821
  /**
46123
45822
  * Accepts a question area widgetId, or an answer area widgetId of
46124
- * the form "answer-numeric-input 1", or the string "answer-area"
45823
+ * the form "answer-input-number 1", or the string "answer-area"
46125
45824
  * for the whole answer area (if the answer area is a single widget).
46126
45825
  */
46127
45826
  _setWidgetProps(widgetId, newProps, callback) {
@@ -46168,37 +45867,18 @@ class ServerItemRenderer extends React__namespace.Component {
46168
45867
  return this.questionRenderer.getPromptJSON();
46169
45868
  }
46170
45869
 
46171
- /**
46172
- * Returns an array of the widget `.getUserInput()` results
46173
- *
46174
- * TODO: can we remove this? Seems to be just for backwards
46175
- * compatibility with old Perseus Chrome logging
46176
- * @deprecated use getUserInput
46177
- */
46178
- getUserInputLegacy() {
46179
- return this.questionRenderer.getUserInput();
46180
- }
46181
-
46182
- /**
46183
- * Returns an object of the widget `.getUserInput()` results
46184
- */
46185
- getUserInput() {
46186
- return this.questionRenderer.getUserInputMap();
46187
- }
46188
-
46189
45870
  /**
46190
45871
  * Grades the item.
46191
- *
46192
- * @deprecated use scorePerseusItem
46193
45872
  */
46194
45873
  scoreInput() {
46195
- const guess = this.getUserInput();
46196
- const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
45874
+ const guessAndScore = this.questionRenderer.guessAndScore();
45875
+ const guess = guessAndScore[0];
45876
+ const score = guessAndScore[1];
46197
45877
 
46198
45878
  // Continue to include an empty guess for the now defunct answer area.
46199
45879
  // TODO(alex): Check whether we rely on the format here for
46200
45880
  // analyzing ProblemLogs. If not, remove this layer.
46201
- const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
45881
+ const maxCompatGuess = [guess, []];
46202
45882
  const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
46203
45883
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
46204
45884
  this.setState({
@@ -48060,6 +47740,138 @@ const registerAllWidgetsForTesting = () => {
48060
47740
  replaceDeprecatedWidgets();
48061
47741
  };
48062
47742
 
47743
+ // These are all the current widgets that require user-input and are supported for automatic scoring
47744
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
47745
+
47746
+ /**
47747
+ * Regex for widget placeholders in a string.
47748
+ *
47749
+ * First capture group is the widget ID (ex. 'radio 1')
47750
+ * Second capture group is the widget type (ex. "radio)
47751
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
47752
+ */
47753
+ function getWidgetRegex() {
47754
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
47755
+ }
47756
+
47757
+ /**
47758
+ * Add a widget placeholder using the widget ID.
47759
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
47760
+ *
47761
+ * @param {string} id
47762
+ * @returns {string}
47763
+ */
47764
+ function addWidget(id) {
47765
+ return `[[☃ ${id}]]`;
47766
+ }
47767
+
47768
+ /**
47769
+ * Get a widget type by a widget's ID
47770
+ * (please don't derive type from ID)
47771
+ *
47772
+ * @param {string} widgetId the ID of the widget
47773
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47774
+ * @returns {string} the widget type (ie "radio")
47775
+ */
47776
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
47777
+ const widget = widgetMap[WidgetId];
47778
+ return widget?.type ?? null;
47779
+ }
47780
+
47781
+ /**
47782
+ * Does the content have a specific type of widget?
47783
+ *
47784
+ * @param {string} type the type of the widget in question (ie "radio")
47785
+ * @param {string} content the string to search through
47786
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47787
+ * @returns {boolean} if the content includes the widget type
47788
+ */
47789
+ function contentHasWidgetType(type, content, widgetMap) {
47790
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
47791
+ }
47792
+
47793
+ /**
47794
+ * Extract all widget IDs, which includes the widget type and instance number.
47795
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
47796
+ *
47797
+ * Content should contain Perseus widget placeholders,
47798
+ * which look like: '[[☃ radio 1]]'.
47799
+ *
47800
+ * @param {string} content
47801
+ * @returns {ReadonlyArray<string>} widgetIds
47802
+ */
47803
+ function getWidgetIdsFromContent(content) {
47804
+ const widgets = [];
47805
+ const localWidgetRegex = getWidgetRegex();
47806
+ let match = localWidgetRegex.exec(content);
47807
+ while (match !== null) {
47808
+ widgets.push(match[1]);
47809
+ match = localWidgetRegex.exec(content);
47810
+ }
47811
+ return widgets;
47812
+ }
47813
+
47814
+ /**
47815
+ * Get a list of widget IDs from content,
47816
+ * but only for specific widget types
47817
+ *
47818
+ * @param {string} type the type of widget (ie "radio")
47819
+ * @param {string} content the string to parse
47820
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47821
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
47822
+ */
47823
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
47824
+ const rv = [];
47825
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
47826
+ widgetIdsInContent.forEach(widgetId => {
47827
+ const widget = widgetMap[widgetId];
47828
+ if (widget?.type === type) {
47829
+ rv.push(widgetId);
47830
+ }
47831
+ });
47832
+ return rv;
47833
+ }
47834
+
47835
+ /**
47836
+ * Pull the widget map out of ItemData
47837
+ *
47838
+ * @param {PerseusItem} PerseusItem containing a widgetMap
47839
+ * @returns {WidgetMap} the widget map in the PerseusItem
47840
+ */
47841
+ function getWidgetsMapFromItemData(itemData) {
47842
+ return itemData.question.widgets;
47843
+ }
47844
+
47845
+ /**
47846
+ * Get the widget information from a WidgetMap
47847
+ *
47848
+ * @param {string} widgetId the ID of the widget
47849
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
47850
+ */
47851
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
47852
+ return widgetMap[widgetId] ?? null;
47853
+ }
47854
+
47855
+ /**
47856
+ * Get select widgets from a widget map.
47857
+ * Useful for multi-items that needs to split
47858
+ * one PerseusItem into several
47859
+ *
47860
+ * @param {ReadonlyArray<string>} widgetIds to extract
47861
+ * @param {PerseusWidgetsMap} widgetMap to extract from
47862
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
47863
+ */
47864
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
47865
+ const widgets = {};
47866
+ widgetIds.forEach(widgetId => {
47867
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
47868
+ if (widget) {
47869
+ widgets[widgetId] = widget;
47870
+ }
47871
+ });
47872
+ return widgets;
47873
+ }
47874
+
48063
47875
  /**
48064
47876
  * This function extracts the answers from the widgets.
48065
47877
  *
@@ -50061,8 +49873,6 @@ exports.buildMapper = buildMapper;
50061
49873
  exports.components = components;
50062
49874
  exports.containerSizeClass = containerSizeClass;
50063
49875
  exports.contentHasWidgetType = contentHasWidgetType;
50064
- exports.conversionRequired = conversionRequired;
50065
- exports.convertDeprecatedWidgets = convertDeprecatedWidgets;
50066
49876
  exports.convertWidgetNameToEnum = convertWidgetNameToEnum;
50067
49877
  exports.displaySigFigs = displaySigFigs;
50068
49878
  exports.generateTestCategorizerWidget = generateTestCategorizerWidget;
@@ -50109,12 +49919,12 @@ exports.libVersion = libVersion;
50109
49919
  exports.lockedFigureColors = lockedFigureColors;
50110
49920
  exports.lockedFigureFillStyles = lockedFigureFillStyles;
50111
49921
  exports.makeSafeUrl = makeSafeUrl;
49922
+ exports.mathOnlyParser = mathOnlyParser;
50112
49923
  exports.mediaQueries = mediaQueries;
50113
49924
  exports.parsePerseusItem = parsePerseusItem;
50114
49925
  exports.plotterPlotTypes = plotterPlotTypes;
50115
49926
  exports.preprocessTex = preprocessTex;
50116
49927
  exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
50117
- exports.scorePerseusItem = scorePerseusItem;
50118
49928
  exports.shapes = shapes;
50119
49929
  exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
50120
49930
  exports.traverse = traverse;