@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/es/index.js CHANGED
@@ -22,8 +22,7 @@ import PropTypes from 'prop-types';
22
22
  import { number as number$1, point, vector as vector$1, line } from '@khanacademy/kmath';
23
23
  import { entries, UnreachableCaseError, keys } from '@khanacademy/wonder-stuff-core';
24
24
  import { CircularSpinner } from '@khanacademy/wonder-blocks-progress-spinner';
25
- import SimpleMarkdown$1 from '@khanacademy/simple-markdown';
26
- import * as SimpleMarkdown from '@khanacademy/pure-markdown';
25
+ import SimpleMarkdown from '@khanacademy/simple-markdown';
27
26
  import { pureMarkdownRules } from '@khanacademy/pure-markdown';
28
27
  import { TextField } from '@khanacademy/wonder-blocks-form';
29
28
  import Button from '@khanacademy/wonder-blocks-button';
@@ -33,7 +32,7 @@ import { OptionItem, SingleSelect, MultiSelect } from '@khanacademy/wonder-block
33
32
  import caretDown from '@phosphor-icons/core/regular/caret-down.svg';
34
33
  import caretUp from '@phosphor-icons/core/regular/caret-up.svg';
35
34
  import debounce$2 from 'lodash.debounce';
36
- import { vec, Coordinates, Ellipse, Plot as Plot$3, Line as Line$4, Point as Point$2, Polygon, Text as Text$1, useTransformContext, Mafs } from 'mafs';
35
+ import { vec, Coordinates, Ellipse, Plot as Plot$3, Line as Line$4, Point as Point$2, Polygon, Text as Text$1, useTransformContext, Polyline, Mafs } from 'mafs';
37
36
  import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
38
37
  import questionIcon from '@phosphor-icons/core/regular/question.svg';
39
38
  import { useDrag } from '@use-gesture/react';
