@khanacademy/perseus 41.5.0 → 42.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/es/index.js +325 -268
  2. package/dist/es/index.js.map +1 -1
  3. package/dist/es/strings.js +3 -2
  4. package/dist/es/strings.js.map +1 -1
  5. package/dist/index.d.ts +3 -1
  6. package/dist/index.js +322 -263
  7. package/dist/index.js.map +1 -1
  8. package/dist/renderer-util.d.ts +3 -2
  9. package/dist/renderer.d.ts +7 -10
  10. package/dist/server-item-renderer.d.ts +33 -2
  11. package/dist/strings.d.ts +2 -1
  12. package/dist/strings.js +3 -2
  13. package/dist/strings.js.map +1 -1
  14. package/dist/types.d.ts +6 -1
  15. package/dist/util/parse-perseus-json/exhaustive-test-tool/index.d.ts +2 -0
  16. package/dist/widget-ai-utils/graded-group/{prompt-utils.d.ts → graded-group-ai-utils.d.ts} +1 -1
  17. package/dist/widget-ai-utils/graded-group-set/{prompt-utils.d.ts → graded-group-set-ai-utils.d.ts} +1 -1
  18. package/dist/widget-ai-utils/group/{prompt-utils.d.ts → group-ai-utils.d.ts} +1 -1
  19. package/dist/widget-ai-utils/prompt-types.d.ts +33 -0
  20. package/dist/widget-ai-utils/unsupported-widget.d.ts +1 -1
  21. package/dist/widgets/categorizer/categorizer.d.ts +1 -1
  22. package/dist/widgets/definition/definition.d.ts +1 -1
  23. package/dist/widgets/dropdown/dropdown.d.ts +1 -1
  24. package/dist/widgets/explanation/explanation.d.ts +1 -1
  25. package/dist/widgets/expression/expression.d.ts +1 -1
  26. package/dist/widgets/graded-group/graded-group.d.ts +1 -1
  27. package/dist/widgets/graded-group-set/graded-group-set.d.ts +1 -1
  28. package/dist/widgets/grapher/grapher.d.ts +1 -1
  29. package/dist/widgets/group/group.d.ts +1 -1
  30. package/dist/widgets/image/image.d.ts +1 -1
  31. package/dist/widgets/input-number/input-number.d.ts +1 -1
  32. package/dist/widgets/interactive-graph.d.ts +1 -1
  33. package/dist/widgets/interactive-graphs/graphs/components/movable-point.d.ts +9 -0
  34. package/dist/widgets/interactive-graphs/graphs/components/use-control-point.d.ts +9 -0
  35. package/dist/widgets/label-image/label-image.d.ts +1 -1
  36. package/dist/widgets/matcher/matcher.d.ts +1 -1
  37. package/dist/widgets/matrix/matrix.d.ts +1 -1
  38. package/dist/widgets/number-line/number-line.d.ts +1 -1
  39. package/dist/widgets/orderer/orderer.d.ts +1 -1
  40. package/dist/widgets/passage/passage.d.ts +1 -1
  41. package/dist/widgets/passage-ref/passage-ref.d.ts +1 -1
  42. package/dist/widgets/radio/radio-component.d.ts +1 -1
  43. package/dist/widgets/sorter/sorter.d.ts +1 -1
  44. package/package.json +1 -1
  45. package/dist/prompt-types.d.ts +0 -33
  46. /package/dist/widget-ai-utils/categorizer/{prompt-utils.d.ts → categorizer-ai-utils.d.ts} +0 -0
  47. /package/dist/widget-ai-utils/cs-program/{prompt-utils.d.ts → cs-program-ai-utils.d.ts} +0 -0
  48. /package/dist/widget-ai-utils/definition/{prompt-utils.d.ts → definition-ai-utils.d.ts} +0 -0
  49. /package/dist/widget-ai-utils/dropdown/{prompt-utils.d.ts → dropdown-ai-utils.d.ts} +0 -0
  50. /package/dist/widget-ai-utils/explanation/{prompt-utils.d.ts → explanation-ai-utils.d.ts} +0 -0
  51. /package/dist/widget-ai-utils/expression/{prompt-utils.d.ts → expression-ai-utils.d.ts} +0 -0
  52. /package/dist/widget-ai-utils/grapher/{prompt-utils.d.ts → grapher-ai-utils.d.ts} +0 -0
  53. /package/dist/widget-ai-utils/iframe/{prompt-utils.d.ts → iframe-ai-utils.d.ts} +0 -0
  54. /package/dist/widget-ai-utils/image/{prompt-utils.d.ts → image-ai-utils.d.ts} +0 -0
  55. /package/dist/widget-ai-utils/input-number/{prompt-utils.d.ts → input-number-ai-utils.d.ts} +0 -0
  56. /package/dist/widget-ai-utils/interaction/{prompt-utils.d.ts → interaction-ai-utils.d.ts} +0 -0
  57. /package/dist/widget-ai-utils/interactive-graph/{prompt-utils.d.ts → interactive-graph-ai-utils.d.ts} +0 -0
  58. /package/dist/widget-ai-utils/label-image/{prompt-utils.d.ts → label-image-ai-utils.d.ts} +0 -0
  59. /package/dist/widget-ai-utils/matcher/{prompt-utils.d.ts → matcher-ai-utils.d.ts} +0 -0
  60. /package/dist/widget-ai-utils/matrix/{prompt-utils.d.ts → matrix-ai-utils.d.ts} +0 -0
  61. /package/dist/widget-ai-utils/measurer/{prompt-utils.d.ts → measurer-ai-utils.d.ts} +0 -0
  62. /package/dist/widget-ai-utils/number-line/{prompt-utils.d.ts → number-line-ai-utils.d.ts} +0 -0
  63. /package/dist/widget-ai-utils/orderer/{prompt-utils.d.ts → orderer-ai-utils.d.ts} +0 -0
  64. /package/dist/widget-ai-utils/passage/{prompt-utils.d.ts → passage-ai-utils.d.ts} +0 -0
  65. /package/dist/widget-ai-utils/passage-ref/{prompt-utils.d.ts → passage-ref-ai-utils.d.ts} +0 -0
  66. /package/dist/widget-ai-utils/phet-simulation/{prompt-utils.d.ts → phet-simulation-ai-utils.d.ts} +0 -0
  67. /package/dist/widget-ai-utils/plotter/{prompt-utils.d.ts → plotter-ai-utils.d.ts} +0 -0
  68. /package/dist/widget-ai-utils/python-program/{prompt-utils.d.ts → python-ai-utils.d.ts} +0 -0
  69. /package/dist/widget-ai-utils/radio/{prompt-utils.d.ts → radio-ai-utils.d.ts} +0 -0
  70. /package/dist/widget-ai-utils/sorter/{prompt-utils.d.ts → sorter-ai-utils.d.ts} +0 -0
  71. /package/dist/widget-ai-utils/video/{prompt-utils.d.ts → video-ai-utils.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -371,10 +371,11 @@ const mockStrings = {
371
371
  graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
372
372
  srPointAtCoordinates: _ref27 => {
373
373
  let {
374
+ num,
374
375
  x,
375
376
  y
376
377
  } = _ref27;
377
- return `Point at ${x} comma ${y}`;
378
+ return `Point ${num} at ${x} comma ${y}`;
378
379
  },
379
380
  srInteractiveElements: _ref28 => {
380
381
  let {
@@ -18874,6 +18875,138 @@ class QuestionParagraph extends React__namespace.Component {
18874
18875
  }
18875
18876
  }
18876
18877
 
18878
+ // These are all the current widgets that require user-input and are supported for automatic scoring
18879
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
18880
+
18881
+ /**
18882
+ * Regex for widget placeholders in a string.
18883
+ *
18884
+ * First capture group is the widget ID (ex. 'radio 1')
18885
+ * Second capture group is the widget type (ex. "radio)
18886
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
18887
+ */
18888
+ function getWidgetRegex() {
18889
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
18890
+ }
18891
+
18892
+ /**
18893
+ * Add a widget placeholder using the widget ID.
18894
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
18895
+ *
18896
+ * @param {string} id
18897
+ * @returns {string}
18898
+ */
18899
+ function addWidget(id) {
18900
+ return `[[☃ ${id}]]`;
18901
+ }
18902
+
18903
+ /**
18904
+ * Get a widget type by a widget's ID
18905
+ * (please don't derive type from ID)
18906
+ *
18907
+ * @param {string} widgetId the ID of the widget
18908
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18909
+ * @returns {string} the widget type (ie "radio")
18910
+ */
18911
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
18912
+ const widget = widgetMap[WidgetId];
18913
+ return widget?.type ?? null;
18914
+ }
18915
+
18916
+ /**
18917
+ * Does the content have a specific type of widget?
18918
+ *
18919
+ * @param {string} type the type of the widget in question (ie "radio")
18920
+ * @param {string} content the string to search through
18921
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18922
+ * @returns {boolean} if the content includes the widget type
18923
+ */
18924
+ function contentHasWidgetType(type, content, widgetMap) {
18925
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
18926
+ }
18927
+
18928
+ /**
18929
+ * Extract all widget IDs, which includes the widget type and instance number.
18930
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
18931
+ *
18932
+ * Content should contain Perseus widget placeholders,
18933
+ * which look like: '[[☃ radio 1]]'.
18934
+ *
18935
+ * @param {string} content
18936
+ * @returns {ReadonlyArray<string>} widgetIds
18937
+ */
18938
+ function getWidgetIdsFromContent(content) {
18939
+ const widgets = [];
18940
+ const localWidgetRegex = getWidgetRegex();
18941
+ let match = localWidgetRegex.exec(content);
18942
+ while (match !== null) {
18943
+ widgets.push(match[1]);
18944
+ match = localWidgetRegex.exec(content);
18945
+ }
18946
+ return widgets;
18947
+ }
18948
+
18949
+ /**
18950
+ * Get a list of widget IDs from content,
18951
+ * but only for specific widget types
18952
+ *
18953
+ * @param {string} type the type of widget (ie "radio")
18954
+ * @param {string} content the string to parse
18955
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18956
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
18957
+ */
18958
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
18959
+ const rv = [];
18960
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
18961
+ widgetIdsInContent.forEach(widgetId => {
18962
+ const widget = widgetMap[widgetId];
18963
+ if (widget?.type === type) {
18964
+ rv.push(widgetId);
18965
+ }
18966
+ });
18967
+ return rv;
18968
+ }
18969
+
18970
+ /**
18971
+ * Pull the widget map out of ItemData
18972
+ *
18973
+ * @param {PerseusItem} PerseusItem containing a widgetMap
18974
+ * @returns {WidgetMap} the widget map in the PerseusItem
18975
+ */
18976
+ function getWidgetsMapFromItemData(itemData) {
18977
+ return itemData.question.widgets;
18978
+ }
18979
+
18980
+ /**
18981
+ * Get the widget information from a WidgetMap
18982
+ *
18983
+ * @param {string} widgetId the ID of the widget
18984
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
18985
+ */
18986
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
18987
+ return widgetMap[widgetId] ?? null;
18988
+ }
18989
+
18990
+ /**
18991
+ * Get select widgets from a widget map.
18992
+ * Useful for multi-items that needs to split
18993
+ * one PerseusItem into several
18994
+ *
18995
+ * @param {ReadonlyArray<string>} widgetIds to extract
18996
+ * @param {PerseusWidgetsMap} widgetMap to extract from
18997
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
18998
+ */
18999
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
19000
+ const widgets = {};
19001
+ widgetIds.forEach(widgetId => {
19002
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
19003
+ if (widget) {
19004
+ widgets[widgetId] = widget;
19005
+ }
19006
+ });
19007
+ return widgets;
19008
+ }
19009
+
18877
19010
  const DEFAULT_ALIGNMENT$1 = "block";
