@khanacademy/perseus 42.0.3 → 43.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
 
@@ -31787,7 +31440,7 @@ function isUnlimitedGraphState(state) {
31787
31440
  function replaceOutsideTeX(mathString) {
31788
31441
  // All the information we need is in the first section,
31789
31442
  // whether it's typed as "blockmath" or "paragraph"
31790
- const firstSection = SimpleMarkdown__namespace.parse(mathString)[0];
31443
+ const firstSection = pureMarkdown.parse(mathString)[0];
31791
31444
 
31792
31445
  // If it's blockMath, the outer level has the full math content.
31793
31446
  if (firstSection.type === "blockMath") {
@@ -31840,6 +31493,38 @@ function escapeSpecialChars(str) {
31840
31493
  return str.replace(/([$\\{}])/g, "\\$1");
31841
31494
  }
31842
31495
 
31496
+ /**
31497
+ * Parse a string of text and math into a list of objects with type and content
31498
+ *
31499
+ * Example: "Pi is about $\frac{22}{7}$" ==>
31500
+ * [
31501
+ * {type: "text", content: "Pi is about "},
31502
+ * {type: "math", content: "\\frac{22}{7}"},
31503
+ * ]
31504
+ */
31505
+ const mathOnlyParser = SimpleMarkdown__default["default"].parserFor({
31506
+ math: {
31507
+ ...pureMarkdown.pureMarkdownRules.math,
31508
+ order: 0
31509
+ },
31510
+ text: {
31511
+ order: 1,
31512
+ match: SimpleMarkdown__default["default"].anyScopeRegex(/^([^$\\{}]+)/),
31513
+ parse: capture => ({
31514
+ content: capture[0]
31515
+ })
31516
+ },
31517
+ specialCharacter: {
31518
+ order: 2,
31519
+ match: SimpleMarkdown__default["default"].anyScopeRegex(/^(\\[\S\s]|\$|\\$|{|})/),
31520
+ parse: capture => ({
31521
+ content: capture[0]
31522
+ })
31523
+ }
31524
+ }, {
31525
+ inline: true
31526
+ });
31527
+
31843
31528
  function AxisLabels() {
31844
31529
  const {
31845
31530
  range,
@@ -32786,12 +32471,17 @@ const ItemExtras = [
32786
32471
  // prettier-ignore
32787
32472
 
32788
32473
  // prettier-ignore
32789
- // While this widget is deprecated, we still need the type for conversion purposes
32474
+
32790
32475
  // prettier-ignore
32476
+
32791
32477
  // prettier-ignore
32478
+
32792
32479
  // prettier-ignore
32480
+
32793
32481
  //prettier-ignore
32482
+
32794
32483
  // A background image applied to various widgets.
32484
+
32795
32485
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
32796
32486
 
32797
32487
  // 2D range: xMin, xMax, yMin, yMax
@@ -33293,7 +32983,9 @@ const actions = {
33293
32983
  removePoint,
33294
32984
  focusPoint,
33295
32985
  blurPoint,
33296
- clickPoint
32986
+ clickPoint,
32987
+ closePolygon,
32988
+ openPolygon
33297
32989
  },
33298
32990
  quadratic: {
33299
32991
  movePoint
@@ -33372,6 +33064,18 @@ function changeKeyboardInvitationVisibility(shouldShow) {
33372
33064
  shouldShow
33373
33065
  };
33374
33066
  }
33067
+ const CLOSE_POLYGON = "close-polygon";
33068
+ function closePolygon() {
33069
+ return {
33070
+ type: CLOSE_POLYGON
33071
+ };
33072
+ }
33073
+ const OPEN_POLYGON = "open-polygon";
33074
+ function openPolygon() {
33075
+ return {
33076
+ type: OPEN_POLYGON
33077
+ };
33078
+ }
33375
33079
  const MOVE_ALL = "move-all";
33376
33080
  function moveAll(delta) {
33377
33081
  return {
@@ -34915,33 +34619,17 @@ const LimitedPolygonGraph = props => {
34915
34619
  }
34916
34620
  })));
34917
34621
  };
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
34622
  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
34623
  const {
34928
34624
  dispatch
34929
34625
  } = props;
34930
34626
  const {
34931
34627
  coords,
34932
- showAngles,
34933
- showSides,
34934
- range,
34935
- snapStep,
34936
- snapTo = "grid"
34628
+ closedPolygon
34937
34629
  } = props.graphState;
34938
34630
  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
34631
  const {
34943
34632
  range: [x, y],
34944
- disableKeyboardInteraction,
34945
34633
  graphDimensionsInPixels
34946
34634
  } = graphConfig;
34947
34635
  const widthPx = graphDimensionsInPixels[0];
@@ -34952,116 +34640,66 @@ const UnlimitedPolygonGraph = props => {
34952
34640
  // TODO(benchristel): can the default set of points be removed here? I don't
34953
34641
  // think coords can be null.
34954
34642
  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
34643
  React__namespace.useEffect(() => {
34971
34644
  const focusedIndex = props.graphState.focusedPointIndex;
34972
34645
  if (focusedIndex != null) {
34973
34646
  pointRef.current[focusedIndex]?.focus();
34974
34647
  }
34975
34648
  }, [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)",
34649
+ if (closedPolygon) {
34650
+ const closedPolygonProps = {
34651
+ ...props,
34652
+ numSides: coords.length
34653
+ };
34654
+ return /*#__PURE__*/React__namespace.createElement(LimitedPolygonGraph, closedPolygonProps);
34655
+ } else {
34656
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
34997
34657
  style: {
34998
- fill: "transparent"
34658
+ fill: "rgba(0,0,0,0)",
34659
+ cursor: "crosshair"
34660
+ },
34661
+ width: widthPx,
34662
+ height: heightPx,
34663
+ x: left,
34664
+ y: top,
34665
+ onClick: event => {
34666
+ const elementRect = event.currentTarget.getBoundingClientRect();
34667
+ const x = event.clientX - elementRect.x;
34668
+ const y = event.clientY - elementRect.y;
34669
+ const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
34670
+ dispatch(actions.polygon.addPoint(graphCoordinates[0]));
34671
+ }
34672
+ }), /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
34673
+ points: [...points],
34674
+ color: "var(--movable-line-stroke-color)",
34675
+ svgPolylineProps: {
34676
+ strokeWidth: "var(--movable-line-stroke-weight)",
34677
+ style: {
34678
+ fill: "transparent"
34679
+ }
34999
34680
  }
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"
34681
+ }), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34682
+ key: i,
34683
+ point: point,
34684
+ sequenceNumber: i + 1,
34685
+ onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
34686
+ ref: ref => {
34687
+ pointRef.current[i] = ref;
35035
34688
  },
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));
34689
+ onFocus: () => {
34690
+ dispatch(actions.polygon.focusPoint(i));
35042
34691
  },
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
- })));
34692
+ onClick: () => {
34693
+ // If the point being clicked is the first point and
34694
+ // there's enough points to form a polygon (3 or more)
34695
+ // Close the shape before setting focus.
34696
+ if (i === 0 && props.graphState.coords.length >= 3) {
34697
+ dispatch(actions.polygon.closePolygon());
34698
+ }
34699
+ dispatch(actions.polygon.clickPoint(i));
34700
+ }
34701
+ })));
34702
+ }
35065
34703
  };