@@ -240,6 +239,8 @@ const mockStrings = {
240
239
  addPoint: "Add Point",
241
240
  removePoint: "Remove Point",
242
241
  graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
242
+ closePolygon: "Close shape",
243
+ openPolygon: "Re-open shape",
243
244
  srPointAtCoordinates: ({
244
245
  num,
245
246
  x,
@@ -370,7 +371,10 @@ class InnerMathInput extends React.Component {
370
371
  locale
371
372
  } = this.context;
372
373
  // Initialize MathQuill.MathField instance
373
- this.__mathField = createMathField(this.__mathFieldWrapperRef, locale, this.props.mathInputStrings, baseConfig => _extends({}, baseConfig, {
374
+ this.__mathField = createMathField(this.__mathFieldWrapperRef, locale, this.props.mathInputStrings,
375
+ // TODO(LEMS-2656): remove TS suppression
376
+ // @ts-expect-error: Type 'EditableMathQuill' is not assignable to type 'MathFieldInterface'.
377
+ baseConfig => _extends({}, baseConfig, {
374
378
  handlers: {
375
379
  edit: debounce(mathField => {
376
380
  // This handler is guaranteed to be called on change, but
@@ -2134,6 +2138,24 @@ const KhanAnswerTypes = {
2134
2138
  }
2135
2139
  };
2136
2140
 
2141
+ /**
2142
+ * Checks user input from the expression widget to see if it is scorable.
2143
+ *
2144
+ * Note: Most of the expression widget's validation requires the Rubric because
2145
+ * of its use of KhanAnswerTypes as a core part of scoring.
2146
+ *
2147
+ * @see `scoreExpression()` for more details.
2148
+ */
2149
+ function validateExpression(userInput) {
2150
+ if (userInput === "") {
2151
+ return {
2152
+ type: "invalid",
2153
+ message: null
2154
+ };
2155
+ }
2156
+ return null;
2157
+ }
2158
+
2137
2159
  /* Content creators input a list of answers which are matched from top to
2138
2160
  * bottom. The intent is that they can include spcific solutions which should
2139
2161
  * be graded as correct or incorrect (or ungraded!) first, then get more
@@ -2153,6 +2175,10 @@ const KhanAnswerTypes = {
2153
2175
  * - Otherwise, pass through the resulting points and message.
2154
2176
  */
2155
2177
  function scoreExpression(userInput, rubric, strings, locale) {
2178
+ const validationError = validateExpression(userInput);
2179
+ if (validationError) {
2180
+ return validationError;
2181
+ }
2156
2182
  const options = _.clone(rubric);
2157
2183
  _.extend(options, {
2158
2184
  decimal_separator: getDecimalSeparator(locale)
@@ -2405,11 +2431,16 @@ class Expression extends React.Component {
2405
2431
  getPromptJSON() {
2406
2432
  return getPromptJSON$t(this.props, this.getUserInput());
2407
2433
  }
2434
+ // TODO(LEMS-2656): remove TS suppression
2435
+ // @ts-expect-error: Type 'FocusPath' is not assignable to type 'InputPath'.
2408
2436
  focusInputPath(inputPath) {
2409
2437
  // eslint-disable-next-line react/no-string-refs
2410
2438
  // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
2411
2439
  this.refs.input.focus();
2412
2440
  }
2441
+
2442
+ // TODO(LEMS-2656): remove TS suppression
2443
+ // @ts-expect-error: Type 'FocusPath' is not assignable to type 'InputPath'.
2413
2444
  blurInputPath(inputPath) {
2414
2445
  var _this$refs$input;
2415
2446
  // eslint-disable-next-line react/no-string-refs
@@ -2592,7 +2623,7 @@ const keypadConfigurationForProps = widgetOptions => {
2592
2623
  times: widgetOptions.times
2593
2624
  };
2594
2625
  };
2595
- const propUpgrades$3 = {
2626
+ const propUpgrades$2 = {
2596
2627
  /* c8 ignore next */
2597
2628
  "1": v0props => ({
2598
2629
  times: v0props.times,
@@ -2652,10 +2683,14 @@ var Expression$1 = {
2652
2683
  major: 1,
2653
2684
  minor: 0
2654
2685
  },
2655
- propUpgrades: propUpgrades$3,
2686
+ propUpgrades: propUpgrades$2,
2656
2687
  // For use by the editor
2657
2688
  isLintable: true,
2689
+ // TODO(LEMS-2656): remove TS suppression
2690
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'.
2658
2691
  scorer: scoreExpression,
2692
+ // TODO(LEMS-2656): remove TS suppression
2693
+ // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusExpressionRubric'.
2659
2694
  getOneCorrectAnswerFromRubric(rubric) {
2660
2695
  const correctAnswers = (rubric.answerForms || []).filter(answerForm => answerForm.considered === "correct");
2661
2696
  if (correctAnswers.length === 0) {
@@ -2738,6 +2773,8 @@ const nestedMap$2 = function nestedMap(children, func, context) {
2738
2773
  return nestedMap(child, func);
2739
2774
  });
2740
2775
  }
2776
+ // TODO(LEMS-2656): remove TS suppression
2777
+ // @ts-expect-error: T | ReadonlyArray<T> is not assignable to T
2741
2778
  return func.call(context, children);
2742
2779
  };
2743
2780
 
@@ -14489,7 +14526,9 @@ const constraints$2 = {
14489
14526
  });
14490
14527
 
14491
14528
  // bound the delta by the calculated bounds
14492
- const boundedDelta = _.reduce(deltaBounds, function (delta, bound) {
14529
+ const boundedDelta = _.reduce(deltaBounds,
14530
+ // @ts-expect-error: Type 'number[]' is not assignable to type 'Coord'.
14531
+ function (delta, bound) {
14493
14532
  const lower = bound[0];
14494
14533
  const upper = bound[1];
14495
14534
  const deltaX = Math.max(lower[0], Math.min(upper[0], delta[0]));
@@ -15020,6 +15059,7 @@ const constraints$1 = {
15020
15059
  };
15021
15060
  },
15022
15061
  snap: function (snap) {
15062
+ // @ts-expect-error: Type 'boolean | Coord' is not assignable to type 'Coord'.
15023
15063
  return function (coord) {
15024
15064
  if (snap === null) {
15025
15065
  // TODO(benchristel), NOTE(kevinb): this should probably return
@@ -15042,6 +15082,8 @@ const constraints$1 = {
15042
15082
  return optionalPaddingPx;
15043
15083
  }
15044
15084
  })();
15085
+
15086
+ // @ts-expect-error: Type 'boolean | Coord' is not assignable to type 'Coord'.
15045
15087
  return function (coord, prev, options) {
15046
15088
  const graphie = this.graphie;
15047
15089
  range = range || graphie.range;
@@ -15932,15 +15974,15 @@ const constraints = {
15932
15974
  });
15933
15975
 
15934
15976
  // bound the delta by the calculated bounds
15935
- const boundedDelta = _.reduce(deltaBounds, function (delta, bound) {
15977
+ const boundedDelta = _.reduce(deltaBounds,
15978
+ // @ts-expect-error - TS2345 - Type 'number[]' is not assignable to type 'Coord'.
15979
+ function (delta, bound) {
15936
15980
  const lower = bound[0];
15937
15981
  const upper = bound[1];
15938
15982
  const deltaX = Math.max(lower[0], Math.min(upper[0], delta[0]));
15939
15983
  const deltaY = Math.max(lower[1], Math.min(upper[1], delta[1]));
15940
15984
  return [deltaX, deltaY];
15941
15985
  }, delta);
15942
-
15943
- // @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'Coord'.
15944
15986
  return vector$1.add(prevCoord, boundedDelta);
15945
15987
  };
15946
15988
  }
@@ -16826,6 +16868,7 @@ class ImageLoader extends React.Component {
16826
16868
 
16827
16869
  this.img = new Image();
16828
16870
  this.img.onload = this.handleLoad;
16871
+ // @ts-expect-error: Type 'string | Event' is not assignable to type 'Event'.
16829
16872
  this.img.onerror = this.handleError;
16830
16873
  this.img.src = this.props.src;
16831
16874
  };
@@ -17916,6 +17959,7 @@ class InteractionTracker {
17916
17959
  this.widgetType = widgetType;
17917
17960
  this.widgetID = widgetID;
17918
17961
  this.setting = setting;
17962
+ // @ts-expect-error: Type 'T | undefined' is not assignable to type 'T'.
17919
17963
  this.track = this._track;
17920
17964
  }
17921
17965
  }
@@ -17926,7 +17970,7 @@ class InteractionTracker {
17926
17970
  */
17927
17971
  const arrayRules = {
17928
17972
  fence: {
17929
- match: SimpleMarkdown$1.defaultRules.fence.match,
17973
+ match: SimpleMarkdown.defaultRules.fence.match,
17930
17974
  order: 1,
17931
17975
  parse: (capture, state, parse) => ({
17932
17976
  type: "codeBlock",
@@ -17935,14 +17979,14 @@ const arrayRules = {
17935
17979
  })
17936
17980
  },
17937
17981
  paragraph: {
17938
- match: SimpleMarkdown$1.defaultRules.paragraph.match,
17982
+ match: SimpleMarkdown.defaultRules.paragraph.match,
17939
17983
  order: 2,
17940
17984
  parse: (capture, state, parse) => ({
17941
17985
  content: capture[1]
17942
17986
  })
17943
17987
  }
17944
17988
  };
17945
- const builtArrayParser = SimpleMarkdown$1.parserFor(arrayRules);
17989
+ const builtArrayParser = SimpleMarkdown.parserFor(arrayRules);
17946
17990
 
17947
17991
  // This should just return an array of strings! magick!
17948
17992
  const parseToArray = source => {
@@ -18367,7 +18411,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18367
18411
  // iOS 14.4, in order for VoiceOver to work.
18368
18412
  table: _extends({}, pureMarkdownRules.table, {
18369
18413
  react: function (node, output, state) {
18370
- const table = SimpleMarkdown$1.defaultRules.table.react(node, output, state);
18414
+ const table = SimpleMarkdown.defaultRules.table.react(node, output, state);
18371
18415
  if (!state.isMobile) {
18372
18416
  return table;
18373
18417
  }
@@ -18416,7 +18460,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18416
18460
  contents = rules$1.lint.react(node.table, output, state);
18417
18461
  }
18418
18462
  } else {
18419
- const tableOutput = SimpleMarkdown$1.defaultRules.table.react(node.table, output, state);
18463
+ const tableOutput = SimpleMarkdown.defaultRules.table.react(node.table, output, state);
18420
18464
  const caption = /*#__PURE__*/React.createElement("caption", {
18421
18465
  key: "caption",
18422
18466
  className: "perseus-table-title"
@@ -18486,7 +18530,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18486
18530
  // window and handle reverse tabnapping phishing attacks
18487
18531
  link: _extends({}, pureMarkdownRules.link, {
18488
18532
  react: function (node, output, state) {
18489
- const link = SimpleMarkdown$1.defaultRules.link.react(node, output, state);
18533
+ const link = SimpleMarkdown.defaultRules.link.react(node, output, state);
18490
18534
  const href = link.props.href;
18491
18535
  let rel = null;
18492
18536
  // The href prop should always be here, but just in case somehow the article's
@@ -18519,7 +18563,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18519
18563
  className: "perseus-markdown-alt perseus-sr-only"
18520
18564
  }, output(node.content, state));
18521
18565
  }
18522
- return SimpleMarkdown$1.defaultRules.codeBlock.react(node, output, state);
18566
+ return SimpleMarkdown.defaultRules.codeBlock.react(node, output, state);
18523
18567
  }
18524
18568
  }),
18525
18569
  // The lint rule never actually matches anything.
@@ -18561,7 +18605,7 @@ const inlineNodeTypes = {
18561
18605
  };
18562
18606
 
18563
18607
  // @ts-expect-error - TS2345 - Argument of type '{ readonly columns: { readonly react: (node: any, output: any, state: any) => Element; readonly order: -2; readonly match: any; readonly parse: (capture: any, parse: any, state: any) => any; }; ... 35 more ...; readonly text: TextInOutRule; }' is not assignable to parameter of type 'ParserRules'.
18564
- const builtParser$1 = SimpleMarkdown$1.parserFor(rules$1);
18608
+ const builtParser$1 = SimpleMarkdown.parserFor(rules$1);
18565
18609
  const parse$3 = (source, state) => {
18566
18610
  const paragraphedSource = source + "\n\n";
18567
18611
  return builtParser$1(paragraphedSource, _extends({}, state, {
@@ -18654,10 +18698,10 @@ var PerseusMarkdown = {
18654
18698
  traverseContent: traverseContent,
18655
18699
  parse: parse$3,
18656
18700
  parseInline: inlineParser,
18657
- reactFor: SimpleMarkdown$1.reactFor,
18658
- ruleOutput: SimpleMarkdown$1.ruleOutput(rules$1, "react"),
18659
- basicOutput: SimpleMarkdown$1.reactFor(SimpleMarkdown$1.ruleOutput(rules$1, "react")),
18660
- sanitizeUrl: SimpleMarkdown$1.sanitizeUrl
18701
+ reactFor: SimpleMarkdown.reactFor,
18702
+ ruleOutput: SimpleMarkdown.ruleOutput(rules$1, "react"),
18703
+ basicOutput: SimpleMarkdown.reactFor(SimpleMarkdown.ruleOutput(rules$1, "react")),
18704
+ sanitizeUrl: SimpleMarkdown.sanitizeUrl
18661
18705
  };
18662
18706
 
18663
18707
  class QuestionParagraph extends React.Component {
@@ -18674,337 +18718,6 @@ class QuestionParagraph extends React.Component {
18674
18718
  }
18675
18719
  }
18676
18720
 
18677
- // Methods to convert input-number widgets to numeric-input widgets
18678
-
18679
- const inputNumberToNumericInput = json => {
18680
- // First we need to create a map of the old input-number keys to the new numeric-input keys
18681
- // so that we can ensure we update the content and widgets accordingly
18682
- const renameMap = getInputNumberRenameMap(json);
18683
-
18684
- // Then we can use this to update the JSON
18685
- return convertInputNumberJson(json, renameMap);
18686
- };
18687
-
18688
- // We need to be able to run this code recursively in order to convert nested input-number widgets
18689
- const convertInputNumberJson = (json, renameMap) => {
18690
- const updatedContent = convertDeprecatedWidgetsInContent(json, renameMap);
18691
- const updatedWidgets = convertInputNumberWidgetOptions(json, renameMap);
18692
- const modernizedJson = _extends({}, json, {
18693
- content: updatedContent,
18694
- widgets: updatedWidgets
18695
- });
18696
- return modernizedJson;
18697
- };
18698
-
18699
- // Convert the input-number json in the widgets of a PerseusRenderer object
18700
- const convertInputNumberWidgetOptions = (json, renameMap) => {
18701
- const widgets = _extends({}, json.widgets);
18702
- // The question.widgets is a dictionary map of widgets, so we need to loop through in order to convert input-number to numeric-input
18703
- // which can exist as both the key or as a value on widget.type.
18704
- for (const key of Object.keys(widgets)) {
18705
- // Loop through the keys of the widgets dictionary
18706
- if (widgets[key].options.widgets) {
18707
- widgets[key].options = _extends({}, inputNumberToNumericInput(widgets[key].options));
18708
- }
18709
- // Check if the widget is an input-number
18710
- if (widgets[key].type === "input-number") {
18711
- // If the answerType is not number or percent, we need to provide
18712
- // the answer form for the numeric-input widget
18713
- const provideAnswerForm = widgets[key].options.answerType !== "number" && widgets[key].options.answerType !== "percent";
18714
- // We need to determine the mathFormat for the numeric-input widget
18715
- const mathFormat = widgets[key].options.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
18716
- : widgets[key].options.answerType; // Otherwise, we can use the answerType directly
18717
-
18718
- // We need to update the answers prop to match the numeric-input widget format
18719
- const answers = [{
18720
- value: widgets[key].options.value,
18721
- simplify: widgets[key].options.simplify,
18722
- // Input Number doesn't have a strict prop, so we default to false
18723
- strict: false,
18724
- // We only want to set maxError if the inexact prop is true
18725
- maxError: widgets[key].options.inexact ? widgets[key].options.maxError : 0,
18726
- status: "correct",
18727
- // Input-number only allows correct answers
18728
- message: ""
18729
- }];
18730
-
18731
- // Add the required answerForms if provided/applicable
18732
- if (provideAnswerForm) {
18733
- answers[0].answerForms = [mathFormat];
18734
- }
18735
-
18736
- // Update the options prop to match the numeric-input widget format
18737
- const upgradedWidgetOptions = {
18738
- answers,
18739
- size: widgets[key].options.size,
18740
- coefficient: false,
18741
- // input-number doesn't have a coefficient prop
18742
- labelText: "",
18743
- // input-number doesn't have a labelText prop
18744
- static: false,
18745
- // static is always false for numeric-input
18746
- rightAlign: widgets[key].options.rightAlign || false
18747
- };
18748
- const upgradedWidget = {
18749
- options: upgradedWidgetOptions,
18750
- type: "numeric-input"
18751
- };
18752
- const newWidgetName = renameMap[key];
18753
- // Create the new key entry
18754
- widgets[newWidgetName] = upgradedWidget;
18755
-
18756
- // We need to delete the old widget key
18757
- delete widgets[key];
18758
- }
18759
- }
18760
- return widgets;
18761
- };
18762
-
18763
- // Convert the deprecated widget refs in the content string
18764
- // of a PerseusRenderer object to their renamed equivalents
18765
- const convertDeprecatedWidgetsInContent = (json, renameMap) => {
18766
- return Object.keys(renameMap).reduce((newContent, oldKey) => {
18767
- const newKey = renameMap[oldKey];
18768
- return newKey ? newContent.replace(oldKey, newKey) : newContent;
18769
- }, json.content);
18770
- };
18771
-
18772
- // Create a map of the old input-number keys to the new numeric-input keys
18773
- const getInputNumberRenameMap = json => {
18774
- const numericRegex = /(?<=\[\[\u2603 )(numeric-input \d+)(?=\]\])/g;
18775
- const inputNumberRegex = /(?<=\[\[\u2603 )(input-number \d+)(?=\]\])/g;
18776
-
18777
- // Get all the content strings within the json, which might be nested within widgets
18778
- const allContentStrings = json.content;
18779
-
18780
- // Loop through the content strings to get all the input-number widgets
18781
- const renameMap = {};
18782
- const inputNumberMatches = [...(allContentStrings.match(inputNumberRegex) || [])];
18783
-
18784
- // We want to count any pre-existing numeric-input widgets
18785
- // so that we can start the new ids at the next number
18786
- const numericMatches = [...(allContentStrings.match(numericRegex) || [])];
18787
- let currentNumericCount = numericMatches.reduce((count, match) => {
18788
- const id = parseInt(match.split(" ")[1], 10);
18789
- return id >= count ? id + 1 : count; // We want to start at the next highest number
18790
- }, 1);
18791
-
18792
- // Now that we have all the necessary information, we can create the renameMap
18793
- for (const match of inputNumberMatches) {
18794
- const oldKey = match;
18795
- const newKey = `numeric-input ${currentNumericCount}`;
18796
- renameMap[oldKey] = newKey;
18797
- currentNumericCount++;
18798
- }
18799
- return renameMap;
18800
- };
18801
-
18802
- // Convert the user input data keys from input-number to numeric-input
18803
- const convertUserInputNumberData = (userInputMap, renameMap) => {
18804
- const updatedUserInputMap = _extends({}, userInputMap);
18805
- for (const key of Object.keys(userInputMap)) {
18806
- if (key.includes("input-number")) {
18807
- const updatedKey = renameMap[key];
18808
- updatedUserInputMap[updatedKey] = userInputMap[key];
18809
- delete updatedUserInputMap[key];
18810
- }
18811
- }
18812
- return updatedUserInputMap;
18813
- };
18814
-
18815
- // Used to convert InputNumber widgets to NumericInput widgets for scoring
18816
- const convertInputNumberForScoring = (rubric, userInputMap) => {
18817
- // First we need to create a map of the old input-number keys to the new numeric-input keys
18818
- // so that we can ensure we update the content, widgets, AND userInput accordingly
18819
- const renameMap = getInputNumberRenameMap(rubric);
18820
- const convertedRubric = convertInputNumberJson(rubric, renameMap);
18821
- const convertedUserData = convertUserInputNumberData(userInputMap, renameMap);
18822
- return {
18823
- convertedRubric,
18824
- convertedUserData
18825
- };
18826
- };
18827
-
18828
- const widgetRegExes = [/input-number \d+/]; // We can add more regexes here in the future
18829
-
18830
- // This utils in this file are used to modernize our Perseus JSON structure,
18831
- // so that we can convert deprecated widgets to their modern equivalents when
18832
- // content creators use the Editor Page to update content containing these widgets.
18833
- // Currently, we're only converting input-number to numeric-input,
18834
- // but we can add more conversions here in the future.
18835
-
18836
- // Modernize the json content of a PerseusRenderer object
18837
- // by converting deprecated widgets to their modern equivalents
18838
- const convertDeprecatedWidgets = json => {
18839
- // Currently we're only converting input-number to numeric-input,
18840
- // But we can add more conversions here in the future
18841
- return inputNumberToNumericInput(json);
18842
- };
18843
- const conversionRequired = json => {
18844
- // If there's no content, then there's no conversion required
18845
- if (!json.content) {
18846
- return false;
18847
- }
18848
- // Check the content string for any top-level input-number widgets
18849
- if (widgetRegExes.some(regex => regex.test(json.content))) {
18850
- return true;
18851
- }
18852
-
18853
- // If there's no deprecated widget in the top-level, then check for any nested widgets
18854
- for (const key of Object.keys(json.widgets)) {
18855
- if (json.widgets[key].options.widgets) {
18856
- const nestedJson = json.widgets[key].options;
18857
- if (conversionRequired(nestedJson)) {
18858
- return true;
18859
- }
18860
- }
18861
- }
18862
- return false;
18863
- };
18864
-
18865
- /**
18866
- * Convert the deprecated widgets in the rubric and user data to their
18867
- * modern equivalents for scoring. These need to be updated together
18868
- * in order to ensure that the widgetKeys match between the rubric and user data.
18869
- */
18870
- const convertDeprecatedWidgetsForScoring = (rubric, userInputMap) => {
18871
- return convertInputNumberForScoring(rubric, userInputMap);
18872
- };
18873
-
18874
- // These are all the current widgets that require user-input and are supported for automatic scoring
18875
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
18876
-
18877
- /**
18878
- * Regex for widget placeholders in a string.
18879
- *
18880
- * First capture group is the widget ID (ex. 'radio 1')
18881
- * Second capture group is the widget type (ex. "radio)
18882
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
18883
- */
18884
- function getWidgetRegex() {
18885
- return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
18886
- }
18887
-
18888
- /**
18889
- * Add a widget placeholder using the widget ID.
18890
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
18891
- *
18892
- * @param {string} id
18893
- * @returns {string}
18894
- */
18895
- function addWidget(id) {
18896
- return `[[☃ ${id}]]`;
18897
- }
18898
-
18899
- /**
18900
- * Get a widget type by a widget's ID
18901
- * (please don't derive type from ID)
18902
- *
18903
- * @param {string} widgetId the ID of the widget
18904
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18905
- * @returns {string} the widget type (ie "radio")
18906
- */
18907
- function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
18908
- var _widget$type;
18909
- const widget = widgetMap[WidgetId];
18910
- return (_widget$type = widget == null ? void 0 : widget.type) != null ? _widget$type : null;
18911
- }
18912
-
18913
- /**
18914
- * Does the content have a specific type of widget?
18915
- *
18916
- * @param {string} type the type of the widget in question (ie "radio")
18917
- * @param {string} content the string to search through
18918
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18919
- * @returns {boolean} if the content includes the widget type
18920
- */
18921
- function contentHasWidgetType(type, content, widgetMap) {
18922
- return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
18923
- }
18924
-
18925
- /**
18926
- * Extract all widget IDs, which includes the widget type and instance number.
18927
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
18928
- *
18929
- * Content should contain Perseus widget placeholders,
18930
- * which look like: '[[☃ radio 1]]'.
18931
- *
18932
- * @param {string} content
18933
- * @returns {ReadonlyArray<string>} widgetIds
18934
- */
18935
- function getWidgetIdsFromContent(content) {
18936
- const widgets = [];
18937
- const localWidgetRegex = getWidgetRegex();
18938
- let match = localWidgetRegex.exec(content);
18939
- while (match !== null) {
18940
- widgets.push(match[1]);
18941
- match = localWidgetRegex.exec(content);
18942
- }
18943
- return widgets;
18944
- }
18945
-
18946
- /**
18947
- * Get a list of widget IDs from content,
18948
- * but only for specific widget types
18949
- *
18950
- * @param {string} type the type of widget (ie "radio")
18951
- * @param {string} content the string to parse
18952
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18953
- * @returns {ReadonlyArray<string>} the widget type (ie "radio")
18954
- */
18955
- function getWidgetIdsFromContentByType(type, content, widgetMap) {
18956
- const rv = [];
18957
- const widgetIdsInContent = getWidgetIdsFromContent(content);
18958
- widgetIdsInContent.forEach(widgetId => {
18959
- const widget = widgetMap[widgetId];
18960
- if ((widget == null ? void 0 : widget.type) === type) {
18961
- rv.push(widgetId);
18962
- }
18963
- });
18964
- return rv;
18965
- }
18966
-
18967
- /**
18968
- * Pull the widget map out of ItemData
18969
- *
18970
- * @param {PerseusItem} PerseusItem containing a widgetMap
18971
- * @returns {WidgetMap} the widget map in the PerseusItem
18972
- */
18973
- function getWidgetsMapFromItemData(itemData) {
18974
- return itemData.question.widgets;
18975
- }
18976
-
18977
- /**
18978
- * Get the widget information from a WidgetMap
18979
- *
18980
- * @param {string} widgetId the ID of the widget
18981
- * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
18982
- */
18983
- function getWidgetFromWidgetMap(widgetId, widgetMap) {
18984
- var _widgetMap$widgetId;
18985
- return (_widgetMap$widgetId = widgetMap[widgetId]) != null ? _widgetMap$widgetId : null;
18986
- }
18987
-
18988
- /**
18989
- * Get select widgets from a widget map.
18990
- * Useful for multi-items that needs to split
18991
- * one PerseusItem into several
18992
- *
18993
- * @param {ReadonlyArray<string>} widgetIds to extract
18994
- * @param {PerseusWidgetsMap} widgetMap to extract from
18995
- * @return {PerseusWidgetsMap} a new widget map with requested widgets
18996
- */
18997
- function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
18998
- const widgets = {};
18999
- widgetIds.forEach(widgetId => {
19000
- const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
19001
- if (widget) {
19002
- widgets[widgetId] = widget;
19003
- }
19004
- });
19005
- return widgets;
19006
- }
19007
-
19008
18721
  const DEFAULT_ALIGNMENT$1 = "block";
19009
18722
  // NOTE(kevinb): "default" is not one in `validAlignments`.
19010
18723
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -19055,9 +18768,6 @@ const replaceDeprecatedWidgets = () => {
19055
18768
  replaceWidget("sequence", "deprecated-standin");
19056
18769
  replaceWidget("simulator", "deprecated-standin");
19057
18770
  replaceWidget("unit-input", "deprecated-standin");
19058
-
19059
- // Input-Number is a special case as it is being replaced by Numeric-Input
19060
- replaceWidget("input-number", "numeric-input");
19061
18771
  };
19062
18772
  const registerEditors = editorsToRegister => {
19063
18773
  editorsToRegister.forEach(editor => {
@@ -19092,11 +18802,6 @@ const replaceDeprecatedEditors = () => {
19092
18802
  replaceEditor("sequence", "deprecated-standin");
19093
18803
  replaceEditor("simulator", "deprecated-standin");
19094
18804
  replaceEditor("unit-input", "deprecated-standin");
19095
-
19096
- // We're replacing the input-number editor with the numeric-input editor here,
19097
- // but we're also modifying the JSON content to convert input-number into the
19098
- // numeric-input format over in editor-page.tsx on line 92.
19099
- replaceEditor("input-number", "numeric-input");
19100
18805
  };
19101
18806
  const getWidget = name => {
19102
18807
  var _widgets$name;
@@ -19147,8 +18852,10 @@ const getVersionVector = () => {
19147
18852
  const getPublicWidgets = () => {
19148
18853
  // TODO(alex): Update underscore.js so that _.pick can take a function.
19149
18854
  // @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.
19150
- return _.pick(widgets, _.reject(_.keys(widgets), function (name) {
19151
- return widgets[name].hidden || name === "input-number";
18855
+ return _.pick(widgets,
18856
+ // @ts-expect-error - TS2345 - Argument of type '(name: string) => boolean | undefined' is not assignable to parameter of type 'Iteratee<string[], boolean, string>'.
18857
+ _.reject(_.keys(widgets), function (name) {
18858
+ return widgets[name].hidden;
19152
18859
  }));
19153
18860
  };
19154
18861
  const isAccessible = widgetInfo => {
@@ -19466,34 +19173,6 @@ widgetIds, userInputMap, strings, locale) {
19466
19173
  }
19467
19174
  });
19468
19175
  }
19469
-
19470
- // TODO: combine scorePerseusItem with scoreWidgetsFunctional
19471
- // once scorePerseusItem is the only one calling scoreWidgetsFunctional
19472
- function scorePerseusItem(perseusRenderData, userInputMap,
19473
- // TODO(LEMS-2461,LEMS-2391): these probably
19474
- // need to be removed before we move this to the server
19475
- strings, locale) {
19476
- let convertedRenderData = perseusRenderData;
19477
- let convertedUserInputMap = userInputMap;
19478
-
19479
- // Check if the PerseusRenderer object contains any deprecated widgets that need to be converted
19480
- const mustConvertData = conversionRequired(perseusRenderData);
19481
- if (mustConvertData) {
19482
- const {
19483
- convertedRubric,
19484
- convertedUserData
19485
- } = convertDeprecatedWidgetsForScoring(perseusRenderData, userInputMap);
19486
- convertedRenderData = convertedRubric;
19487
- convertedUserInputMap = convertedUserData;
19488
- }
19489
-
19490
- // There seems to be a chance that PerseusRenderer.widgets might include
19491
- // widget data for widgets that are not in PerseusRenderer.content,
19492
- // so this checks that the widgets are being used before scoring them
19493
- const usedWidgetIds = getWidgetIdsFromContent(convertedRenderData.content);
19494
- const scores = scoreWidgetsFunctional(convertedRenderData.widgets, usedWidgetIds, convertedUserInputMap, strings, locale);
19495
- return Util.flattenScores(scores);
19496
- }
19497
19176
  function scoreWidgetsFunctional(widgets,
19498
19177
  // This is a port of old code, I'm not sure why
19499
19178
  // we need widgetIds vs the keys of the widgets object
@@ -19514,7 +19193,7 @@ widgetIds, userInputMap, strings, locale) {
19514
19193
  const userInput = userInputMap[id];
19515
19194
  const scorer = getWidgetScorer(widget.type);
19516
19195
  if (widget.type === "group") {
19517
- const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
19196
+ const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19518
19197
  widgetScores[id] = Util.flattenScores(scores);
19519
19198
  } else if (scorer) {
19520
19199
  widgetScores[id] = scorer(userInput, widget.options, strings, locale);
@@ -20346,7 +20025,7 @@ class Renderer extends React.Component {
20346
20025
  // Instead, we center them with css magic in styles.less
20347
20026
  // /cry(aria)
20348
20027
  this._foundTextNodes = true;
20349
- if (this.widgetIds.includes(node.id)) {
20028
+ if (_.contains(this.widgetIds, node.id)) {
20350
20029
  // We don't want to render a duplicate widget key/ref,
20351
20030
  // as this causes problems with react (for obvious
20352
20031
  // reasons). Instead we just notify the
@@ -20650,7 +20329,7 @@ class Renderer extends React.Component {
20650
20329
  };
20651
20330
  this.getInputPaths = () => {
20652
20331
  const inputPaths = [];
20653
- this.widgetIds.forEach(widgetId => {
20332
+ _.each(this.widgetIds, widgetId => {
20654
20333
  const widget = this.getWidgetInstance(widgetId);
20655
20334
  if (widget && widget.getInputPaths) {
20656
20335
  // Grab all input paths and add widgetID to the front
@@ -20658,7 +20337,7 @@ class Renderer extends React.Component {
20658
20337
  // Prefix paths with their widgetID and add to collective
20659
20338
  // list of paths.
20660
20339
  // @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
20661
- widgetInputPaths.forEach(inputPath => {
20340
+ _.each(widgetInputPaths, inputPath => {
20662
20341
  const relativeInputPath = [widgetId].concat(inputPath);
20663
20342
  inputPaths.push(relativeInputPath);
20664
20343
  });
@@ -20787,20 +20466,12 @@ class Renderer extends React.Component {
20787
20466
  this.getWidgetIds = () => {
20788
20467
  return this.widgetIds;
20789
20468
  };
20790
- /**
20791
- * @deprecated use scorePerseusItem
20792
- */
20793
- this.guessAndScore = () => {
20794
- const totalGuess = this.getUserInput();
20795
- const totalScore = this.score();
20796
- return [totalGuess, totalScore];
20797
- };
20798
20469
  this.examples = () => {
20799
20470
  const widgetIds = this.widgetIds;
20800
- const examples = widgetIds.map(widgetId => {
20471
+ const examples = _.compact(_.map(widgetIds, widgetId => {
20801
20472
  const widget = this.getWidgetInstance(widgetId);
20802
20473
  return widget != null && widget.examples ? widget.examples() : null;
20803
- }).filter(Boolean);
20474
+ }));
20804
20475
 
20805
20476
  // no widgets with examples
20806
20477
  if (!examples.length) {
@@ -21055,15 +20726,27 @@ class Renderer extends React.Component {
21055
20726
  }
21056
20727
 
21057
20728
  /**
21058
- * Scores the content.
21059
- *
21060
- * @deprecated use scorePerseusItem
20729
+ * Returns an object mapping from widget ID to perseus-style score.
20730
+ * The keys of this object are the values of the array returned
20731
+ * from `getWidgetIds`.
20732
+ */
20733
+ scoreWidgets() {
20734
+ return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20735
+ }
20736
+
20737
+ /**
20738
+ * Grades the content.
21061
20739
  */
21062
20740
  score() {
21063
- const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20741
+ const scores = this.scoreWidgets();
21064
20742
  const combinedScore = Util.flattenScores(scores);
21065
20743
  return combinedScore;
21066
20744
  }
20745
+ guessAndScore() {
20746
+ const totalGuess = this.getUserInput();
20747
+ const totalScore = this.score();
20748
+ return [totalGuess, totalScore];
20749
+ }
21067
20750
  render() {
21068
20751
  const apiOptions = this.getApiOptions();
21069
20752
  if (this.reuseMarkdown) {
@@ -22073,11 +21756,15 @@ class InputNumber extends React.Component {
22073
21756
  this.refs.input.focus();
22074
21757
  return true;
22075
21758
  };
21759
+ // TODO(LEMS-2656): remove TS suppression
21760
+ // @ts-expect-error: Type 'FocusPath' is not assignable to type 'Path'.
22076
21761
  this.focusInputPath = inputPath => {
22077
21762
  // eslint-disable-next-line react/no-string-refs
22078
21763
  // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
22079
21764
  this.refs.input.focus();
22080
21765
  };
21766
+ // TODO(LEMS-2656): remove TS suppression
21767
+ // @ts-expect-error: Type 'FocusPath' is not assignable to type 'Path'.
22081
21768
  this.blurInputPath = inputPath => {
22082
21769
  var _this$refs$input;
22083
21770
  // eslint-disable-next-line react/no-string-refs
@@ -22095,6 +21782,8 @@ class InputNumber extends React.Component {
22095
21782
  /* c8 ignore next */
22096
21783
  return [[]];
22097
21784
  };
21785
+ // TODO(LEMS-2656): remove TS suppression
21786
+ // @ts-expect-error: Type 'FocusPath' is not assignable to type 'Path'.
22098
21787
  this.setInputValue = (path, newValue, cb) => {
22099
21788
  this.props.onChange({
22100
21789
  currentValue: newValue
@@ -22222,6 +21911,8 @@ var InputNumber$1 = {
22222
21911
  widget: InputNumber,
22223
21912
  transform: propTransform$3,
22224
21913
  isLintable: true,
21914
+ // TODO(LEMS-2656): remove TS suppression
21915
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInputNumberUserInput'.
22225
21916
  scorer: scoreInputNumber,
22226
21917
  getOneCorrectAnswerFromRubric(rubric) {
22227
21918
  if (rubric.value == null) {
@@ -22512,7 +22203,7 @@ class NumericInput extends React.Component {
22512
22203
  height: "auto"
22513
22204
  }
22514
22205
  });
22515
- return /*#__PURE__*/React.createElement(InputWithExamples, {
22206
+ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(InputWithExamples, {
22516
22207
  ref: ref => this.inputRef = ref,
22517
22208
  value: this.props.currentValue,
22518
22209
  onChange: this.handleChange,
@@ -22524,7 +22215,7 @@ class NumericInput extends React.Component {
22524
22215
  id: this.props.widgetId,
22525
22216
  disabled: this.props.apiOptions.readOnly,
22526
22217
  style: styles.input
22527
- });
22218
+ }));
22528
22219
  }
22529
22220
  }
22530
22221
 
@@ -22593,69 +22284,19 @@ const propsTransform = function propsTransform(widgetOptions) {
22593
22284
  });
22594
22285
  return rendererProps;
22595
22286
  };
22596
-
22597
- // This function is being used to replace the input-number widget
22598
- // with the numeric-input widget
22599
- const propUpgrades$2 = {
22600
- /* c8 ignore next */
22601
- "1": initialProps => {
22602
- // If the initialProps has simplify, it means we're upgrading from
22603
- // input-number to numeric-input. In this case, we need to upgrade
22604
- // the widget options accordingly.
22605
- if (initialProps.simplify !== undefined) {
22606
- // If the answerType is not number or percent, we need to provide
22607
- // the answer form for the numeric-input widget
22608
- const provideAnswerForm = initialProps.answerType !== "number" && initialProps.answerType !== "percent";
22609
-
22610
- // We need to determine the mathFormat for the numeric-input widget
22611
- const mathFormat = initialProps.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
22612
- : initialProps.answerType; // Otherwise, we can use the answerType directly
22613
-
22614
- // If adjusting this logic, also adjust the logic in the convertInputNumberWidgetOptions
22615
- // function in input-number.ts in the Perseus Editor package's util folder
22616
- const answers = [{
22617
- value: initialProps.value,
22618
- simplify: initialProps.simplify,
22619
- answerForms: provideAnswerForm ? [mathFormat] : undefined,
22620
- strict: initialProps.inexact,
22621
- // We only want to set maxError if the inexact prop is true
22622
- maxError: initialProps.inexact ? initialProps.maxError : 0,
22623
- status: "correct",
22624
- // Input-number only allows correct answers
22625
- message: ""
22626
- }];
22627
- return {
22628
- answers,
22629
- size: initialProps.size,
22630
- coefficient: false,
22631
- // input-number doesn't have a coefficient prop
22632
- labelText: "",
22633
- // input-number doesn't have a labelText prop
22634
- static: false,
22635
- // static is always false for numeric-input
22636
- rightAlign: initialProps.rightAlign || false
22637
- };
22638
- } else {
22639
- // Otherwise simply return the initialProps as there's no differences
22640
- // between v0 and v1 for numeric-input
22641
- return initialProps;
22642
- }
22643
- }
22644
- };
22645
22287
  var NumericInput$1 = {
22646
22288
  name: "numeric-input",
22647
22289
  displayName: "Numeric input",
22648
22290
  defaultAlignment: "inline-block",
22649
22291
  accessible: true,
22650
22292
  widget: NumericInput,
22651
- version: {
22652
- major: 1,
22653
- minor: 0
22654
- },
22655
22293
  transform: propsTransform,
22656
- propUpgrades: propUpgrades$2,
22657
22294
  isLintable: true,
22295
+ // TODO(LEMS-2656): remove TS suppression
22296
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumericInputUserInput'.
22658
22297
  scorer: scoreNumericInput,
22298
+ // TODO(LEMS-2656): remove TS suppression
22299
+ // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusNumericInputRubric'
22659
22300
  getOneCorrectAnswerFromRubric(rubric) {
22660
22301
  const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
22661
22302
  const answerStrings = correctAnswers.map(answer => {
@@ -24166,12 +23807,12 @@ const AltText = ({
24166
23807
  }));
24167
23808
  };
24168
23809
  const rules = {
24169
- newline: SimpleMarkdown$1.defaultRules.newline,
24170
- paragraph: SimpleMarkdown$1.defaultRules.paragraph,
24171
- escape: SimpleMarkdown$1.defaultRules.escape,
23810
+ newline: SimpleMarkdown.defaultRules.newline,
23811
+ paragraph: SimpleMarkdown.defaultRules.paragraph,
23812
+ escape: SimpleMarkdown.defaultRules.escape,
24172
23813
  passageFootnote: {
24173
- order: SimpleMarkdown$1.defaultRules.escape.order + 0.1,
24174
- match: SimpleMarkdown$1.inlineRegex(/^\^/),
23814
+ order: SimpleMarkdown.defaultRules.escape.order + 0.1,
23815
+ match: SimpleMarkdown.inlineRegex(/^\^/),
24175
23816
  parse: (capture, parse, state) => {
24176
23817
  // if no footnotes have been seen, we're id 1. otherwise,
24177
23818
  // we're the next subsequent id
@@ -24200,7 +23841,7 @@ const rules = {
24200
23841
  }
24201
23842
  },
24202
23843
  refStart: {
24203
- order: SimpleMarkdown$1.defaultRules.escape.order + 0.2,
23844
+ order: SimpleMarkdown.defaultRules.escape.order + 0.2,
24204
23845
  match: function (source) {
24205
23846
  const capture = /^\{\{/.exec(source);
24206
23847
  if (capture) {
@@ -24286,8 +23927,8 @@ const rules = {
24286
23927
  }
24287
23928
  },
24288
23929
  refEnd: {
24289
- order: SimpleMarkdown$1.defaultRules.escape.order + 0.3,
24290
- match: SimpleMarkdown$1.inlineRegex(/^\}\}/),
23930
+ order: SimpleMarkdown.defaultRules.escape.order + 0.3,
23931
+ match: SimpleMarkdown.inlineRegex(/^\}\}/),
24291
23932
  parse: (capture, parse, state) => {
24292
23933
  if (!state.useRefs) {
24293
23934
  return {
@@ -24315,8 +23956,8 @@ const rules = {
24315
23956
  }
24316
23957
  },
24317
23958
  squareLabel: {
24318
- order: SimpleMarkdown$1.defaultRules.escape.order + 0.4,
24319
- match: SimpleMarkdown$1.inlineRegex(/^\[\[(\w+)\]\]( *)/),
23959
+ order: SimpleMarkdown.defaultRules.escape.order + 0.4,
23960
+ match: SimpleMarkdown.inlineRegex(/^\[\[(\w+)\]\]( *)/),
24320
23961
  parse: (capture, parse, state) => {
24321
23962
  if (!state.firstQuestionRef) {
24322
23963
  state.firstQuestionRef = capture[1];
@@ -24342,8 +23983,8 @@ const rules = {
24342
23983
  }
24343
23984
  },
24344
23985
  circleLabel: {
24345
- order: SimpleMarkdown$1.defaultRules.escape.order + 0.5,
24346
- match: SimpleMarkdown$1.inlineRegex(/^\(\((\w+)\)\)( *)/),
23986
+ order: SimpleMarkdown.defaultRules.escape.order + 0.5,
23987
+ match: SimpleMarkdown.inlineRegex(/^\(\((\w+)\)\)( *)/),
24347
23988
  parse: (capture, parse, state) => {
24348
23989
  return {
24349
23990
  content: capture[1],
@@ -24367,8 +24008,8 @@ const rules = {
24367
24008
  }
24368
24009
  },
24369
24010
  squareBracketRef: {
24370
- order: SimpleMarkdown$1.defaultRules.escape.order + 0.6,
24371
- match: SimpleMarkdown$1.inlineRegex(/^\[(\d+)\]( *)/),
24011
+ order: SimpleMarkdown.defaultRules.escape.order + 0.6,
24012
+ match: SimpleMarkdown.inlineRegex(/^\[(\d+)\]( *)/),
24372
24013
  parse: (capture, parse, state) => {
24373
24014
  if (!state.firstSentenceRef) {
24374
24015
  state.firstSentenceRef = capture[1];
@@ -24391,8 +24032,8 @@ const rules = {
24391
24032
  }
24392
24033
  },
24393
24034
  highlight: {
24394
- order: SimpleMarkdown$1.defaultRules.escape.order + 0.7,
24395
- match: SimpleMarkdown$1.inlineRegex(/^{highlighting.start}(.+?){highlighting.end}/),
24035
+ order: SimpleMarkdown.defaultRules.escape.order + 0.7,
24036
+ match: SimpleMarkdown.inlineRegex(/^{highlighting.start}(.+?){highlighting.end}/),
24396
24037
  parse: (capture, parse, state) => {
24397
24038
  return {
24398
24039
  content: capture[1]
@@ -24406,8 +24047,8 @@ const rules = {
24406
24047
  }
24407
24048
  },
24408
24049
  reviewHighlight: {
24409
- order: SimpleMarkdown$1.defaultRules.escape.order + 0.7,
24410
- match: SimpleMarkdown$1.inlineRegex(/^{review-highlighting.start}(.+?){review-highlighting.end}/),
24050
+ order: SimpleMarkdown.defaultRules.escape.order + 0.7,
24051
+ match: SimpleMarkdown.inlineRegex(/^{review-highlighting.start}(.+?){review-highlighting.end}/),
24411
24052
  parse: (capture, parse, state) => {
24412
24053
  return {
24413
24054
  content: capture[1]
@@ -24420,11 +24061,11 @@ const rules = {
24420
24061
  }, node.content)];
24421
24062
  }
24422
24063
  },
24423
- strong: SimpleMarkdown$1.defaultRules.strong,
24424
- u: SimpleMarkdown$1.defaultRules.u,
24425
- em: SimpleMarkdown$1.defaultRules.em,
24426
- del: SimpleMarkdown$1.defaultRules.del,
24427
- text: _extends({}, SimpleMarkdown$1.defaultRules.text, {
24064
+ strong: SimpleMarkdown.defaultRules.strong,
24065
+ u: SimpleMarkdown.defaultRules.u,
24066
+ em: SimpleMarkdown.defaultRules.em,
24067
+ del: SimpleMarkdown.defaultRules.del,
24068
+ text: _extends({}, SimpleMarkdown.defaultRules.text, {
24428
24069
  react(node, output, state) {
24429
24070
  return /*#__PURE__*/React.createElement("span", {
24430
24071
  key: state.key
@@ -24465,7 +24106,10 @@ const CIRCLE_LABEL_STYLE = {
24465
24106
  borderRadius: "50%",
24466
24107
  textAlign: "center"
24467
24108
  };
24468
- const builtParser = SimpleMarkdown$1.parserFor(rules);
24109
+
24110
+ // TODO(LEMS-2656): remove TS suppression
24111
+ // @ts-expect-error: State is not assignable to ParseState
24112
+ const builtParser = SimpleMarkdown.parserFor(rules);
24469
24113
  const parse$2 = (source, state) => {
24470
24114
  // @ts-expect-error - TS2322 - Type 'ParseState | {}' is not assignable to type 'ParseState | null | undefined'.
24471
24115
  state = state || {};
@@ -24474,7 +24118,7 @@ const parse$2 = (source, state) => {
24474
24118
  };
24475
24119
 
24476
24120
  // @ts-expect-error - TS2322 - Type 'ReactOutput' is not assignable to type '(arg1: SingleASTNode[]) => ReactElement<any, string | JSXElementConstructor<any>>'.
24477
- const output = SimpleMarkdown$1.reactFor(SimpleMarkdown$1.ruleOutput(rules, "react"));
24121
+ const output = SimpleMarkdown.reactFor(SimpleMarkdown.ruleOutput(rules, "react"));
24478
24122
  var PassageMarkdown = {
24479
24123
  parse: parse$2,
24480
24124
  output,
@@ -26897,6 +26541,8 @@ var Radio = {
26897
26541
  },
26898
26542
  propUpgrades: propUpgrades$1,
26899
26543
  isLintable: true,
26544
+ // TODO(LEMS-2656): remove TS suppression
26545
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput
26900
26546
  scorer: scoreRadio
26901
26547
  };
26902
26548
 
@@ -27117,6 +26763,8 @@ var Categorizer$1 = {
27117
26763
  return _.pick(editorProps, "items", "categories", "values", "randomizeItems");
27118
26764
  },
27119
26765
  isLintable: true,
26766
+ // TODO(LEMS-2656): remove TS suppression
26767
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
27120
26768
  scorer: scoreCategorizer
27121
26769
  };
27122
26770
 
@@ -27350,6 +26998,8 @@ var CSProgram$1 = {
27350
26998
  supportedAlignments: ["block", "full-width"],
27351
26999
  widget: CSProgram,
27352
27000
  hidden: true,
27001
+ // TODO(LEMS-2656): remove TS suppression
27002
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
27353
27003
  scorer: scoreCSProgram
27354
27004
  };
27355
27005
 
@@ -27567,6 +27217,8 @@ var Dropdown$1 = {
27567
27217
  accessible: true,
27568
27218
  widget: Dropdown,
27569
27219
  transform: optionsTransform,
27220
+ // TODO(LEMS-2656): remove TS suppression
27221
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'.
27570
27222
  scorer: scoreDropdown
27571
27223
  };
27572
27224
 
@@ -29129,6 +28781,8 @@ class FunctionGrapher extends React.Component {
29129
28781
  return this.props.asymptote;
29130
28782
  };
29131
28783
  this.change = (...args) => {
28784
+ // TODO(LEMS-2656): remove TS suppression
28785
+ // @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.
29132
28786
  return change.apply(this, args);
29133
28787
  };
29134
28788
  this.renderPlot = () => {
@@ -29492,6 +29146,8 @@ var Grapher$1 = {
29492
29146
  widget: Grapher,
29493
29147
  transform: propTransform$2,
29494
29148
  staticTransform: staticTransform$4,
29149
+ // TODO(LEMS-2656): remove TS suppression
29150
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGrapherUserInput'.
29495
29151
  scorer: scoreGrapher
29496
29152
  };
29497
29153
 
@@ -29613,9 +29269,6 @@ class Group extends React.Component {
29613
29269
  this.change("widgets", this.rendererRef.props.widgets);
29614
29270
  }
29615
29271
  };
29616
-
29617
- // TODO(LEMS-2391): replace this when there's a separate check
29618
- // for valid/invalid state
29619
29272
  const score = (_this$rendererRef12 = this.rendererRef) == null ? void 0 : _this$rendererRef12.score();
29620
29273
  const isValid = score && score.type !== "invalid";
29621
29274
  const isInvalid = score && score.type === "invalid";
@@ -29823,6 +29476,8 @@ var Iframe$1 = {
29823
29476
  widget: Iframe,
29824
29477
  // Let's not expose it to all content creators yet
29825
29478
  hidden: true,
29479
+ // TODO(LEMS-2656): remove TS suppression
29480
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusIframeUserInput'.
29826
29481
  scorer: scoreIframe
29827
29482
  };
29828
29483
 
@@ -31474,6 +31129,37 @@ function isUnlimitedGraphState(state) {
31474
31129
  return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
31475
31130
  }
31476
31131
 
31132
+ /**
31133
+ * Parse a string of text and math into a list of objects with type and content
31134
+ *
31135
+ * Example: "Pi is about $\frac{22}{7}$" ==>
31136
+ * [
31137
+ * {type: "text", content: "Pi is about "},
31138
+ * {type: "math", content: "\\frac{22}{7}"},
31139
+ * ]
31140
+ */
31141
+ const mathOnlyParser = SimpleMarkdown.parserFor({
31142
+ math: _extends({}, pureMarkdownRules.math, {
31143
+ order: 0
31144
+ }),
31145
+ text: {
31146
+ order: 1,
31147
+ match: SimpleMarkdown.anyScopeRegex(/^([^$\\{}]+)/),
31148
+ parse: capture => ({
31149
+ content: capture[0]
31150
+ })
31151
+ },
31152
+ specialCharacter: {
31153
+ order: 2,
31154
+ match: SimpleMarkdown.anyScopeRegex(/^(\\[\S\s]|\$|\\$|{|})/),
31155
+ parse: capture => ({
31156
+ content: capture[0]
31157
+ })
31158
+ }
31159
+ }, {
31160
+ inline: true
31161
+ });
31162
+
31477
31163
  /**
31478
31164
  * Replace all text outside of the $ TeX blocks with `\\text{...}`
31479
31165
  * This way, the entire resulting string can be rendered within <TeX>
@@ -31482,57 +31168,19 @@ function isUnlimitedGraphState(state) {
31482
31168
  function replaceOutsideTeX(mathString) {
31483
31169
  // All the information we need is in the first section,
31484
31170
  // whether it's typed as "blockmath" or "paragraph"
31485
- const firstSection = SimpleMarkdown.parse(mathString)[0];
31486
-
31487
- // If it's blockMath, the outer level has the full math content.
31488
- if (firstSection.type === "blockMath") {
31489
- return firstSection.content;
31490
- }
31491
-
31492
- // If it's a paragraph, we need to iterate through the sections
31493
- // to look for individual math blocks.
31494
- const condensedNodes = condenseTextNodes(firstSection.content);
31171
+ const parsed = mathOnlyParser(mathString);
31495
31172
  let result = "";
31496
- for (const piece of condensedNodes) {
31497
- piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
31173
+ for (const piece of parsed) {
31174
+ piece.type === "math" ? result += "$" + piece.content + "$" : piece.type === "specialCharacter" ? result += escapeIfUnescaped(piece.content) : result += piece.content;
31498
31175
  }
31499
- return result;
31176
+ return `\\text{${result}}`;
31500
31177
  }
31501
- // Helper function for replaceOutsideTeX()
31502
- // Condense adjacent text nodes into a single text node
31503
- function condenseTextNodes(nodes) {
31504
- const result = [];
31505
- if (!nodes) {
31506
- return result;
31507
- }
31508
- let currentText = "";
31509
- for (const node of nodes) {
31510
- if (node.type === "math") {
31511
- if (currentText) {
31512
- result.push({
31513
- type: "text",
31514
- content: currentText
31515
- });
31516
- currentText = "";
31517
- }
31518
- result.push(node);
31519
- } else {
31520
- currentText += node.content;
31521
- }
31522
- }
31523
- if (currentText) {
31524
- result.push({
31525
- type: "text",
31526
- content: currentText
31527
- });
31178
+ function escapeIfUnescaped(character) {
31179
+ if (character.length === 1) {
31180
+ return "\\" + character;
31181
+ } else {
31182
+ return character;
31528
31183
  }
31529
- return result;
31530
- }
31531
-
31532
- // Helper function for replaceOutsideTeX()
31533
- function escapeSpecialChars(str) {
31534
- // Escape $, \, {, and } characters
31535
- return str.replace(/([$\\{}])/g, "\\$1");
31536
31184
  }
31537
31185
 
31538
31186
  function AxisLabels() {
@@ -32477,12 +32125,17 @@ const ItemExtras = [
32477
32125
  // prettier-ignore
32478
32126
 
32479
32127
  // prettier-ignore
32480
- // While this widget is deprecated, we still need the type for conversion purposes
32128
+
32481
32129
  // prettier-ignore
32130
+
32482
32131
  // prettier-ignore
32132
+
32483
32133
  // prettier-ignore
32134
+
32484
32135
  //prettier-ignore
32136
+
32485
32137
  // A background image applied to various widgets.
32138
+
32486
32139
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
32487
32140
 
32488
32141
  // 2D range: xMin, xMax, yMin, yMax
@@ -32992,7 +32645,9 @@ const actions = {
32992
32645
  removePoint,
32993
32646
  focusPoint,
32994
32647
  blurPoint,
32995
- clickPoint
32648
+ clickPoint,
32649
+ closePolygon,
32650
+ openPolygon
32996
32651
  },
32997
32652
  quadratic: {
32998
32653
  movePoint
@@ -33071,6 +32726,18 @@ function changeKeyboardInvitationVisibility(shouldShow) {
33071
32726
  shouldShow
33072
32727
  };
33073
32728
  }
32729
+ const CLOSE_POLYGON = "close-polygon";
32730
+ function closePolygon() {
32731
+ return {
32732
+ type: CLOSE_POLYGON
32733
+ };
32734
+ }
32735
+ const OPEN_POLYGON = "open-polygon";
32736
+ function openPolygon() {
32737
+ return {
32738
+ type: OPEN_POLYGON
32739
+ };
32740
+ }
33074
32741
  const MOVE_ALL = "move-all";
33075
32742
  function moveAll(delta) {
33076
32743
  return {
@@ -34584,33 +34251,17 @@ const LimitedPolygonGraph = props => {
34584
34251
  }
34585
34252
  })));
34586
34253
  };
34587
-
34588
- // TODO(catjohnson): reduce redundancy between LimitedPolygonGraph and UnlimitedPolygonGraph
34589
- // both components are vary similar, however more implementation is needed to be added before
34590
- // it is clear what can and can't be shared between components.
34591
34254
  const UnlimitedPolygonGraph = props => {
34592
- const [hovered, setHovered] = React.useState(false);
34593
- // This is more so required for the re-rendering that occurs when state
34594
- // updates; specifically with regard to line weighting and polygon focus.
34595
- const [focusVisible, setFocusVisible] = React.useState(false);
34596
34255
  const {
34597
34256
  dispatch
34598
34257
  } = props;
34599
34258
  const {
34600
34259
  coords,
34601
- showAngles,
34602
- showSides,
34603
- range,
34604
- snapStep,
34605
- snapTo = "grid"
34260
+ closedPolygon
34606
34261
  } = props.graphState;
34607
34262
  const graphConfig = useGraphConfig();
34608
-
34609
- // TODO(catjohnson): Explore abstracting this code as it is similar to point.tsx
34610
- // and hopefully we can cut down ont the unlimited graph redundancy.
34611
34263
  const {
34612
34264
  range: [x, y],
34613
- disableKeyboardInteraction,
34614
34265
  graphDimensionsInPixels
34615
34266
  } = graphConfig;
34616
34267
  const widthPx = graphDimensionsInPixels[0];
@@ -34621,21 +34272,6 @@ const UnlimitedPolygonGraph = props => {
34621
34272
  // TODO(benchristel): can the default set of points be removed here? I don't
34622
34273
  // think coords can be null.
34623
34274
  const points = coords != null ? coords : [[0, 0]];
34624
- const polygonRef = React.useRef(null);
34625
- const dragReferencePoint = points[0];
34626
- const constrain = ["angles", "sides"].includes(snapTo) ? p => p : p => snap(snapStep, p);
34627
- const {
34628
- dragging
34629
- } = useDraggable({
34630
- gestureTarget: polygonRef,
34631
- point: dragReferencePoint,
34632
- onMove: newPoint => {
34633
- const delta = vec.sub(newPoint, dragReferencePoint);
34634
- dispatch(actions.polygon.moveAll(delta));
34635
- },
34636
- constrainKeyboardMovement: constrain
34637
- });
34638
- const lines = getLines(points);
34639
34275
  React.useEffect(() => {
34640
34276
  const focusedIndex = props.graphState.focusedPointIndex;
34641
34277
  if (focusedIndex != null) {
@@ -34643,94 +34279,59 @@ const UnlimitedPolygonGraph = props => {
34643
34279
  (_pointRef$current$foc = pointRef.current[focusedIndex]) == null || _pointRef$current$foc.focus();
34644
34280
  }
34645
34281
  }, [props.graphState.focusedPointIndex, pointRef]);
34646
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
34647
- style: {
34648
- fill: "rgba(0,0,0,0)",
34649
- cursor: "crosshair"
34650
- },
34651
- width: widthPx,
34652
- height: heightPx,
34653
- x: left,
34654
- y: top,
34655
- onClick: event => {
34656
- const elementRect = event.currentTarget.getBoundingClientRect();
34657
- const x = event.clientX - elementRect.x;
34658
- const y = event.clientY - elementRect.y;
34659
- const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
34660
- dispatch(actions.polygon.addPoint(graphCoordinates[0]));
34661
- }
34662
- }), /*#__PURE__*/React.createElement(Polygon, {
34663
- points: [...points],
34664
- color: "var(--movable-line-stroke-color)",
34665
- svgPolygonProps: {
34666
- strokeWidth: focusVisible ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)",
34667
- style: {
34668
- fill: "transparent"
34669
- }
34670
- }
34671
- }), props.graphState.coords.map((point, i) => {
34672
- const pt1 = points.at(i - 1);
34673
- const pt2 = points[(i + 1) % points.length];
34674
- if (!pt1 || !pt2) {
34675
- return null;
34676
- }
34677
- return /*#__PURE__*/React.createElement(PolygonAngle, {
34678
- key: "angle-" + i,
34679
- centerPoint: point,
34680
- endPoints: [pt1, pt2],
34681
- range: range,
34682
- polygonLines: lines,
34683
- showAngles: !!showAngles,
34684
- snapTo: snapTo
34282
+ if (closedPolygon) {
34283
+ const closedPolygonProps = _extends({}, props, {
34284
+ numSides: coords.length
34685
34285
  });
34686
- }), showSides && lines.map(([start, end], i) => {
34687
- const [x, y] = vec.midpoint(start, end);
34688
- const length = parseFloat(vec.dist(start, end).toFixed(snapTo === "sides" ? 0 : 1));
34689
- return /*#__PURE__*/React.createElement(TextLabel, {
34690
- key: "side-" + i,
34691
- x: x,
34692
- y: y
34693
- }, !Number.isInteger(length) && "≈ ", length);
34694
- }), /*#__PURE__*/React.createElement(Polygon, {
34695
- points: [...points],
34696
- color: "transparent",
34697
- svgPolygonProps: {
34698
- ref: polygonRef,
34699
- tabIndex: disableKeyboardInteraction ? -1 : 0,
34700
- strokeWidth: TARGET_SIZE,
34286
+ return /*#__PURE__*/React.createElement(LimitedPolygonGraph, closedPolygonProps);
34287
+ } else {
34288
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
34701
34289
  style: {
34702
- cursor: dragging ? "grabbing" : "grab",
34703
- fill: hovered ? "var(--mafs-blue)" : "transparent"
34290
+ fill: "rgba(0,0,0,0)",
34291
+ cursor: "crosshair"
34704
34292
  },
34705
- onMouseEnter: () => setHovered(true),
34706
- onMouseLeave: () => setHovered(false),
34707
- // Required to remove line weighting when user clicks away
34708
- // from the focused polygon
34709
- onKeyDownCapture: () => {
34710
- setFocusVisible(hasFocusVisible(polygonRef.current));
34293
+ width: widthPx,
34294
+ height: heightPx,
34295
+ x: left,
34296
+ y: top,
34297
+ onClick: event => {
34298
+ const elementRect = event.currentTarget.getBoundingClientRect();
34299
+ const x = event.clientX - elementRect.x;
34300
+ const y = event.clientY - elementRect.y;
34301
+ const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
34302
+ dispatch(actions.polygon.addPoint(graphCoordinates[0]));
34303
+ }
34304
+ }), /*#__PURE__*/React.createElement(Polyline, {
34305
+ points: [...points],
34306
+ color: "var(--movable-line-stroke-color)",
34307
+ svgPolylineProps: {
34308
+ strokeWidth: "var(--movable-line-stroke-weight)",
34309
+ style: {
34310
+ fill: "transparent"
34311
+ }
34312
+ }
34313
+ }), props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34314
+ key: i,
34315
+ point: point,
34316
+ sequenceNumber: i + 1,
34317
+ onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
34318
+ ref: ref => {
34319
+ pointRef.current[i] = ref;
34711
34320
  },
34712
- // Required for lines to darken on focus
34713
- onFocus: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
34714
- // Required for line weighting to update on blur. Without this,
34715
- // the user has to hover over the shape for it to update
34716
- onBlur: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
34717
- className: "movable-polygon"
34718
- }
34719
- }), props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34720
- key: i,
34721
- point: point,
34722
- sequenceNumber: i + 1,
34723
- onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
34724
- ref: ref => {
34725
- pointRef.current[i] = ref;
34726
- },
34727
- onFocus: () => {
34728
- dispatch(actions.pointGraph.focusPoint(i));
34729
- },
34730
- onClick: () => {
34731
- dispatch(actions.pointGraph.clickPoint(i));
34732
- }
34733
- })));
34321
+ onFocus: () => {
34322
+ dispatch(actions.polygon.focusPoint(i));
34323
+ },
34324
+ onClick: () => {
34325
+ // If the point being clicked is the first point and
34326
+ // there's enough points to form a polygon (3 or more)
34327
+ // Close the shape before setting focus.
34328
+ if (i === 0 && props.graphState.coords.length >= 3) {
34329
+ dispatch(actions.polygon.closePolygon());
34330
+ }
34331
+ dispatch(actions.polygon.clickPoint(i));
34332
+ }
34333
+ })));
34334
+ }
34734
34335
  };
34735
34336
  function getLines(points) {
34736
34337
  return points.map((point, i) => {
@@ -35130,6 +34731,7 @@ const MafsGraph = props => {
35130
34731
  const uniqueId = React.useId();
35131
34732
  const descriptionId = `interactive-graph-description-${uniqueId}`;
35132
34733
  const interactiveElementsDescriptionId = `interactive-graph-interactive-elements-description-${uniqueId}`;
34734
+ const unlimitedGraphKeyboardPromptId = `unlimited-graph-keyboard-prompt-${uniqueId}`;
35133
34735
  const graphRef = React.useRef(null);
35134
34736
  const {
35135
34737
  analytics
@@ -35202,7 +34804,7 @@ const MafsGraph = props => {
35202
34804
  handleKeyboardEvent(event, state, dispatch);
35203
34805
  },
35204
34806
  "aria-label": fullGraphAriaLabel,
35205
- "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId),
34807
+ "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && "unlimited-graph-keyboard-prompt"),
35206
34808
  ref: graphRef,
35207
34809
  tabIndex: 0,
35208
34810
  onFocus: event => {
@@ -35272,6 +34874,7 @@ const MafsGraph = props => {
35272
34874
  height: height
35273
34875
  }, /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, props.showProtractor && /*#__PURE__*/React.createElement(Protractor, null), graph)))), interactionPrompt && /*#__PURE__*/React.createElement(View, {
35274
34876
  style: {
34877
+ display: interactionPrompt ? undefined : "hidden",
35275
34878
  textAlign: "center",
35276
34879
  backgroundColor: "white",
35277
34880
  border: "1px solid #21242C52",
@@ -35283,7 +34886,9 @@ const MafsGraph = props => {
35283
34886
  top: "50%",
35284
34887
  transform: "translateY(-50%)"
35285
34888
  }
35286
- }, /*#__PURE__*/React.createElement(LabelMedium, null, strings.graphKeyboardPrompt))), renderGraphControls({
34889
+ }, /*#__PURE__*/React.createElement(LabelMedium, {
34890
+ id: unlimitedGraphKeyboardPromptId
34891
+ }, strings.graphKeyboardPrompt))), renderGraphControls({
35287
34892
  state,
35288
34893
  dispatch,
35289
34894
  width,
@@ -35333,23 +34938,51 @@ const renderPointGraphControls = props => {
35333
34938
  }
35334
34939
  }, perseusStrings.removePoint));
35335
34940
  };
35336
-
35337
- /**
35338
- * TODO(catjohnson): The polygon controls will be vastly different from the point controls.
35339
- * Will be keeping this function as is for the time being, even if it's a copy of the point graph
35340
- * controls.
35341
- */
35342
34941
  const renderPolygonGraphControls = props => {
35343
34942
  const {
35344
34943
  interactionMode,
35345
34944
  showRemovePointButton,
35346
- focusedPointIndex
34945
+ focusedPointIndex,
34946
+ closedPolygon,
34947
+ coords
35347
34948
  } = props.state;
35348
34949
  const {
35349
34950
  perseusStrings
35350
34951
  } = props;
35351
34952
  const shouldShowRemoveButton = showRemovePointButton && focusedPointIndex !== null;
35352
- return /*#__PURE__*/React.createElement(View, {
34953
+
34954
+ // We want to disable the closePolygon button when
34955
+ // there are not enough coords to make a polygon
34956
+ const disableCloseButton = coords.length < 3;
34957
+
34958
+ // If polygon is closed, show open button.
34959
+ // If polygon is open, show close button.
34960
+ const polygonButton = closedPolygon ? /*#__PURE__*/React.createElement(Button, {
34961
+ kind: "secondary",
34962
+ style: {
34963
+ width: "100%",
34964
+ marginLeft: "20px"
34965
+ },
34966
+ tabIndex: 0,
34967
+ onClick: () => {
34968
+ props.dispatch(actions.polygon.openPolygon());
34969
+ }
34970
+ }, perseusStrings.openPolygon) : /*#__PURE__*/React.createElement(Button, {
34971
+ kind: "secondary"
34972
+ // Conditional disable when there are less than 3 points in
34973
+ // the graph
34974
+ ,
34975
+ disabled: disableCloseButton,
34976
+ style: {
34977
+ width: "100%",
34978
+ marginLeft: "20px"
34979
+ },
34980
+ tabIndex: disableCloseButton ? -1 : 0,
34981
+ onClick: () => {
34982
+ props.dispatch(actions.polygon.closePolygon());
34983
+ }
34984
+ }, perseusStrings.closePolygon);
34985
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(View, {
35353
34986
  style: {
35354
34987
  flexDirection: "row",
35355
34988
  width: props.width
@@ -35359,8 +34992,13 @@ const renderPolygonGraphControls = props => {
35359
34992
  style: {
35360
34993
  width: "100%",
35361
34994
  marginLeft: "20px"
35362
- },
35363
- tabIndex: 0,
34995
+ }
34996
+ // Disable button when polygon is closed.
34997
+ ,
34998
+ disabled: closedPolygon
34999
+ // Do not make the button tabbable when it is disabled.
35000
+ ,
35001
+ tabIndex: closedPolygon ? -1 : 0,
35364
35002
  onClick: () => {
35365
35003
  props.dispatch(actions.polygon.addPoint([0, 0]));
35366
35004
  }
@@ -35368,19 +35006,21 @@ const renderPolygonGraphControls = props => {
35368
35006
  id: REMOVE_BUTTON_ID,
35369
35007
  kind: "secondary",
35370
35008
  color: "destructive"
35009
+ // Disable button when polygon is closed.
35010
+ ,
35011
+ disabled: closedPolygon || !shouldShowRemoveButton
35371
35012
  // This button is meant to be interacted with by the mouse only
35372
35013
  // Never allow learners to tab to this button
35373
35014
  ,
35374
35015
  tabIndex: -1,
35375
35016
  style: {
35376
35017
  width: "100%",
35377
- marginLeft: "20px",
35378
- visibility: shouldShowRemoveButton ? "visible" : "hidden"
35018
+ marginLeft: "20px"
35379
35019
  },
35380
35020
  onClick: _event => {
35381
35021
  props.dispatch(actions.polygon.removePoint(props.state.focusedPointIndex));
35382
35022
  }
35383
- }, perseusStrings.removePoint));
35023
+ }, perseusStrings.removePoint), polygonButton));
35384
35024
  };
35385
35025
  const renderGraphControls = props => {
35386
35026
  const {
@@ -35431,7 +35071,7 @@ function handleBlurEvent(_event, state, dispatch) {
35431
35071
  }
35432
35072
  function handleKeyboardEvent(event, state, dispatch) {
35433
35073
  if (isUnlimitedGraphState(state)) {
35434
- if (event.key === "Backspace") {
35074
+ if (event.key === "Backspace" || event.key === "Delete") {
35435
35075
  var _document$activeEleme;
35436
35076
  // TODO(benchristel): Checking classList here is a hack to prevent
35437
35077
  // points from being deleted if the user presses the backspace key
@@ -35439,7 +35079,10 @@ function handleKeyboardEvent(event, state, dispatch) {
35439
35079
  // should move the keyboard event handler to the movable point
35440
35080
  // handle element.
35441
35081
  if ((_document$activeEleme = document.activeElement) != null && _document$activeEleme.classList.contains("movable-point__focusable-handle")) {
35442
- dispatch(actions.global.deleteIntent());
35082
+ // Only allow delete if type is point or a polygon that is open.
35083
+ if (state.type === "point" || state.type === "polygon" && !state.closedPolygon) {
35084
+ dispatch(actions.global.deleteIntent());
35085
+ }
35443
35086
  }
35444
35087
 
35445
35088
  // After removing a point blur
@@ -35670,7 +35313,8 @@ function initializeGraphState(params) {
35670
35313
  focusedPointIndex: null,
35671
35314
  showRemovePointButton: false,
35672
35315
  interactionMode: "mouse",
35673
- showKeyboardInteractionInvitation: false
35316
+ showKeyboardInteractionInvitation: false,
35317
+ closedPolygon: false
35674
35318
  });
35675
35319
  case "point":
35676
35320
  return _extends({}, shared, {
@@ -35950,6 +35594,10 @@ function interactiveGraphReducer(state, action) {
35950
35594
  return doDeleteIntent(state);
35951
35595
  case CLICK_POINT:
35952
35596
  return doClickPoint(state, action);
35597
+ case CLOSE_POLYGON:
35598
+ return doClosePolygon(state);
35599
+ case OPEN_POLYGON:
35600
+ return doOpenPolygon(state);
35953
35601
  case CHANGE_INTERACTION_MODE:
35954
35602
  return doChangeInteractionMode(state, action);
35955
35603
  case CHANGE_KEYBOARD_INVITATION_VISIBILITY:
@@ -36003,6 +35651,22 @@ function doClickPoint(state, action) {
36003
35651
  }
36004
35652
  return state;
36005
35653
  }
35654
+ function doClosePolygon(state) {
35655
+ if (isUnlimitedGraphState(state) && state.type === "polygon") {
35656
+ return _extends({}, state, {
35657
+ closedPolygon: true
35658
+ });
35659
+ }
35660
+ return state;
35661
+ }
35662
+ function doOpenPolygon(state) {
35663
+ if (isUnlimitedGraphState(state) && state.type === "polygon") {
35664
+ return _extends({}, state, {
35665
+ closedPolygon: false
35666
+ });
35667
+ }
35668
+ return state;
35669
+ }
36006
35670
  function doChangeInteractionMode(state, action) {
36007
35671
  if (isUnlimitedGraphState(state)) {
36008
35672
  const nextKeyboardInvitation = action.mode === "keyboard" ? false : state.showKeyboardInteractionInvitation;
@@ -36201,8 +35865,12 @@ function doMovePoint(state, action) {
36201
35865
  newValue: newValue
36202
35866
  });
36203
35867
 
36204
- // Reject the move if it would cause the sides of the polygon to cross
36205
- if (polygonSidesIntersect(newCoords)) {
35868
+ // Boolean value to track whether we can let the polygon sides interact.
35869
+ // They can interact if it's an unlimited polygon that is open.
35870
+ const polygonSidesCanIntersect = state.numSides === "unlimited" && !state.closedPolygon;
35871
+
35872
+ // Reject the move if it would cause the sides of the polygon to cross.
35873
+ if (!polygonSidesCanIntersect && polygonSidesIntersect(newCoords)) {
36206
35874
  return state;
36207
35875
  }
36208
35876
  return _extends({}, state, {
@@ -36359,13 +36027,14 @@ function doAddPoint(state, action) {
36359
36027
  return state;
36360
36028
  }
36361
36029
  }
36030
+ const newCoords = [...state.coords, snappedPoint];
36362
36031
 
36363
36032
  // If there's no point in spot where we want the new point to go we add it there
36364
36033
  return _extends({}, state, {
36365
36034
  hasBeenInteractedWith: true,
36366
- coords: [...state.coords, snappedPoint],
36035
+ coords: newCoords,
36367
36036
  showRemovePointButton: false,
36368
- focusedPointIndex: state.coords.length
36037
+ focusedPointIndex: newCoords.length - 1
36369
36038
  });
36370
36039
  }
36371
36040
  function doRemovePoint(state, action) {
@@ -38676,6 +38345,8 @@ var InteractiveGraph$1 = {
38676
38345
  displayName: "Interactive graph (Assessments only)",
38677
38346
  widget: InteractiveGraph,
38678
38347
  staticTransform: staticTransform$3,
38348
+ // TODO(LEMS-2656): remove TS suppression
38349
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInteractiveGraphUserInput'.
38679
38350
  scorer: scoreInteractiveGraph
38680
38351
  };
38681
38352
 
@@ -39669,6 +39340,8 @@ var LabelImage$1 = {
39669
39340
  widget: LabelImageWithDependencies,
39670
39341
  accessible: true,
39671
39342
  isLintable: true,
39343
+ // TODO(LEMS-2656): remove TS suppression
39344
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusLabelImageUserInput'.
39672
39345
  scorer: scoreLabelImage
39673
39346
  };
39674
39347
 
@@ -40611,6 +40284,8 @@ var Matcher$1 = {
40611
40284
  displayName: "Matcher (two column)",
40612
40285
  widget: Matcher,
40613
40286
  isLintable: true,
40287
+ // TODO(LEMS-2656): remove TS suppression
40288
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatcherUserInput'.
40614
40289
  scorer: scoreMatcher
40615
40290
  };
40616
40291
 
@@ -40627,7 +40302,35 @@ const getPromptJSON$8 = (renderProps, userInput) => {
40627
40302
  };
40628
40303
  };
40629
40304
 
40305
+ /**
40306
+ * Checks user input from the matrix widget to see if it is scorable.
40307
+ *
40308
+ * Note: The matrix widget cannot do much validation without the Scoring
40309
+ * Data because of its use of KhanAnswerTypes as a core part of scoring.
40310
+ *
40311
+ * @see `scoreMatrix()` for more details.
40312
+ */
40313
+ function validateMatrix(userInput, validationData, strings) {
40314
+ const supplied = userInput.answers;
40315
+ const suppliedSize = getMatrixSize(supplied);
40316
+ for (let row = 0; row < suppliedSize[0]; row++) {
40317
+ for (let col = 0; col < suppliedSize[1]; col++) {
40318
+ if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
40319
+ return {
40320
+ type: "invalid",
40321
+ message: strings.fillAllCells
40322
+ };
40323
+ }
40324
+ }
40325
+ }
40326
+ return null;
40327
+ }
40328
+
40630
40329
  function scoreMatrix(userInput, rubric, strings) {
40330
+ const validationResult = validateMatrix(userInput, rubric, strings);
40331
+ if (validationResult != null) {
40332
+ return validationResult;
40333
+ }
40631
40334
  const solution = rubric.answers;
40632
40335
  const supplied = userInput.answers;
40633
40336
  const solutionSize = getMatrixSize(solution);
@@ -40635,13 +40338,9 @@ function scoreMatrix(userInput, rubric, strings) {
40635
40338
  const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
40636
40339
  const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
40637
40340
  let message = null;
40638
- let hasEmptyCell = false;
40639
40341
  let incorrect = false;
40640
40342
  _(suppliedSize[0]).times(row => {
40641
40343
  _(suppliedSize[1]).times(col => {
40642
- if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
40643
- hasEmptyCell = true;
40644
- }
40645
40344
  if (!incorrectSize) {
40646
40345
  const validator = createValidator(
40647
40346
  // @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
@@ -40659,12 +40358,6 @@ function scoreMatrix(userInput, rubric, strings) {
40659
40358
  }
40660
40359
  });
40661
40360
  });
40662
- if (hasEmptyCell) {
40663
- return {
40664
- type: "invalid",
40665
- message: strings.fillAllCells
40666
- };
40667
- }
40668
40361
  if (incorrectSize) {
40669
40362
  return {
40670
40363
  type: "points",
@@ -41093,6 +40786,8 @@ var Matrix$1 = {
41093
40786
  transform: propTransform$1,
41094
40787
  staticTransform: staticTransform$2,
41095
40788
  isLintable: true,
40789
+ // TODO(LEMS-2656): remove TS suppression
40790
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
41096
40791
  scorer: scoreMatrix
41097
40792
  };
41098
40793
 
@@ -42724,6 +42419,8 @@ var NumberLine$1 = {
42724
42419
  widget: NumberLine,
42725
42420
  transform: numberLineTransform,
42726
42421
  staticTransform: staticTransform$1,
42422
+ // TODO(LEMS-2656): remove TS suppression
42423
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
42727
42424
  scorer: scoreNumberLine
42728
42425
  };
42729
42426
 
@@ -43304,6 +43001,8 @@ var Orderer$1 = {
43304
43001
  hidden: true,
43305
43002
  widget: Orderer,
43306
43003
  isLintable: true,
43004
+ // TODO(LEMS-2656): remove TS suppression
43005
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
43307
43006
  scorer: scoreOrderer
43308
43007
  };
43309
43008
 
@@ -44390,6 +44089,8 @@ var Plotter$1 = {
44390
44089
  hidden: true,
44391
44090
  widget: Plotter,
44392
44091
  staticTransform: staticTransform,
44092
+ // TODO(LEMS-2656): remove TS suppression
44093
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
44393
44094
  scorer: scorePlotter
44394
44095
  };
44395
44096
 
@@ -44575,6 +44276,8 @@ var Sorter$1 = {
44575
44276
  displayName: "Sorter",
44576
44277
  widget: Sorter,
44577
44278
  isLintable: true,
44279
+ // TODO(LEMS-2656): remove TS suppression
44280
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
44578
44281
  scorer: scoreSorter
44579
44282
  };
44580
44283
 
@@ -44870,6 +44573,8 @@ var Table$1 = {
44870
44573
  transform: propTransform,
44871
44574
  hidden: true,
44872
44575
  isLintable: true,
44576
+ // TODO(LEMS-2656): remove TS suppression
44577
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
44873
44578
  scorer: scoreTable
44874
44579
  };
44875
44580
 
@@ -45053,7 +44758,7 @@ const init = function init() {
45053
44758
 
45054
44759
  // This file is processed by a Rollup plugin (replace) to inject the production
45055
44760
  const libName = "@khanacademy/perseus";
45056
- const libVersion = "42.0.3";
44761
+ const libVersion = "43.0.1";
45057
44762
  addLibraryVersionToPerseusDebug(libName, libVersion);
45058
44763
 
45059
44764
  const apiVersion = {
@@ -45677,13 +45382,6 @@ class ServerItemRenderer extends React.Component {
45677
45382
  this.props.onRendered(true);
45678
45383
  }
45679
45384
  }
45680
- if (this.props.score && this.props.score !== prevProps.score) {
45681
- const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45682
- this.setState({
45683
- questionCompleted: this.props.score.correct,
45684
- questionHighlightedWidgets: emptyQuestionAreaWidgets
45685
- });
45686
- }
45687
45385
  }
45688
45386
  componentWillUnmount() {
45689
45387
  if (this.blurTimeoutID != null) {
@@ -45764,7 +45462,7 @@ class ServerItemRenderer extends React.Component {
45764
45462
 
45765
45463
  /**
45766
45464
  * Accepts a question area widgetId, or an answer area widgetId of
45767
- * the form "answer-numeric-input 1", or the string "answer-area"
45465
+ * the form "answer-input-number 1", or the string "answer-area"
45768
45466
  * for the whole answer area (if the answer area is a single widget).
45769
45467
  */
45770
45468
  _setWidgetProps(widgetId, newProps, callback) {
@@ -45801,37 +45499,18 @@ class ServerItemRenderer extends React.Component {
45801
45499
  return this.questionRenderer.getPromptJSON();
45802
45500
  }
45803
45501
 
45804
- /**
45805
- * Returns an array of the widget `.getUserInput()` results
45806
- *
45807
- * TODO: can we remove this? Seems to be just for backwards
45808
- * compatibility with old Perseus Chrome logging
45809
- * @deprecated use getUserInput
45810
- */
45811
- getUserInputLegacy() {
45812
- return this.questionRenderer.getUserInput();
45813
- }
45814
-
45815
- /**
45816
- * Returns an object of the widget `.getUserInput()` results
45817
- */
45818
- getUserInput() {
45819
- return this.questionRenderer.getUserInputMap();
45820
- }
45821
-
45822
45502
  /**
45823
45503
  * Grades the item.
45824
- *
45825
- * @deprecated use scorePerseusItem
45826
45504
  */
45827
45505
  scoreInput() {
45828
- const guess = this.getUserInput();
45829
- const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
45506
+ const guessAndScore = this.questionRenderer.guessAndScore();
45507
+ const guess = guessAndScore[0];
45508
+ const score = guessAndScore[1];
45830
45509
 
45831
45510
  // Continue to include an empty guess for the now defunct answer area.
45832
45511
  // TODO(alex): Check whether we rely on the format here for
45833
45512
  // analyzing ProblemLogs. If not, remove this layer.
45834
- const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
45513
+ const maxCompatGuess = [guess, []];
45835
45514
  const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
45836
45515
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45837
45516
  this.setState({
@@ -47679,6 +47358,140 @@ const registerAllWidgetsForTesting = () => {
47679
47358
  replaceDeprecatedWidgets();
47680
47359
  };
47681
47360
 
47361
+ // These are all the current widgets that require user-input and are supported for automatic scoring
47362
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
47363
+
47364
+ /**
47365
+ * Regex for widget placeholders in a string.
47366
+ *
47367
+ * First capture group is the widget ID (ex. 'radio 1')
47368
+ * Second capture group is the widget type (ex. "radio)
47369
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
47370
+ */
47371
+ function getWidgetRegex() {
47372
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
47373
+ }
47374
+
47375
+ /**
47376
+ * Add a widget placeholder using the widget ID.
47377
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
47378
+ *
47379
+ * @param {string} id
47380
+ * @returns {string}
47381
+ */
47382
+ function addWidget(id) {
47383
+ return `[[☃ ${id}]]`;
47384
+ }
47385
+
47386
+ /**
47387
+ * Get a widget type by a widget's ID
47388
+ * (please don't derive type from ID)
47389
+ *
47390
+ * @param {string} widgetId the ID of the widget
47391
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47392
+ * @returns {string} the widget type (ie "radio")
47393
+ */
47394
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
47395
+ var _widget$type;
47396
+ const widget = widgetMap[WidgetId];
47397
+ return (_widget$type = widget == null ? void 0 : widget.type) != null ? _widget$type : null;
47398
+ }
47399
+
47400
+ /**
47401
+ * Does the content have a specific type of widget?
47402
+ *
47403
+ * @param {string} type the type of the widget in question (ie "radio")
47404
+ * @param {string} content the string to search through
47405
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47406
+ * @returns {boolean} if the content includes the widget type
47407
+ */
47408
+ function contentHasWidgetType(type, content, widgetMap) {
47409
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
47410
+ }
47411
+
47412
+ /**
47413
+ * Extract all widget IDs, which includes the widget type and instance number.
47414
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
47415
+ *
47416
+ * Content should contain Perseus widget placeholders,
47417
+ * which look like: '[[☃ radio 1]]'.
47418
+ *
47419
+ * @param {string} content
47420
+ * @returns {ReadonlyArray<string>} widgetIds
47421
+ */
47422
+ function getWidgetIdsFromContent(content) {
47423
+ const widgets = [];
47424
+ const localWidgetRegex = getWidgetRegex();
47425
+ let match = localWidgetRegex.exec(content);
47426
+ while (match !== null) {
47427
+ widgets.push(match[1]);
47428
+ match = localWidgetRegex.exec(content);
47429
+ }
47430
+ return widgets;
47431
+ }
47432
+
47433
+ /**
47434
+ * Get a list of widget IDs from content,
47435
+ * but only for specific widget types
47436
+ *
47437
+ * @param {string} type the type of widget (ie "radio")
47438
+ * @param {string} content the string to parse
47439
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47440
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
47441
+ */
47442
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
47443
+ const rv = [];
47444
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
47445
+ widgetIdsInContent.forEach(widgetId => {
47446
+ const widget = widgetMap[widgetId];
47447
+ if ((widget == null ? void 0 : widget.type) === type) {
47448
+ rv.push(widgetId);
47449
+ }
47450
+ });
47451
+ return rv;
47452
+ }
47453
+
47454
+ /**
47455
+ * Pull the widget map out of ItemData
47456
+ *
47457
+ * @param {PerseusItem} PerseusItem containing a widgetMap
47458
+ * @returns {WidgetMap} the widget map in the PerseusItem
47459
+ */
47460
+ function getWidgetsMapFromItemData(itemData) {
47461
+ return itemData.question.widgets;
47462
+ }
47463
+
47464
+ /**
47465
+ * Get the widget information from a WidgetMap
47466
+ *
47467
+ * @param {string} widgetId the ID of the widget
47468
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
47469
+ */
47470
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
47471
+ var _widgetMap$widgetId;
47472
+ return (_widgetMap$widgetId = widgetMap[widgetId]) != null ? _widgetMap$widgetId : null;
47473
+ }
47474
+
47475
+ /**
47476
+ * Get select widgets from a widget map.
47477
+ * Useful for multi-items that needs to split
47478
+ * one PerseusItem into several
47479
+ *
47480
+ * @param {ReadonlyArray<string>} widgetIds to extract
47481
+ * @param {PerseusWidgetsMap} widgetMap to extract from
47482
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
47483
+ */
47484
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
47485
+ const widgets = {};
47486
+ widgetIds.forEach(widgetId => {
47487
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
47488
+ if (widget) {
47489
+ widgets[widgetId] = widget;
47490
+ }
47491
+ });
47492
+ return widgets;
47493
+ }
47494
+
47682
47495
  /**
47683
47496
  * This function extracts the answers from the widgets.
47684
47497
  *
@@ -49623,5 +49436,5 @@ const GrapherUtil = {
49623
49436
  typeToButton
49624
49437
  };
49625
49438
 
49626
- export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusExpressionAnswerFormConsidered, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, addWidget, apiVersion, bodyXsmallBold, buildEmptyItemTreeForShape, buildMapper, components, containerSizeClass, contentHasWidgetType, conversionRequired, convertDeprecatedWidgets, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getClockwiseAngle, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, lockedFigureColors, lockedFigureFillStyles, makeSafeUrl, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, scorePerseusItem, shapes, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
49439
+ export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusExpressionAnswerFormConsidered, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, addWidget, apiVersion, bodyXsmallBold, buildEmptyItemTreeForShape, buildMapper, components, containerSizeClass, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getClockwiseAngle, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, lockedFigureColors, lockedFigureFillStyles, makeSafeUrl, mathOnlyParser, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
49627
49440
  //# sourceMappingURL=index.js.map