18878
19011
  // NOTE(kevinb): "default" is not one in `validAlignments`.
18879
19012
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -19334,6 +19467,20 @@ widgetIds, userInputMap, strings, locale) {
19334
19467
  }
19335
19468
  });
19336
19469
  }
19470
+
19471
+ // TODO: combine scorePerseusItem with scoreWidgetsFunctional
19472
+ // once scorePerseusItem is the only one calling scoreWidgetsFunctional
19473
+ function scorePerseusItem(perseusRenderData, userInputMap,
19474
+ // TODO(LEMS-2461,LEMS-2391): these probably
19475
+ // need to be removed before we move this to the server
19476
+ strings, locale) {
19477
+ // There seems to be a chance that PerseusRenderer.widgets might include
19478
+ // widget data for widgets that are not in PerseusRenderer.content,
19479
+ // so this checks that the widgets are being used before scoring them
19480
+ const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
19481
+ const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
19482
+ return Util.flattenScores(scores);
19483
+ }
19337
19484
  function scoreWidgetsFunctional(widgets,
19338
19485
  // This is a port of old code, I'm not sure why
19339
19486
  // we need widgetIds vs the keys of the widgets object
@@ -19354,7 +19501,7 @@ widgetIds, userInputMap, strings, locale) {
19354
19501
  const userInput = userInputMap[id];
19355
19502
  const scorer = getWidgetScorer(widget.type);
19356
19503
  if (widget.type === "group") {
19357
- const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19504
+ const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
19358
19505
  widgetScores[id] = Util.flattenScores(scores);
19359
19506
  } else if (scorer) {
19360
19507
  widgetScores[id] = scorer(userInput, widget.options, strings, locale);
@@ -20403,7 +20550,7 @@ class Renderer extends React__namespace.Component {
20403
20550
  // Instead, we center them with css magic in styles.less
20404
20551
  // /cry(aria)
20405
20552
  this._foundTextNodes = true;
20406
- if (___default["default"].contains(this.widgetIds, node.id)) {
20553
+ if (this.widgetIds.includes(node.id)) {
20407
20554
  // We don't want to render a duplicate widget key/ref,
20408
20555
  // as this causes problems with react (for obvious
20409
20556
  // reasons). Instead we just notify the
@@ -20728,7 +20875,7 @@ class Renderer extends React__namespace.Component {
20728
20875
  };
20729
20876
  getInputPaths = () => {
20730
20877
  const inputPaths = [];
20731
- ___default["default"].each(this.widgetIds, widgetId => {
20878
+ this.widgetIds.forEach(widgetId => {
20732
20879
  const widget = this.getWidgetInstance(widgetId);
20733
20880
  if (widget && widget.getInputPaths) {
20734
20881
  // Grab all input paths and add widgetID to the front
@@ -20736,7 +20883,7 @@ class Renderer extends React__namespace.Component {
20736
20883
  // Prefix paths with their widgetID and add to collective
20737
20884
  // list of paths.
20738
20885
  // @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
20739
- ___default["default"].each(widgetInputPaths, inputPath => {
20886
+ widgetInputPaths.forEach(inputPath => {
20740
20887
  const relativeInputPath = [widgetId].concat(inputPath);
20741
20888
  inputPaths.push(relativeInputPath);
20742
20889
  });
@@ -20929,33 +21076,30 @@ class Renderer extends React__namespace.Component {
20929
21076
  }
20930
21077
 
20931
21078
  /**
20932
- * Returns an object mapping from widget ID to perseus-style score.
20933
- * The keys of this object are the values of the array returned
20934
- * from `getWidgetIds`.
20935
- */
20936
- scoreWidgets() {
20937
- return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20938
- }
20939
-
20940
- /**
20941
- * Grades the content.
21079
+ * Scores the content.
21080
+ *
21081
+ * @deprecated use scorePerseusItem
20942
21082
  */
20943
21083
  score() {
20944
- const scores = this.scoreWidgets();
21084
+ const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20945
21085
  const combinedScore = Util.flattenScores(scores);
20946
21086
  return combinedScore;
20947
21087
  }
20948
- guessAndScore() {
21088
+
21089
+ /**
21090
+ * @deprecated use scorePerseusItem
21091
+ */
21092
+ guessAndScore = () => {
20949
21093
  const totalGuess = this.getUserInput();
20950
21094
  const totalScore = this.score();
20951
21095
  return [totalGuess, totalScore];
20952
- }
21096
+ };
20953
21097
  examples = () => {
20954
21098
  const widgetIds = this.widgetIds;
20955
- const examples = ___default["default"].compact(___default["default"].map(widgetIds, widgetId => {
21099
+ const examples = widgetIds.map(widgetId => {
20956
21100
  const widget = this.getWidgetInstance(widgetId);
20957
21101
  return widget != null && widget.examples ? widget.examples() : null;
20958
- }));
21102
+ }).filter(Boolean);
20959
21103
 
20960
21104
  // no widgets with examples
20961
21105
  if (!examples.length) {
@@ -22463,7 +22607,9 @@ const propUpgrades$2 = {
22463
22607
  // input-number to numeric-input. In this case, we need to upgrade
22464
22608
  // the widget options accordingly.
22465
22609
  if (initialProps.value) {
22466
- const provideAnswerForm = initialProps.answerType !== "number";
22610
+ // If the answerType is not number or percent, we need to provide
22611
+ // the answer form for the numeric-input widget
22612
+ const provideAnswerForm = initialProps.answerType !== "number" && initialProps.answerType !== "percent";
22467
22613
 
22468
22614
  // We need to determine the mathFormat for the numeric-input widget
22469
22615
  const mathFormat = initialProps.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
@@ -29528,6 +29674,9 @@ class Group extends React__namespace.Component {
29528
29674
  this.change("widgets", this.rendererRef.props.widgets);
29529
29675
  }
29530
29676
  };
29677
+
29678
+ // TODO(LEMS-2391): replace this when there's a separate check
29679
+ // for valid/invalid state
29531
29680
  const score = this.rendererRef?.score();
29532
29681
  const isValid = score && score.type !== "invalid";
29533
29682
  const isInvalid = score && score.type === "invalid";
@@ -31371,6 +31520,108 @@ function AxisArrows() {
31371
31520
  })));
31372
31521
  }
31373
31522
 
31523
+ /**
31524
+ * 44 is touch best practice and AAA compliant for WCAG
31525
+ * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
31526
+ */
31527
+ const TARGET_SIZE = 44;
31528
+ const REMOVE_BUTTON_ID = "perseus_mafs_remove_button";
31529
+
31530
+ // same as pointsFromNormalized in interactive-graph.tsx
31531
+ const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
31532
+ // maps over [x, y]
31533
+ coords.map((coord, i) => {
31534
+ const axisRange = range[i];
31535
+ if (noSnap) {
31536
+ return axisRange[MIN] + size(axisRange) * coord;
31537
+ }
31538
+ const axisStep = step[i];
31539
+ const nSteps = Math.floor(size(axisRange) / axisStep);
31540
+ const tick = Math.round(coord * nSteps);
31541
+ return axisRange[MIN] + axisStep * tick;
31542
+ }));
31543
+
31544
+ // same as normalizeCoords in interactive-graph.tsx
31545
+ const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
31546
+ return (coord + ranges[i][1]) / size(ranges[i]);
31547
+ }));
31548
+
31549
+ // Returns the closest point to the given `point` that is within the graph
31550
+ // bounds given in `state`.
31551
+ function bound$1(_ref) {
31552
+ let {
31553
+ snapStep,
31554
+ range,
31555
+ point
31556
+ } = _ref;
31557
+ const boundingBox = inset(snapStep, range);
31558
+ return clampToBox(boundingBox, point);
31559
+ }
31560
+ function isUnlimitedGraphState(state) {
31561
+ return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
31562
+ }
31563
+
31564
+ /**
31565
+ * Replace all text outside of the $ TeX blocks with `\\text{...}`
31566
+ * This way, the entire resulting string can be rendered within <TeX>
31567
+ * and the text outside of the $ blocks will be non-TeX text.
31568
+ */
31569
+ function replaceOutsideTeX(mathString) {
31570
+ // All the information we need is in the first section,
31571
+ // whether it's typed as "blockmath" or "paragraph"
31572
+ const firstSection = SimpleMarkdown__namespace.parse(mathString)[0];
31573
+
31574
+ // If it's blockMath, the outer level has the full math content.
31575
+ if (firstSection.type === "blockMath") {
31576
+ return firstSection.content;
31577
+ }
31578
+
31579
+ // If it's a paragraph, we need to iterate through the sections
31580
+ // to look for individual math blocks.
31581
+ const condensedNodes = condenseTextNodes(firstSection.content);
31582
+ let result = "";
31583
+ for (const piece of condensedNodes) {
31584
+ piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
31585
+ }
31586
+ return result;
31587
+ }
31588
+ // Helper function for replaceOutsideTeX()
31589
+ // Condense adjacent text nodes into a single text node
31590
+ function condenseTextNodes(nodes) {
31591
+ const result = [];
31592
+ if (!nodes) {
31593
+ return result;
31594
+ }
31595
+ let currentText = "";
31596
+ for (const node of nodes) {
31597
+ if (node.type === "math") {
31598
+ if (currentText) {
31599
+ result.push({
31600
+ type: "text",
31601
+ content: currentText
31602
+ });
31603
+ currentText = "";
31604
+ }
31605
+ result.push(node);
31606
+ } else {
31607
+ currentText += node.content;
31608
+ }
31609
+ }
31610
+ if (currentText) {
31611
+ result.push({
31612
+ type: "text",
31613
+ content: currentText
31614
+ });
31615
+ }
31616
+ return result;
31617
+ }
31618
+
31619
+ // Helper function for replaceOutsideTeX()
31620
+ function escapeSpecialChars(str) {
31621
+ // Escape $, \, {, and } characters
31622
+ return str.replace(/([$\\{}])/g, "\\$1");
31623
+ }
31624
+
31374
31625
  function AxisLabels() {
31375
31626
  const {
31376
31627
  range,
@@ -31402,7 +31653,7 @@ function AxisLabels() {
31402
31653
  fontSize: "14px",
31403
31654
  transform: "translate(7px, -50%)"
31404
31655
  }
31405
- }, /*#__PURE__*/React__default["default"].createElement(TeX, null, xAxisLabelText)), /*#__PURE__*/React__default["default"].createElement("span", {
31656
+ }, /*#__PURE__*/React__default["default"].createElement(TeX, null, replaceOutsideTeX(xAxisLabelText))), /*#__PURE__*/React__default["default"].createElement("span", {
31406
31657
  style: {
31407
31658
  position: "absolute",
31408
31659
  left: x2,
@@ -31410,7 +31661,7 @@ function AxisLabels() {
31410
31661
  fontSize: "14px",
31411
31662
  transform: "translate(-50%, 0px)"
31412
31663
  }
31413
- }, /*#__PURE__*/React__default["default"].createElement(TeX, null, yAxisLabelText)));
31664
+ }, /*#__PURE__*/React__default["default"].createElement(TeX, null, replaceOutsideTeX(yAxisLabelText))));
31414
31665
  }
31415
31666
 
31416
31667
  // The size of the ticks and labels in pixels
@@ -32348,108 +32599,6 @@ const lockedFigureFillStyles = {
32348
32599
 
32349
32600
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
32350
32601
 
32351
- /**
32352
- * 44 is touch best practice and AAA compliant for WCAG
32353
- * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
32354
- */
32355
- const TARGET_SIZE = 44;
32356
- const REMOVE_BUTTON_ID = "perseus_mafs_remove_button";
32357
-
32358
- // same as pointsFromNormalized in interactive-graph.tsx
32359
- const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
32360
- // maps over [x, y]
32361
- coords.map((coord, i) => {
32362
- const axisRange = range[i];
32363
- if (noSnap) {
32364
- return axisRange[MIN] + size(axisRange) * coord;
32365
- }
32366
- const axisStep = step[i];
32367
- const nSteps = Math.floor(size(axisRange) / axisStep);
32368
- const tick = Math.round(coord * nSteps);
32369
- return axisRange[MIN] + axisStep * tick;
32370
- }));
32371
-
32372
- // same as normalizeCoords in interactive-graph.tsx
32373
- const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
32374
- return (coord + ranges[i][1]) / size(ranges[i]);
32375
- }));
32376
-
32377
- // Returns the closest point to the given `point` that is within the graph
32378
- // bounds given in `state`.
32379
- function bound$1(_ref) {
32380
- let {
32381
- snapStep,
32382
- range,
32383
- point
32384
- } = _ref;
32385
- const boundingBox = inset(snapStep, range);
32386
- return clampToBox(boundingBox, point);
32387
- }
32388
- function isUnlimitedGraphState(state) {
32389
- return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
32390
- }
32391
-
32392
- /**
32393
- * Replace all text outside of the $ TeX blocks with `\\text{...}`
32394
- * This way, the entire resulting string can be rendered within <TeX>
32395
- * and the text outside of the $ blocks will be non-TeX text.
32396
- */
32397
- function replaceOutsideTeX(mathString) {
32398
- // All the information we need is in the first section,
32399
- // whether it's typed as "blockmath" or "paragraph"
32400
- const firstSection = SimpleMarkdown__namespace.parse(mathString)[0];
32401
-
32402
- // If it's blockMath, the outer level has the full math content.
32403
- if (firstSection.type === "blockMath") {
32404
- return firstSection.content;
32405
- }
32406
-
32407
- // If it's a paragraph, we need to iterate through the sections
32408
- // to look for individual math blocks.
32409
- const condensedNodes = condenseTextNodes(firstSection.content);
32410
- let result = "";
32411
- for (const piece of condensedNodes) {
32412
- piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
32413
- }
32414
- return result;
32415
- }
32416
- // Helper function for replaceOutsideTeX()
32417
- // Condense adjacent text nodes into a single text node
32418
- function condenseTextNodes(nodes) {
32419
- const result = [];
32420
- if (!nodes) {
32421
- return result;
32422
- }
32423
- let currentText = "";
32424
- for (const node of nodes) {
32425
- if (node.type === "math") {
32426
- if (currentText) {
32427
- result.push({
32428
- type: "text",
32429
- content: currentText
32430
- });
32431
- currentText = "";
32432
- }
32433
- result.push(node);
32434
- } else {
32435
- currentText += node.content;
32436
- }
32437
- }
32438
- if (currentText) {
32439
- result.push({
32440
- type: "text",
32441
- content: currentText
32442
- });
32443
- }
32444
- return result;
32445
- }
32446
-
32447
- // Helper function for replaceOutsideTeX()
32448
- function escapeSpecialChars(str) {
32449
- // Escape $, \, {, and } characters
32450
- return str.replace(/([$\\{}])/g, "\\$1");
32451
- }
32452
-
32453
32602
  function LockedLabel(props) {
32454
32603
  const {
32455
32604
  coord,
@@ -33678,6 +33827,7 @@ function useControlPoint(params) {
33678
33827
  } = useGraphConfig();
33679
33828
  const {
33680
33829
  point,
33830
+ sequenceNumber,
33681
33831
  color,
33682
33832
  cursor,
33683
33833
  constrain = p => snap(snapStep, p),
@@ -33718,6 +33868,7 @@ function useControlPoint(params) {
33718
33868
  ref: focusableHandleRef,
33719
33869
  role: "button",
33720
33870
  "aria-label": strings.srPointAtCoordinates({
33871
+ num: sequenceNumber,
33721
33872
  x: srFormatNumber(point[X], locale),
33722
33873
  y: srFormatNumber(point[Y], locale)
33723
33874
  })
@@ -33795,6 +33946,7 @@ const MovableLine = props => {
33795
33946
  focusableHandle: focusableHandle1
33796
33947
  } = useControlPoint({
33797
33948
  point: start,
33949
+ sequenceNumber: 1,
33798
33950
  color,
33799
33951
  onMove: p => onMovePoint(0, p)
33800
33952
  });
@@ -33803,6 +33955,7 @@ const MovableLine = props => {
33803
33955
  focusableHandle: focusableHandle2
33804
33956
  } = useControlPoint({
33805
33957
  point: end,
33958
+ sequenceNumber: 2,
33806
33959
  color,
33807
33960
  onMove: p => onMovePoint(1, p)
33808
33961
  });
@@ -33984,14 +34137,17 @@ function AngleGraph(props) {
33984
34137
  // Render the lines, angle, and then movable points
33985
34138
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, svgLines, /*#__PURE__*/React__namespace.createElement(Angle, angleParams), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33986
34139
  point: coords[1],
34140
+ sequenceNumber: 1,
33987
34141
  constrain: p => p,
33988
34142
  onMove: destination => dispatch(actions.angle.movePoint(1, destination))
33989
34143
  }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33990
34144
  point: coords[0],
34145
+ sequenceNumber: 2,
33991
34146
  constrain: getAngleSideConstraint(coords[0], coords[1], snapDegrees || 1),
33992
34147
  onMove: destination => dispatch(actions.angle.movePoint(0, destination))
33993
34148
  }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33994
34149
  point: coords[2],
34150
+ sequenceNumber: 3,
33995
34151
  constrain: getAngleSideConstraint(coords[2], coords[1], snapDegrees || 1),
33996
34152
  onMove: destination => dispatch(actions.angle.movePoint(2, destination))
33997
34153
  }));
@@ -34156,6 +34312,7 @@ function CircleGraph(props) {
34156
34312
  onMove: c => dispatch(actions.circle.moveCenter(c))
34157
34313
  }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34158
34314
  point: radiusPoint,
34315
+ sequenceNumber: 1,
34159
34316
  cursor: "ew-resize",
34160
34317
  onMove: newRadiusPoint => {
34161
34318
  dispatch(actions.circle.moveRadiusPoint(newRadiusPoint));
@@ -34331,6 +34488,7 @@ function LimitedPointGraph(props) {
34331
34488
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34332
34489
  key: i,
34333
34490
  point: point,
34491
+ sequenceNumber: i + 1,
34334
34492
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination))
34335
34493
  })));