35066
34704
  function getLines(points) {
35067
34705
  return points.map((point, i) => {
@@ -35660,23 +35298,47 @@ const renderPointGraphControls = props => {
35660
35298
  }
35661
35299
  }, perseusStrings.removePoint));
35662
35300
  };
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
35301
  const renderPolygonGraphControls = props => {
35670
35302
  const {
35671
35303
  interactionMode,
35672
35304
  showRemovePointButton,
35673
- focusedPointIndex
35305
+ focusedPointIndex,
35306
+ closedPolygon,
35307
+ coords
35674
35308
  } = props.state;
35675
35309
  const {
35676
35310
  perseusStrings
35677
35311
  } = props;
35678
35312
  const shouldShowRemoveButton = showRemovePointButton && focusedPointIndex !== null;
35679
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
35313
+
35314
+ // If polygon is closed, show open button.
35315
+ // If polygon is open, show close button.
35316
+ const polygonButton = closedPolygon ? /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
35317
+ kind: "secondary",
35318
+ style: {
35319
+ width: "100%",
35320
+ marginLeft: "20px"
35321
+ },
35322
+ tabIndex: 0,
35323
+ onClick: () => {
35324
+ props.dispatch(actions.polygon.openPolygon());
35325
+ }
35326
+ }, perseusStrings.openPolygon) : /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
35327
+ kind: "secondary"
35328
+ // Conditional disable when there are less than 3 points in
35329
+ // the graph
35330
+ ,
35331
+ disabled: coords.length < 3,
35332
+ style: {
35333
+ width: "100%",
35334
+ marginLeft: "20px"
35335
+ },
35336
+ tabIndex: 0,
35337
+ onClick: () => {
35338
+ props.dispatch(actions.polygon.closePolygon());
35339
+ }
35340
+ }, perseusStrings.closePolygon);
35341
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
35680
35342
  style: {
35681
35343
  flexDirection: "row",
35682
35344
  width: props.width
@@ -35686,8 +35348,13 @@ const renderPolygonGraphControls = props => {
35686
35348
  style: {
35687
35349
  width: "100%",
35688
35350
  marginLeft: "20px"
35689
- },
35690
- tabIndex: 0,
35351
+ }
35352
+ // Disable button when polygon is closed.
35353
+ ,
35354
+ disabled: closedPolygon
35355
+ // Do not make the button tabbable when it is disabled.
35356
+ ,
35357
+ tabIndex: closedPolygon ? -1 : 0,
35691
35358
  onClick: () => {
35692
35359
  props.dispatch(actions.polygon.addPoint([0, 0]));
35693
35360
  }
@@ -35695,19 +35362,21 @@ const renderPolygonGraphControls = props => {
35695
35362
  id: REMOVE_BUTTON_ID,
35696
35363
  kind: "secondary",
35697
35364
  color: "destructive"
35365
+ // Disable button when polygon is closed.
35366
+ ,
35367
+ disabled: closedPolygon || !shouldShowRemoveButton
35698
35368
  // This button is meant to be interacted with by the mouse only
35699
35369
  // Never allow learners to tab to this button
35700
35370
  ,
35701
35371
  tabIndex: -1,
35702
35372
  style: {
35703
35373
  width: "100%",
35704
- marginLeft: "20px",
35705
- visibility: shouldShowRemoveButton ? "visible" : "hidden"
35374
+ marginLeft: "20px"
35706
35375
  },
35707
35376
  onClick: _event => {
35708
35377
  props.dispatch(actions.polygon.removePoint(props.state.focusedPointIndex));
35709
35378
  }
35710
- }, perseusStrings.removePoint));
35379
+ }, perseusStrings.removePoint), polygonButton));
35711
35380
  };