34336
34494
  }
@@ -34372,6 +34530,7 @@ function UnlimitedPointGraph(props) {
34372
34530
  }), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34373
34531
  key: i,
34374
34532
  point: point,
34533
+ sequenceNumber: i + 1,
34375
34534
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
34376
34535
  ref: ref => {
34377
34536
  itemsRef.current[i] = ref;
@@ -34403,9 +34562,10 @@ function describePointGraph(state, i18n) {
34403
34562
  if (state.coords.length === 0) {
34404
34563
  return strings.srNoInteractiveElements;
34405
34564
  }
34406
- const pointDescriptions = state.coords.map(_ref2 => {
34565
+ const pointDescriptions = state.coords.map((_ref2, index) => {
34407
34566
  let [x, y] = _ref2;
34408
34567
  return strings.srPointAtCoordinates({
34568
+ num: index + 1,
34409
34569
  x: srFormatNumber(x, locale),
34410
34570
  y: srFormatNumber(y, locale)
34411
34571
  });
@@ -34525,6 +34685,7 @@ const LimitedPolygonGraph = props => {
34525
34685
  key: "point-" + i,
34526
34686
  constrain: constrain,
34527
34687
  point: point,
34688
+ sequenceNumber: i + 1,
34528
34689
  onMove: destination => {
34529
34690
  const now = Date.now();
34530
34691
  const targetFPS = 40;
@@ -34671,6 +34832,7 @@ const UnlimitedPolygonGraph = props => {
34671
34832
  }), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34672
34833
  key: i,
34673
34834
  point: point,
34835
+ sequenceNumber: i + 1,
34674
34836
  onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
34675
34837
  ref: ref => {
34676
34838
  pointRef.current[i] = ref;
@@ -34743,6 +34905,7 @@ function QuadraticGraph(props) {
34743
34905
  }), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34744
34906
  key: "point-" + i,
34745
34907
  point: coord,
34908
+ sequenceNumber: i + 1,
34746
34909
  onMove: destination => dispatch(actions.quadratic.movePoint(i, destination))
34747
34910
  })));
34748
34911
  }
@@ -34871,6 +35034,7 @@ function SinusoidGraph(props) {
34871
35034
  }), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34872
35035
  key: "point-" + i,
34873
35036
  point: coord,
35037
+ sequenceNumber: i + 1,
34874
35038
  onMove: destination => dispatch(actions.sinusoid.movePoint(i, destination))
34875
35039
  })));
34876
35040
  }
@@ -45039,7 +45203,7 @@ const init = function () {
45039
45203
 
45040
45204
  // This file is processed by a Rollup plugin (replace) to inject the production
45041
45205
  const libName = "@khanacademy/perseus";
45042
- const libVersion = "41.5.0";
45206
+ const libVersion = "42.0.0";
45043
45207
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45044
45208
 
45045
45209
  const apiVersion = {
@@ -45644,6 +45808,13 @@ class ServerItemRenderer extends React__namespace.Component {
45644
45808
  this.props.onRendered(true);
45645
45809
  }
45646
45810
  }
45811
+ if (this.props.score && this.props.score !== prevProps.score) {
45812
+ const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45813
+ this.setState({
45814
+ questionCompleted: this.props.score.correct,
45815
+ questionHighlightedWidgets: emptyQuestionAreaWidgets
45816
+ });
45817
+ }
45647
45818
  }
45648
45819
  componentWillUnmount() {
45649
45820
  if (this.blurTimeoutID != null) {
@@ -45779,18 +45950,37 @@ class ServerItemRenderer extends React__namespace.Component {
45779
45950
  return this.questionRenderer.getPromptJSON();
45780
45951
  }
45781
45952
 
45953
+ /**
45954
+ * Returns an array of the widget `.getUserInput()` results
45955
+ *
45956
+ * TODO: can we remove this? Seems to be just for backwards
45957
+ * compatibility with old Perseus Chrome logging
45958
+ * @deprecated use getUserInput
45959
+ */
45960
+ getUserInputLegacy() {
45961
+ return this.questionRenderer.getUserInput();
45962
+ }
45963
+
45964
+ /**
45965
+ * Returns an object of the widget `.getUserInput()` results
45966
+ */
45967
+ getUserInput() {
45968
+ return this.questionRenderer.getUserInputMap();
45969
+ }
45970
+
45782
45971
  /**
45783
45972
  * Grades the item.
45973
+ *
45974
+ * @deprecated use scorePerseusItem
45784
45975
  */
45785
45976
  scoreInput() {
45786
- const guessAndScore = this.questionRenderer.guessAndScore();
45787
- const guess = guessAndScore[0];
45788
- const score = guessAndScore[1];
45977
+ const guess = this.getUserInput();
45978
+ const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
45789
45979
 
45790
45980
  // Continue to include an empty guess for the now defunct answer area.
45791
45981
  // TODO(alex): Check whether we rely on the format here for
45792
45982
  // analyzing ProblemLogs. If not, remove this layer.
45793
- const maxCompatGuess = [guess, []];
45983
+ const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
45794
45984
  const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
45795
45985
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45796
45986
  this.setState({
@@ -47652,138 +47842,6 @@ const registerAllWidgetsForTesting = () => {
47652
47842
  replaceDeprecatedWidgets();
47653
47843
  };
47654
47844
 
47655
- // These are all the current widgets that require user-input and are supported for automatic scoring
47656
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
47657
-
47658
- /**
47659
- * Regex for widget placeholders in a string.
47660
- *
47661
- * First capture group is the widget ID (ex. 'radio 1')
47662
- * Second capture group is the widget type (ex. "radio)
47663
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
47664
- */
47665
- function getWidgetRegex() {
47666
- return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
47667
- }
47668
-
47669
- /**
47670
- * Add a widget placeholder using the widget ID.
47671
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
47672
- *
47673
- * @param {string} id
47674
- * @returns {string}
47675
- */
47676
- function addWidget(id) {
47677
- return `[[☃ ${id}]]`;
47678
- }
47679
-
47680
- /**
47681
- * Get a widget type by a widget's ID
47682
- * (please don't derive type from ID)
47683
- *
47684
- * @param {string} widgetId the ID of the widget
47685
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47686
- * @returns {string} the widget type (ie "radio")
47687
- */
47688
- function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
47689
- const widget = widgetMap[WidgetId];
47690
- return widget?.type ?? null;
47691
- }
47692
-
47693
- /**
47694
- * Does the content have a specific type of widget?
47695
- *
47696
- * @param {string} type the type of the widget in question (ie "radio")
47697
- * @param {string} content the string to search through
47698
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47699
- * @returns {boolean} if the content includes the widget type
47700
- */
47701
- function contentHasWidgetType(type, content, widgetMap) {
47702
- return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
47703
- }
47704
-
47705
- /**
47706
- * Extract all widget IDs, which includes the widget type and instance number.
47707
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
47708
- *
47709
- * Content should contain Perseus widget placeholders,
47710
- * which look like: '[[☃ radio 1]]'.
47711
- *
47712
- * @param {string} content
47713
- * @returns {ReadonlyArray<string>} widgetIds
47714
- */
47715
- function getWidgetIdsFromContent(content) {
47716
- const widgets = [];
47717
- const localWidgetRegex = getWidgetRegex();
47718
- let match = localWidgetRegex.exec(content);
47719
- while (match !== null) {
47720
- widgets.push(match[1]);
47721
- match = localWidgetRegex.exec(content);
47722
- }
47723
- return widgets;
47724
- }
47725
-
47726
- /**
47727
- * Get a list of widget IDs from content,
47728
- * but only for specific widget types
47729
- *
47730
- * @param {string} type the type of widget (ie "radio")
47731
- * @param {string} content the string to parse
47732
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47733
- * @returns {ReadonlyArray<string>} the widget type (ie "radio")
47734
- */
47735
- function getWidgetIdsFromContentByType(type, content, widgetMap) {
47736
- const rv = [];
47737
- const widgetIdsInContent = getWidgetIdsFromContent(content);
47738
- widgetIdsInContent.forEach(widgetId => {
47739
- const widget = widgetMap[widgetId];
47740
- if (widget?.type === type) {
47741
- rv.push(widgetId);
47742
- }
47743
- });
47744
- return rv;
47745
- }
47746
-
47747
- /**
47748
- * Pull the widget map out of ItemData
47749
- *
47750
- * @param {PerseusItem} PerseusItem containing a widgetMap
47751
- * @returns {WidgetMap} the widget map in the PerseusItem
47752
- */
47753
- function getWidgetsMapFromItemData(itemData) {
47754
- return itemData.question.widgets;
47755
- }
47756
-
47757
- /**
47758
- * Get the widget information from a WidgetMap
47759
- *
47760
- * @param {string} widgetId the ID of the widget
47761
- * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
47762
- */
47763
- function getWidgetFromWidgetMap(widgetId, widgetMap) {
47764
- return widgetMap[widgetId] ?? null;
47765
- }
47766
-
47767
- /**
47768
- * Get select widgets from a widget map.
47769
- * Useful for multi-items that needs to split
47770
- * one PerseusItem into several
47771
- *
47772
- * @param {ReadonlyArray<string>} widgetIds to extract
47773
- * @param {PerseusWidgetsMap} widgetMap to extract from
47774
- * @return {PerseusWidgetsMap} a new widget map with requested widgets
47775
- */
47776
- function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
47777
- const widgets = {};
47778
- widgetIds.forEach(widgetId => {
47779
- const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
47780
- if (widget) {
47781
- widgets[widgetId] = widget;
47782
- }
47783
- });
47784
- return widgets;
47785
- }
47786
-
47787
47845
  /**
47788
47846
  * This function extracts the answers from the widgets.
47789
47847
  *
@@ -49836,6 +49894,7 @@ exports.parsePerseusItem = parsePerseusItem;
49836
49894
  exports.plotterPlotTypes = plotterPlotTypes;
49837
49895
  exports.preprocessTex = preprocessTex;
49838
49896
  exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
49897
+ exports.scorePerseusItem = scorePerseusItem;
49839
49898
  exports.shapes = shapes;
49840
49899
  exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
49841
49900
  exports.traverse = traverse;