35712
35381
  const renderGraphControls = props => {
35713
35382
  const {
@@ -35758,14 +35427,17 @@ function handleBlurEvent(_event, state, dispatch) {
35758
35427
  }
35759
35428
  function handleKeyboardEvent(event, state, dispatch) {
35760
35429
  if (isUnlimitedGraphState(state)) {
35761
- if (event.key === "Backspace") {
35430
+ if (event.key === "Backspace" || event.key === "Delete") {
35762
35431
  // TODO(benchristel): Checking classList here is a hack to prevent
35763
35432
  // points from being deleted if the user presses the backspace key
35764
35433
  // while the whole graph is focused. Instead of doing this, we
35765
35434
  // should move the keyboard event handler to the movable point
35766
35435
  // handle element.
35767
35436
  if (document.activeElement?.classList.contains("movable-point__focusable-handle")) {
35768
- dispatch(actions.global.deleteIntent());
35437
+ // Only allow delete if type is point or a polygon that is open.
35438
+ if (state.type === "point" || state.type === "polygon" && !state.closedPolygon) {
35439
+ dispatch(actions.global.deleteIntent());
35440
+ }
35769
35441
  }
35770
35442
 
35771
35443
  // After removing a point blur
@@ -36015,7 +35687,8 @@ function initializeGraphState(params) {
36015
35687
  focusedPointIndex: null,
36016
35688
  showRemovePointButton: false,
36017
35689
  interactionMode: "mouse",
36018
- showKeyboardInteractionInvitation: false
35690
+ showKeyboardInteractionInvitation: false,
35691
+ closedPolygon: false
36019
35692
  };
36020
35693
  case "point":
36021
35694
  return {
@@ -36299,6 +35972,10 @@ function interactiveGraphReducer(state, action) {
36299
35972
  return doDeleteIntent(state);
36300
35973
  case CLICK_POINT:
36301
35974
  return doClickPoint(state, action);
35975
+ case CLOSE_POLYGON:
35976
+ return doClosePolygon(state);
35977
+ case OPEN_POLYGON:
35978
+ return doOpenPolygon(state);
36302
35979
  case CHANGE_INTERACTION_MODE:
36303
35980
  return doChangeInteractionMode(state, action);
36304
35981
  case CHANGE_KEYBOARD_INVITATION_VISIBILITY:
@@ -36355,6 +36032,24 @@ function doClickPoint(state, action) {
36355
36032
  }
36356
36033
  return state;
36357
36034
  }
36035
+ function doClosePolygon(state) {
36036
+ if (isUnlimitedGraphState(state) && state.type === "polygon") {
36037
+ return {
36038
+ ...state,
36039
+ closedPolygon: true
36040
+ };
36041
+ }
36042
+ return state;
36043
+ }
36044
+ function doOpenPolygon(state) {
36045
+ if (isUnlimitedGraphState(state) && state.type === "polygon") {
36046
+ return {
36047
+ ...state,
36048
+ closedPolygon: false
36049
+ };
36050
+ }
36051
+ return state;
36052
+ }
36358
36053
  function doChangeInteractionMode(state, action) {
36359
36054
  if (isUnlimitedGraphState(state)) {
36360
36055
  const nextKeyboardInvitation = action.mode === "keyboard" ? false : state.showKeyboardInteractionInvitation;
@@ -36728,14 +36423,15 @@ function doAddPoint(state, action) {
36728
36423
  return state;
36729
36424
  }
36730
36425
  }
36426
+ const newCoords = [...state.coords, snappedPoint];
36731
36427
 
36732
36428
  // If there's no point in spot where we want the new point to go we add it there
36733
36429
  return {
36734
36430
  ...state,
36735
36431
  hasBeenInteractedWith: true,
36736
- coords: [...state.coords, snappedPoint],
36432
+ coords: newCoords,
36737
36433
  showRemovePointButton: false,
36738
- focusedPointIndex: state.coords.length
36434
+ focusedPointIndex: newCoords.length - 1
36739
36435
  };
36740
36436
  }
36741
36437
  function doRemovePoint(state, action) {
@@ -39031,6 +38727,8 @@ var InteractiveGraph$1 = {
39031
38727
  displayName: "Interactive graph (Assessments only)",
39032
38728
  widget: InteractiveGraph,
39033
38729
  staticTransform: staticTransform$3,
38730
+ // TODO(LEMS-2656): remove TS suppression
38731
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInteractiveGraphUserInput'.
39034
38732
  scorer: scoreInteractiveGraph
39035
38733
  };
39036
38734
 
@@ -40040,6 +39738,8 @@ var LabelImage$1 = {
40040
39738
  widget: LabelImageWithDependencies,
40041
39739
  accessible: true,
40042
39740
  isLintable: true,
39741
+ // TODO(LEMS-2656): remove TS suppression
39742
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusLabelImageUserInput'.
40043
39743
  scorer: scoreLabelImage
40044
39744
  };
40045
39745
 
@@ -40979,6 +40679,8 @@ var Matcher$1 = {
40979
40679
  displayName: "Matcher (two column)",
40980
40680
  widget: Matcher,
40981
40681
  isLintable: true,
40682
+ // TODO(LEMS-2656): remove TS suppression
40683
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatcherUserInput'.
40982
40684
  scorer: scoreMatcher
40983
40685
  };
40984
40686
 
@@ -40995,7 +40697,35 @@ const getPromptJSON$8 = (renderProps, userInput) => {
40995
40697
  };
40996
40698
  };
40997
40699
 
40700
+ /**
40701
+ * Checks user input from the matrix widget to see if it is scorable.
40702
+ *
40703
+ * Note: The matrix widget cannot do much validation without the Scoring
40704
+ * Data because of its use of KhanAnswerTypes as a core part of scoring.
40705
+ *
40706
+ * @see `scoreMatrix()` for more details.
40707
+ */
40708
+ function validateMatrix(userInput, validationData, strings) {
40709
+ const supplied = userInput.answers;
40710
+ const suppliedSize = getMatrixSize(supplied);
40711
+ for (let row = 0; row < suppliedSize[0]; row++) {
40712
+ for (let col = 0; col < suppliedSize[1]; col++) {
40713
+ if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
40714
+ return {
40715
+ type: "invalid",
40716
+ message: strings.fillAllCells
40717
+ };
40718
+ }
40719
+ }
40720
+ }
40721
+ return null;
40722
+ }
40723
+
40998
40724
  function scoreMatrix(userInput, rubric, strings) {
40725
+ const validationResult = validateMatrix(userInput, rubric, strings);
40726
+ if (validationResult != null) {
40727
+ return validationResult;
40728
+ }
40999
40729
  const solution = rubric.answers;
41000
40730
  const supplied = userInput.answers;
41001
40731
  const solutionSize = getMatrixSize(solution);
@@ -41003,13 +40733,9 @@ function scoreMatrix(userInput, rubric, strings) {
41003
40733
  const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
41004
40734
  const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
41005
40735
  let message = null;
41006
- let hasEmptyCell = false;
41007
40736
  let incorrect = false;
41008
40737
  ___default["default"](suppliedSize[0]).times(row => {
41009
40738
  ___default["default"](suppliedSize[1]).times(col => {
41010
- if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
41011
- hasEmptyCell = true;
41012
- }
41013
40739
  if (!incorrectSize) {
41014
40740
  const validator = createValidator(
41015
40741
  // @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
@@ -41027,12 +40753,6 @@ function scoreMatrix(userInput, rubric, strings) {
41027
40753
  }
41028
40754
  });
41029
40755
  });
41030
- if (hasEmptyCell) {
41031
- return {
41032
- type: "invalid",
41033
- message: strings.fillAllCells
41034
- };
41035
- }
41036
40756
  if (incorrectSize) {
41037
40757
  return {
41038
40758
  type: "points",
@@ -41460,6 +41180,8 @@ var Matrix$1 = {
41460
41180
  transform: propTransform$1,
41461
41181
  staticTransform: staticTransform$2,
41462
41182
  isLintable: true,
41183
+ // TODO(LEMS-2656): remove TS suppression
41184
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
41463
41185
  scorer: scoreMatrix
41464
41186
  };
41465
41187
 
@@ -43095,6 +42817,8 @@ var NumberLine$1 = {
43095
42817
  widget: NumberLine,
43096
42818
  transform: numberLineTransform,
43097
42819
  staticTransform: staticTransform$1,
42820
+ // TODO(LEMS-2656): remove TS suppression
42821
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
43098
42822
  scorer: scoreNumberLine
43099
42823
  };
43100
42824
 
@@ -43669,6 +43393,8 @@ var Orderer$1 = {
43669
43393
  hidden: true,
43670
43394
  widget: Orderer,
43671
43395
  isLintable: true,
43396
+ // TODO(LEMS-2656): remove TS suppression
43397
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
43672
43398
  scorer: scoreOrderer
43673
43399
  };
43674
43400
 
@@ -44761,6 +44487,8 @@ var Plotter$1 = {
44761
44487
  hidden: true,
44762
44488
  widget: Plotter,
44763
44489
  staticTransform: staticTransform,
44490
+ // TODO(LEMS-2656): remove TS suppression
44491
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
44764
44492
  scorer: scorePlotter
44765
44493
  };
44766
44494
 
@@ -44944,6 +44672,8 @@ var Sorter$1 = {
44944
44672
  displayName: "Sorter",
44945
44673
  widget: Sorter,
44946
44674
  isLintable: true,
44675
+ // TODO(LEMS-2656): remove TS suppression
44676
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
44947
44677
  scorer: scoreSorter
44948
44678
  };
44949
44679
 
@@ -45236,6 +44966,8 @@ var Table$1 = {
45236
44966
  transform: propTransform,
45237
44967
  hidden: true,
45238
44968
  isLintable: true,
44969
+ // TODO(LEMS-2656): remove TS suppression
44970
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
45239
44971
  scorer: scoreTable
45240
44972
  };
45241
44973
 
@@ -45421,7 +45153,7 @@ const init = function () {
45421
45153
 
45422
45154
  // This file is processed by a Rollup plugin (replace) to inject the production
45423
45155
  const libName = "@khanacademy/perseus";
45424
- const libVersion = "42.0.3";
45156
+ const libVersion = "43.0.0";
45425
45157
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45426
45158
 
45427
45159
  const apiVersion = {
@@ -46026,13 +45758,6 @@ class ServerItemRenderer extends React__namespace.Component {
46026
45758
  this.props.onRendered(true);
46027
45759
  }
46028
45760
  }
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
45761
  }
46037
45762
  componentWillUnmount() {
46038
45763
  if (this.blurTimeoutID != null) {
@@ -46121,7 +45846,7 @@ class ServerItemRenderer extends React__namespace.Component {
46121
45846
 
46122
45847
  /**
46123
45848
  * Accepts a question area widgetId, or an answer area widgetId of
46124
- * the form "answer-numeric-input 1", or the string "answer-area"
45849
+ * the form "answer-input-number 1", or the string "answer-area"
46125
45850
  * for the whole answer area (if the answer area is a single widget).
46126
45851
  */
46127
45852
  _setWidgetProps(widgetId, newProps, callback) {
@@ -46168,37 +45893,18 @@ class ServerItemRenderer extends React__namespace.Component {
46168
45893
  return this.questionRenderer.getPromptJSON();
46169
45894
  }
46170
45895
 
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
45896
  /**
46190
45897
  * Grades the item.
46191
- *
46192
- * @deprecated use scorePerseusItem
46193
45898
  */
46194
45899
  scoreInput() {
46195
- const guess = this.getUserInput();
46196
- const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
45900
+ const guessAndScore = this.questionRenderer.guessAndScore();
45901
+ const guess = guessAndScore[0];
45902
+ const score = guessAndScore[1];
46197
45903
 
46198
45904
  // Continue to include an empty guess for the now defunct answer area.
46199
45905
  // TODO(alex): Check whether we rely on the format here for
46200
45906
  // analyzing ProblemLogs. If not, remove this layer.
46201
- const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
45907
+ const maxCompatGuess = [guess, []];
46202
45908
  const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
46203
45909
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
46204
45910
  this.setState({
@@ -48060,6 +47766,138 @@ const registerAllWidgetsForTesting = () => {
48060
47766
  replaceDeprecatedWidgets();
48061
47767
  };
48062
47768
 
47769
+ // These are all the current widgets that require user-input and are supported for automatic scoring
47770
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
47771
+
47772
+ /**
47773
+ * Regex for widget placeholders in a string.
47774
+ *
47775
+ * First capture group is the widget ID (ex. 'radio 1')
47776
+ * Second capture group is the widget type (ex. "radio)
47777
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
47778
+ */
47779
+ function getWidgetRegex() {
47780
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
47781
+ }
47782
+
47783
+ /**
47784
+ * Add a widget placeholder using the widget ID.
47785
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
47786
+ *
47787
+ * @param {string} id
47788
+ * @returns {string}
47789
+ */
47790
+ function addWidget(id) {
47791
+ return `[[☃ ${id}]]`;
47792
+ }
47793
+
47794
+ /**
47795
+ * Get a widget type by a widget's ID
47796
+ * (please don't derive type from ID)
47797
+ *
47798
+ * @param {string} widgetId the ID of the widget
47799
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47800
+ * @returns {string} the widget type (ie "radio")
47801
+ */
47802
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
47803
+ const widget = widgetMap[WidgetId];
47804
+ return widget?.type ?? null;
47805
+ }
47806
+
47807
+ /**
47808
+ * Does the content have a specific type of widget?
47809
+ *
47810
+ * @param {string} type the type of the widget in question (ie "radio")
47811
+ * @param {string} content the string to search through
47812
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47813
+ * @returns {boolean} if the content includes the widget type
47814
+ */
47815
+ function contentHasWidgetType(type, content, widgetMap) {
47816
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
47817
+ }
47818
+
47819
+ /**
47820
+ * Extract all widget IDs, which includes the widget type and instance number.
47821
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
47822
+ *
47823
+ * Content should contain Perseus widget placeholders,
47824
+ * which look like: '[[☃ radio 1]]'.
47825
+ *
47826
+ * @param {string} content
47827
+ * @returns {ReadonlyArray<string>} widgetIds
47828
+ */
47829
+ function getWidgetIdsFromContent(content) {
47830
+ const widgets = [];
47831
+ const localWidgetRegex = getWidgetRegex();
47832
+ let match = localWidgetRegex.exec(content);
47833
+ while (match !== null) {
47834
+ widgets.push(match[1]);
47835
+ match = localWidgetRegex.exec(content);
47836
+ }
47837
+ return widgets;
47838
+ }
47839
+
47840
+ /**
47841
+ * Get a list of widget IDs from content,
47842
+ * but only for specific widget types
47843
+ *
47844
+ * @param {string} type the type of widget (ie "radio")
47845
+ * @param {string} content the string to parse
47846
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47847
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
47848
+ */
47849
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
47850
+ const rv = [];
47851
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
47852
+ widgetIdsInContent.forEach(widgetId => {
47853
+ const widget = widgetMap[widgetId];
47854
+ if (widget?.type === type) {
47855
+ rv.push(widgetId);
47856
+ }
47857
+ });
47858
+ return rv;
47859
+ }
47860
+
47861
+ /**
47862
+ * Pull the widget map out of ItemData
47863
+ *
47864
+ * @param {PerseusItem} PerseusItem containing a widgetMap
47865
+ * @returns {WidgetMap} the widget map in the PerseusItem
47866
+ */
47867
+ function getWidgetsMapFromItemData(itemData) {
47868
+ return itemData.question.widgets;
47869
+ }
47870
+
47871
+ /**
47872
+ * Get the widget information from a WidgetMap
47873
+ *
47874
+ * @param {string} widgetId the ID of the widget
47875
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
47876
+ */
47877
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
47878
+ return widgetMap[widgetId] ?? null;
47879
+ }
47880
+
47881
+ /**
47882
+ * Get select widgets from a widget map.
47883
+ * Useful for multi-items that needs to split
47884
+ * one PerseusItem into several
47885
+ *
47886
+ * @param {ReadonlyArray<string>} widgetIds to extract
47887
+ * @param {PerseusWidgetsMap} widgetMap to extract from
47888
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
47889
+ */
47890
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
47891
+ const widgets = {};
47892
+ widgetIds.forEach(widgetId => {
47893
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
47894
+ if (widget) {
47895
+ widgets[widgetId] = widget;
47896
+ }
47897
+ });
47898
+ return widgets;
47899
+ }
47900
+
48063
47901
  /**
48064
47902
  * This function extracts the answers from the widgets.
48065
47903
  *
@@ -50061,8 +49899,6 @@ exports.buildMapper = buildMapper;
50061
49899
  exports.components = components;
50062
49900
  exports.containerSizeClass = containerSizeClass;
50063
49901
  exports.contentHasWidgetType = contentHasWidgetType;
50064
- exports.conversionRequired = conversionRequired;
50065
- exports.convertDeprecatedWidgets = convertDeprecatedWidgets;
50066
49902
  exports.convertWidgetNameToEnum = convertWidgetNameToEnum;
50067
49903
  exports.displaySigFigs = displaySigFigs;
50068
49904
  exports.generateTestCategorizerWidget = generateTestCategorizerWidget;
@@ -50109,12 +49945,12 @@ exports.libVersion = libVersion;
50109
49945
  exports.lockedFigureColors = lockedFigureColors;
50110
49946
  exports.lockedFigureFillStyles = lockedFigureFillStyles;
50111
49947
  exports.makeSafeUrl = makeSafeUrl;
49948
+ exports.mathOnlyParser = mathOnlyParser;
50112
49949
  exports.mediaQueries = mediaQueries;
50113
49950
  exports.parsePerseusItem = parsePerseusItem;
50114
49951
  exports.plotterPlotTypes = plotterPlotTypes;
50115
49952
  exports.preprocessTex = preprocessTex;
50116
49953
  exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
50117
- exports.scorePerseusItem = scorePerseusItem;
50118
49954
  exports.shapes = shapes;
50119
49955
  exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
50120
49956
  exports.traverse = traverse;