@khanacademy/perseus 36.1.1 → 37.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 (57) hide show
  1. package/dist/all-widgets.d.ts +1 -0
  2. package/dist/basic-widgets.d.ts +1 -0
  3. package/dist/components/math-input.d.ts +1 -1
  4. package/dist/es/index.js +523 -660
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/extra-widgets.d.ts +1 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +489 -635
  9. package/dist/index.js.map +1 -1
  10. package/dist/renderer-util.d.ts +8 -0
  11. package/dist/renderer.d.ts +19 -20
  12. package/dist/server-item-renderer.d.ts +3 -1
  13. package/dist/types.d.ts +11 -3
  14. package/dist/util/graphie.d.ts +1 -1
  15. package/dist/util.d.ts +4 -0
  16. package/dist/validation.types.d.ts +19 -9
  17. package/dist/widgets/categorizer/categorizer.d.ts +3 -5
  18. package/dist/widgets/cs-program/cs-program.d.ts +4 -8
  19. package/dist/widgets/definition/definition.d.ts +3 -4
  20. package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +35 -0
  21. package/dist/widgets/deprecated-standin/index.d.ts +1 -37
  22. package/dist/widgets/dropdown/dropdown.d.ts +2 -3
  23. package/dist/widgets/explanation/explanation.d.ts +4 -5
  24. package/dist/widgets/expression/expression.d.ts +4 -8
  25. package/dist/widgets/graded-group/graded-group.d.ts +1 -0
  26. package/dist/widgets/graded-group-set/graded-group-set.d.ts +1 -0
  27. package/dist/widgets/grapher/grapher.d.ts +2 -3
  28. package/dist/widgets/group/group.d.ts +8 -5
  29. package/dist/widgets/iframe/iframe.d.ts +2 -3
  30. package/dist/widgets/image/image.d.ts +3 -4
  31. package/dist/widgets/input-number/input-number.d.ts +2 -5
  32. package/dist/widgets/interaction/interaction.d.ts +3 -4
  33. package/dist/widgets/interactive-graph.d.ts +3 -5
  34. package/dist/widgets/interactive-graphs/math/angles.d.ts +3 -1
  35. package/dist/widgets/interactive-graphs/math/index.d.ts +1 -1
  36. package/dist/widgets/label-image/label-image.d.ts +3 -4
  37. package/dist/widgets/matcher/matcher.d.ts +2 -3
  38. package/dist/widgets/matrix/matrix.d.ts +3 -5
  39. package/dist/widgets/measurer/measurer.d.ts +2 -4
  40. package/dist/widgets/molecule/molecule.d.ts +2 -4
  41. package/dist/widgets/number-line/number-line.d.ts +3 -4
  42. package/dist/widgets/numeric-input/numeric-input.d.ts +2 -3
  43. package/dist/widgets/orderer/orderer.d.ts +2 -3
  44. package/dist/widgets/passage/passage.d.ts +3 -9
  45. package/dist/widgets/passage-ref/passage-ref.d.ts +3 -4
  46. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +2 -4
  47. package/dist/widgets/phet-simulation/phet-simulation.d.ts +2 -2
  48. package/dist/widgets/plotter/plotter.d.ts +2 -3
  49. package/dist/widgets/python-program/python-program.d.ts +1 -0
  50. package/dist/widgets/radio/radio-component.d.ts +2 -5
  51. package/dist/widgets/radio/radio.d.ts +1 -0
  52. package/dist/widgets/sorter/sorter.d.ts +2 -3
  53. package/dist/widgets/table/table.d.ts +3 -5
  54. package/dist/widgets/video/video.d.ts +2 -8
  55. package/dist/widgets.d.ts +2 -1
  56. package/package.json +9 -9
  57. package/dist/widgets/expression/expression.types.d.ts +0 -2
package/dist/index.js CHANGED
@@ -2386,23 +2386,11 @@ const anglicizeOperators = tex => {
2386
2386
  const normalizeTex = tex => {
2387
2387
  return anglicizeOperators(tex);
2388
2388
  };
2389
- const deriveKeypadVersion = apiOptions => {
2390
- // We can derive which version of the keypad is in use. This is
2391
- // a bit tricky, but this code will be relatively short-lived
2392
- // as we coalesce onto the new, v2 Keypad, at which point we
2393
- // can remove this `virtualKeypadVersion` field entirely.
2394
- return apiOptions.nativeKeypadProxy != null ? "REACT_NATIVE_KEYPAD" : "MATH_INPUT_KEYPAD_V2";
2395
- };
2396
2389
  // The new, MathQuill input expression widget
2397
2390
  class Expression extends React__namespace.Component {
2398
2391
  static contextType = PerseusI18nContext;
2399
2392
  _textareaId = `expression_textarea_${Date.now()}`;
2400
2393
  _isMounted = false;
2401
-
2402
- // TODO remove this in favor of just using expressionValidator
2403
- static validate(userInput, rubric, strings, locale) {
2404
- return expressionValidator(userInput, rubric, strings, locale);
2405
- }
2406
2394
  static getUserInputFromProps(props) {
2407
2395
  return normalizeTex(props.value);
2408
2396
  }
@@ -2478,26 +2466,6 @@ class Expression extends React__namespace.Component {
2478
2466
  });
2479
2467
  }
2480
2468
  };
2481
- simpleValidate = _ref => {
2482
- let {
2483
- scoring,
2484
- ...rubric
2485
- } = _ref;
2486
- const score = expressionValidator(this.getUserInput(), rubric, this.context.strings, this.context.locale);
2487
-
2488
- // "scoring" is a flag that indicates when we are checking answers.
2489
- // otherwise, we may just be checking validity after changes.
2490
- if (scoring && score.type !== "invalid") {
2491
- this.props.analytics?.onAnalyticsEvent({
2492
- type: "perseus:expression-evaluated",
2493
- payload: {
2494
- result: score.earned === score.total ? "correct" : "incorrect",
2495
- virtualKeypadVersion: deriveKeypadVersion(this.props.apiOptions)
2496
- }
2497
- });
2498
- }
2499
- return score;
2500
- };
2501
2469
  getUserInput() {
2502
2470
  return Expression.getUserInputFromProps(this.props);
2503
2471
  }
@@ -2796,7 +2764,8 @@ var Expression$1 = {
2796
2764
  },
2797
2765
  propUpgrades: propUpgrades$2,
2798
2766
  // For use by the editor
2799
- isLintable: true
2767
+ isLintable: true,
2768
+ validator: expressionValidator
2800
2769
  };
2801
2770
 
2802
2771
  /**
@@ -3026,6 +2995,9 @@ function combineScores(scoreA, scoreB) {
3026
2995
  }
3027
2996
  });
3028
2997
  }
2998
+ function flattenScores(widgetScoreMap) {
2999
+ return Object.values(widgetScoreMap).reduce(combineScores, noScore);
3000
+ }
3029
3001
  function isCorrect(score) {
3030
3002
  return score.type === "points" && score.earned >= score.total;
3031
3003
  }
@@ -3556,6 +3528,7 @@ const Util = {
3556
3528
  shuffle: shuffle$3,
3557
3529
  split,
3558
3530
  combineScores,
3531
+ flattenScores,
3559
3532
  keScoreFromPerseusScore,
3560
3533
  firstNumericalParse: firstNumericalParse$1,
3561
3534
  stringArrayOfSize: stringArrayOfSize$1,
@@ -10094,6 +10067,8 @@ const setLabelMargins = function (span, size) {
10094
10067
  marginTop: -height / 2 - y * scale
10095
10068
  });
10096
10069
  } else {
10070
+ const currentHeightMatchesProps = span.scrollHeight === height;
10071
+
10097
10072
  // We are using jQuery to collect information and calculate a scale
10098
10073
  // since we don't have a way to pass it to this function.
10099
10074
  // We need the width of the container in order to calculate the scale to apply to the label.
@@ -10115,6 +10090,17 @@ const setLabelMargins = function (span, size) {
10115
10090
  // Inherited line-height values can really mess up placement.
10116
10091
  $container.css("line-height", "normal");
10117
10092
 
10093
+ // TODO: Verify that this change doesn't impact other graphies
10094
+ // Check the full list of consuming widgets.
10095
+
10096
+ // If the change in line-height affected the height of the element,
10097
+ // then the height used for calculations should be updated.
10098
+ // This can happen when the first label in the container calls this method,
10099
+ // and the line-height was different when the height measurement was originally referenced.
10100
+ if (currentHeightMatchesProps && span.scrollHeight !== height) {
10101
+ height = span.scrollHeight;
10102
+ }
10103
+
10118
10104
  // The expected width of the graphie is found in the "max-width" property on ".svg-image" containers,
10119
10105
  // and is found in the "width" property on ".graphie" containers.
10120
10106
  const widthValues = $container.css(["max-width", "width"]) ?? {
@@ -10176,7 +10162,10 @@ const GraphUtils = {
10176
10162
  return kmath.point.distanceToPoint(point1, point2);
10177
10163
  },
10178
10164
  // Find the angle in degrees between two or three points
10179
- findAngle: function (point1, point2, vertex) {
10165
+ // This function is deprecated as it has several issues calculating
10166
+ // correctly when dealing with reflex angles or the position of point2
10167
+ // (LEMS-2202) Remove this function while removing the Legacy Interactive Graph.
10168
+ findAngleDeprecated: function (point1, point2, vertex) {
10180
10169
  if (vertex === undefined) {
10181
10170
  const x = point1[0] - point2[0];
10182
10171
  const y = point1[1] - point2[1];
@@ -10185,7 +10174,7 @@ const GraphUtils = {
10185
10174
  }
10186
10175
  return (180 + Math.atan2(-y, -x) * 180 / Math.PI + 360) % 360;
10187
10176
  }
10188
- return GraphUtils.findAngle(point1, vertex) - GraphUtils.findAngle(point2, vertex);
10177
+ return GraphUtils.findAngleDeprecated(point1, vertex) - GraphUtils.findAngleDeprecated(point2, vertex);
10189
10178
  },
10190
10179
  graphs: {}
10191
10180
  };
@@ -11202,8 +11191,8 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
11202
11191
  p1 = options.point3;
11203
11192
  p3 = options.point1;
11204
11193
  }
11205
- const startAngle = GraphUtils.findAngle(p1, vertex);
11206
- const endAngle = GraphUtils.findAngle(p3, vertex);
11194
+ const startAngle = GraphUtils.findAngleDeprecated(p1, vertex);
11195
+ const endAngle = GraphUtils.findAngleDeprecated(p3, vertex);
11207
11196
  const angle = (endAngle + 360 - startAngle) % 360;
11208
11197
  const halfAngle = (startAngle + angle / 2) % 360;
11209
11198
  const sPadding = 5 * options.pushOut;
@@ -11370,15 +11359,15 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
11370
11359
  let angle = 135;
11371
11360
  let halfAngle;
11372
11361
  if (p1 && p3) {
11373
- const startAngle = GraphUtils.findAngle(p1, vertex);
11374
- const endAngle = GraphUtils.findAngle(p3, vertex);
11362
+ const startAngle = GraphUtils.findAngleDeprecated(p1, vertex);
11363
+ const endAngle = GraphUtils.findAngleDeprecated(p3, vertex);
11375
11364
  angle = (endAngle + 360 - startAngle) % 360;
11376
11365
  halfAngle = (startAngle + angle / 2 + 180) % 360;
11377
11366
  } else if (p1) {
11378
- const parallelAngle = GraphUtils.findAngle(vertex, p1);
11367
+ const parallelAngle = GraphUtils.findAngleDeprecated(vertex, p1);
11379
11368
  halfAngle = parallelAngle + 90;
11380
11369
  } else if (p3) {
11381
- const parallelAngle = GraphUtils.findAngle(p3, vertex);
11370
+ const parallelAngle = GraphUtils.findAngleDeprecated(p3, vertex);
11382
11371
  halfAngle = parallelAngle + 90;
11383
11372
  } else {
11384
11373
  // Standalone point
@@ -11584,7 +11573,7 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
11584
11573
  const vertex = constraints.fixedAngle.vertex.coord || constraints.fixedAngle.vertex;
11585
11574
  const ref = constraints.fixedAngle.ref.coord || constraints.fixedAngle.ref;
11586
11575
  const distPoint = constraints.fixedDistance.point.coord || constraints.fixedDistance.point;
11587
- const constrainedAngle = (constraints.fixedAngle.angle + GraphUtils.findAngle(ref, vertex)) * Math.PI / 180;
11576
+ const constrainedAngle = (constraints.fixedAngle.angle + GraphUtils.findAngleDeprecated(ref, vertex)) * Math.PI / 180;
11588
11577
  const length = constraints.fixedDistance.dist;
11589
11578
  newCoord[0] = length * Math.cos(constrainedAngle) + distPoint[0];
11590
11579
  newCoord[1] = length * Math.sin(constrainedAngle) + distPoint[1];
@@ -11593,8 +11582,8 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
11593
11582
  } else if (typeof constraints.fixedAngle.angle === "number") {
11594
11583
  const vertex = constraints.fixedAngle.vertex.coord || constraints.fixedAngle.vertex;
11595
11584
  const ref = constraints.fixedAngle.ref.coord || constraints.fixedAngle.ref;
11596
- const constrainedAngle = (constraints.fixedAngle.angle + GraphUtils.findAngle(ref, vertex)) * Math.PI / 180;
11597
- const angle = GraphUtils.findAngle(coord, vertex) * Math.PI / 180;
11585
+ const constrainedAngle = (constraints.fixedAngle.angle + GraphUtils.findAngleDeprecated(ref, vertex)) * Math.PI / 180;
11586
+ const angle = GraphUtils.findAngleDeprecated(coord, vertex) * Math.PI / 180;
11598
11587
  const distance = GraphUtils.getDistance(coord, vertex);
11599
11588
  let length = distance * Math.cos(constrainedAngle - angle);
11600
11589
  length = length < 1.0 ? 1.0 : length;
@@ -11604,7 +11593,7 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
11604
11593
  // distance is constrained
11605
11594
  } else if (typeof constraints.fixedDistance.dist === "number") {
11606
11595
  const distPoint = constraints.fixedDistance.point.coord || constraints.fixedDistance.point;
11607
- let angle = GraphUtils.findAngle(coord, distPoint);
11596
+ let angle = GraphUtils.findAngleDeprecated(coord, distPoint);
11608
11597
  const length = constraints.fixedDistance.dist;
11609
11598
  angle = angle * Math.PI / 180;
11610
11599
  newCoord[0] = length * Math.cos(angle) + distPoint[0];
@@ -13804,7 +13793,7 @@ ___default["default"].extend(MovableAngle.prototype, {
13804
13793
  ___default["default"].each([0, 2], function (i) {
13805
13794
  const oldPoint = points[i].coord;
13806
13795
  let newPoint = kmath.vector.add(oldPoint, delta);
13807
- let angle = GraphUtils.findAngle(newVertex, newPoint);
13796
+ let angle = GraphUtils.findAngleDeprecated(newVertex, newPoint);
13808
13797
  angle *= Math.PI / 180;
13809
13798
  newPoint = graphie.constrainToBoundsOnAngle(newPoint, 10, angle);
13810
13799
  newPoints[i] = newPoint;
@@ -13837,7 +13826,7 @@ ___default["default"].extend(MovableAngle.prototype, {
13837
13826
  return false;
13838
13827
  }
13839
13828
  if (snap) {
13840
- let angle = GraphUtils.findAngle(newPoint, vertex);
13829
+ let angle = GraphUtils.findAngleDeprecated(newPoint, vertex);
13841
13830
  angle = Math.round((angle - snapOffset) / snap) * snap + snapOffset;
13842
13831
  const distance = GraphUtils.getDistance(newPoint, vertex);
13843
13832
  return kmath.vector.add(vertex, polar$1(distance, angle));
@@ -13925,7 +13914,7 @@ ___default["default"].extend(MovableAngle.prototype, {
13925
13914
  * coords when interpreted in a clockwise direction.
13926
13915
  */
13927
13916
  _getClockwiseAngle: function (coords) {
13928
- const rawAngle = GraphUtils.findAngle(
13917
+ const rawAngle = GraphUtils.findAngleDeprecated(
13929
13918
  // The order of these is "weird" to match what a clockwise
13930
13919
  // order is in graphie.labelAngle
13931
13920
  coords[2],
@@ -14774,7 +14763,7 @@ const mapObject$1 = function (obj, lambda) {
14774
14763
  * });
14775
14764
  * {a: 'aa', b: 'bb'}
14776
14765
  */
14777
- const mapObjectFromArray$1 = function (arr, lambda) {
14766
+ const mapObjectFromArray = function (arr, lambda) {
14778
14767
  const result = {};
14779
14768
  ___default["default"].each(arr, function (elem) {
14780
14769
  result[elem] = lambda(elem);
@@ -14784,7 +14773,7 @@ const mapObjectFromArray$1 = function (arr, lambda) {
14784
14773
  var objective_ = {
14785
14774
  pluck: pluck,
14786
14775
  mapObject: mapObject$1,
14787
- mapObjectFromArray: mapObjectFromArray$1
14776
+ mapObjectFromArray: mapObjectFromArray
14788
14777
  };
14789
14778
 
14790
14779
  /**
@@ -19000,169 +18989,6 @@ class QuestionParagraph extends React__namespace.Component {
19000
18989
  }
19001
18990
  }
19002
18991
 
19003
- /**
19004
- * TranslationLinter asynchronously calls Khan Academy's poentry linter.
19005
- *
19006
- * The poentry linter checks for issues that prevent content from being
19007
- * translated. This linter is KA-specific and so TranslationLinter does not do
19008
- * anything in non-KA environments.
19009
- */
19010
-
19011
- // A LintCB is called once the linter has been run.
19012
-
19013
- // msec to wait before we actually call the linter after receiving the
19014
- // last call. The timer is reset if the lint is called during the wait.
19015
- const DEBOUNCE_TIMEOUT = 1000;
19016
- class TranslationLinter {
19017
- // To avoid linting when nothing has changed.
19018
- previousContent = null;
19019
-
19020
- /**
19021
- * Runs the poentry linter on the given perseus string asynchronously.
19022
- *
19023
- * It doesn't do anything unless we're in a KA environment.
19024
- *
19025
- * onLintErrorsGenerated is called even if there are no errors. It is not
19026
- * called if perseusStr is unchanged from the last call.
19027
- *
19028
- * Linting is relatively expensive -- so we debounce linting each instance
19029
- * for some amount of time.
19030
- */
19031
- runLinter = _.debounce((perseusStr, onLintErrorsGenerated) => {
19032
- // TODO(FEI-5003): Do we still need to check for 'KA'?
19033
- if (typeof KA === "undefined") {
19034
- return;
19035
- }
19036
- if (perseusStr === this.previousContent) {
19037
- return;
19038
- }
19039
- this.previousContent = perseusStr;
19040
- if (perseusStr === "") {
19041
- onLintErrorsGenerated([]);
19042
- return;
19043
- }
19044
- fetch("/api/internal/translations/lint_poentry?preview=1&lang=en", {
19045
- headers: {
19046
- "Content-Type": "application/json"
19047
- },
19048
- body: JSON.stringify({
19049
- // The poentry linter verifies that the translation from
19050
- // the original text to the translated text is sane. We
19051
- // want to ensure that the translation from English to
19052
- // English is sane, so msgid === mstr.
19053
- msgid: perseusStr,
19054
- // msgid is the original text
19055
- msgstr: perseusStr,
19056
- // msgstr is the translated text
19057
- format: "perseus_text",
19058
- filename: ""
19059
- }),
19060
- method: "POST"
19061
- }).then(response => {
19062
- if (response.status >= 400) {
19063
- return {
19064
- status: "error",
19065
- message: "Could not run i18n linter."
19066
- };
19067
- }
19068
- return response.json();
19069
- }, rejection => {
19070
- return {
19071
- status: "error",
19072
- message: "Could not run i18n linter."
19073
- };
19074
- }).then(json => {
19075
- if (json.status === "error") {
19076
- onLintErrorsGenerated(["Some part of this text makes it untranslatable. " + "The specific message from the i18n linter was: " + json.message.replace(/\n/g, " ")]);
19077
- } else {
19078
- onLintErrorsGenerated([]);
19079
- }
19080
- });
19081
- }, DEBOUNCE_TIMEOUT);
19082
-
19083
- /**
19084
- * Applies an array of errors generated by linters without position
19085
- * information (TranslationLinter and the legacy getSaveWarnings()) to the
19086
- * top of a Perseus tree.
19087
- */
19088
- applyLintErrors(parsedMarkdown, translationLintErrors) {
19089
- // These lint errors do not have position data associated with
19090
- // them, so we just plop them at the top.
19091
- if (translationLintErrors.length) {
19092
- const errorText = translationLintErrors.join("\n\n");
19093
- parsedMarkdown.unshift({
19094
- content: {
19095
- type: "text",
19096
- content: ""
19097
- },
19098
- insideTable: false,
19099
- message: errorText,
19100
- ruleName: "legacy-error",
19101
- severity: PerseusLinter.Rule.Severity.ERROR,
19102
- type: "lint"
19103
- });
19104
- }
19105
- }
19106
- }
19107
-
19108
- /**
19109
- * Preprocess TeX code to convert things that MathJax doesn't know how to handle
19110
- * to things is does.
19111
- */
19112
- var preprocessTex = (texCode => texCode
19113
- // Replace uses of \begin{align}...\end{align} which KaTeX doesn't
19114
- // support (yet) with \begin{aligned}...\end{aligned} which renders
19115
- // the same is supported by KaTeX. It does the same for align*.
19116
- // TODO(kevinb) update content to use aligned instead of align.
19117
- // TODO(LEMS-1608) Remove this replacement as MathJax supports the
19118
- // "align" macro correctly (and, in fact, it is not synonymous with
19119
- // "aligned").
19120
- .replace(/\{align[*]?\}/g, "{aligned}")
19121
- // Replace non-breaking spaces with regular spaces.
19122
- .replace(/[\u00a0]/g, " "));
19123
-
19124
- const {
19125
- interactiveSizes
19126
- } = constants$1;
19127
-
19128
- // Note: these size cutoffs represent content-width cutoffs as specified in
19129
- // http://zpl.io/1mVmvU (broken link - we no longer use Zeplin)
19130
- // TODO(benkomalo): these values aren't used in JS outside of this file, but
19131
- // are coupled to the values in styles/styles.less - DRY it up at some point
19132
- const smMax = articleMaxWidthTableInPx;
19133
- const mdMax = articleMaxWidthInPx;
19134
- const containerSizeClass = {
19135
- SMALL: "small",
19136
- MEDIUM: "medium",
19137
- LARGE: "large",
19138
- XLARGE: "xlarge"
19139
- };
19140
- const getClassFromWidth = width => {
19141
- if (!width) {
19142
- return containerSizeClass.MEDIUM;
19143
- }
19144
- if (width <= smMax) {
19145
- return containerSizeClass.SMALL;
19146
- }
19147
- if (width <= mdMax) {
19148
- return containerSizeClass.MEDIUM;
19149
- }
19150
- return containerSizeClass.LARGE;
19151
- };
19152
- const getInteractiveBoxFromSizeClass = sizeClass => {
19153
- if (sizeClass === containerSizeClass.SMALL) {
19154
- return [interactiveSizes.defaultBoxSizeSmall, interactiveSizes.defaultBoxSizeSmall];
19155
- }
19156
- return [interactiveSizes.defaultBoxSize, interactiveSizes.defaultBoxSize];
19157
- };
19158
-
19159
- var sizingUtils = /*#__PURE__*/Object.freeze({
19160
- __proto__: null,
19161
- containerSizeClass: containerSizeClass,
19162
- getClassFromWidth: getClassFromWidth,
19163
- getInteractiveBoxFromSizeClass: getInteractiveBoxFromSizeClass
19164
- });
19165
-
19166
18992
  const DEFAULT_ALIGNMENT$1 = "block";
19167
18993
  // NOTE(kevinb): "default" is not one in `validAlignments`.
19168
18994
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -19261,6 +19087,9 @@ const getWidget = name => {
19261
19087
  }
19262
19088
  return widgets[name].widget;
19263
19089
  };
19090
+ const getWidgetValidator = name => {
19091
+ return widgets[name]?.validator ?? null;
19092
+ };
19264
19093
  const getEditor = name => {
19265
19094
  return ___default["default"].has(editors, name) ? editors[name] : null;
19266
19095
  };
@@ -19566,6 +19395,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19566
19395
  replaceEditor: replaceEditor,
19567
19396
  replaceDeprecatedEditors: replaceDeprecatedEditors,
19568
19397
  getWidget: getWidget,
19398
+ getWidgetValidator: getWidgetValidator,
19569
19399
  getEditor: getEditor,
19570
19400
  getTransform: getTransform,
19571
19401
  getVersion: getVersion,
@@ -19585,6 +19415,222 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19585
19415
  isLintable: isLintable
19586
19416
  });
19587
19417
 
19418
+ function emptyWidgetsFunctional(widgets,
19419
+ // This is a port of old code, I'm not sure why
19420
+ // we need widgetIds vs the keys of the widgets object
19421
+ widgetIds, userInputMap, strings, locale) {
19422
+ return widgetIds.filter(id => {
19423
+ const widget = widgets[id];
19424
+ if (!widget || widget.static) {
19425
+ // Static widgets shouldn't count as empty
19426
+ return false;
19427
+ }
19428
+ let score = null;
19429
+ const userInput = userInputMap[id];
19430
+ const validator = getWidgetValidator(widget.type);
19431
+ if (widget.type === "group") {
19432
+ const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19433
+ score = Util.flattenScores(scores);
19434
+ } else if (validator) {
19435
+ score = validator(userInput, widget.options, strings, locale);
19436
+ }
19437
+ if (score) {
19438
+ return Util.scoreIsEmpty(score);
19439
+ }
19440
+ });
19441
+ }
19442
+ function scoreWidgetsFunctional(widgets,
19443
+ // This is a port of old code, I'm not sure why
19444
+ // we need widgetIds vs the keys of the widgets object
19445
+ widgetIds, userInputMap, strings, locale) {
19446
+ const gradedWidgetIds = widgetIds.filter(id => {
19447
+ const props = widgets[id];
19448
+ const widgetIsGraded = props?.graded == null || props.graded;
19449
+ const widgetIsStatic = !!props?.static;
19450
+ // Ungraded widgets or widgets set to static shouldn't be graded.
19451
+ return widgetIsGraded && !widgetIsStatic;
19452
+ });
19453
+ const widgetScores = {};
19454
+ gradedWidgetIds.forEach(id => {
19455
+ const widget = widgets[id];
19456
+ if (!widget) {
19457
+ return;
19458
+ }
19459
+ const userInput = userInputMap[id];
19460
+ const validator = getWidgetValidator(widget.type);
19461
+ if (widget.type === "group") {
19462
+ const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19463
+ widgetScores[id] = Util.flattenScores(scores);
19464
+ } else if (validator) {
19465
+ widgetScores[id] = validator(userInput, widget.options, strings, locale);
19466
+ }
19467
+ });
19468
+ return widgetScores;
19469
+ }
19470
+
19471
+ /**
19472
+ * TranslationLinter asynchronously calls Khan Academy's poentry linter.
19473
+ *
19474
+ * The poentry linter checks for issues that prevent content from being
19475
+ * translated. This linter is KA-specific and so TranslationLinter does not do
19476
+ * anything in non-KA environments.
19477
+ */
19478
+
19479
+ // A LintCB is called once the linter has been run.
19480
+
19481
+ // msec to wait before we actually call the linter after receiving the
19482
+ // last call. The timer is reset if the lint is called during the wait.
19483
+ const DEBOUNCE_TIMEOUT = 1000;
19484
+ class TranslationLinter {
19485
+ // To avoid linting when nothing has changed.
19486
+ previousContent = null;
19487
+
19488
+ /**
19489
+ * Runs the poentry linter on the given perseus string asynchronously.
19490
+ *
19491
+ * It doesn't do anything unless we're in a KA environment.
19492
+ *
19493
+ * onLintErrorsGenerated is called even if there are no errors. It is not
19494
+ * called if perseusStr is unchanged from the last call.
19495
+ *
19496
+ * Linting is relatively expensive -- so we debounce linting each instance
19497
+ * for some amount of time.
19498
+ */
19499
+ runLinter = _.debounce((perseusStr, onLintErrorsGenerated) => {
19500
+ // TODO(FEI-5003): Do we still need to check for 'KA'?
19501
+ if (typeof KA === "undefined") {
19502
+ return;
19503
+ }
19504
+ if (perseusStr === this.previousContent) {
19505
+ return;
19506
+ }
19507
+ this.previousContent = perseusStr;
19508
+ if (perseusStr === "") {
19509
+ onLintErrorsGenerated([]);
19510
+ return;
19511
+ }
19512
+ fetch("/api/internal/translations/lint_poentry?preview=1&lang=en", {
19513
+ headers: {
19514
+ "Content-Type": "application/json"
19515
+ },
19516
+ body: JSON.stringify({
19517
+ // The poentry linter verifies that the translation from
19518
+ // the original text to the translated text is sane. We
19519
+ // want to ensure that the translation from English to
19520
+ // English is sane, so msgid === mstr.
19521
+ msgid: perseusStr,
19522
+ // msgid is the original text
19523
+ msgstr: perseusStr,
19524
+ // msgstr is the translated text
19525
+ format: "perseus_text",
19526
+ filename: ""
19527
+ }),
19528
+ method: "POST"
19529
+ }).then(response => {
19530
+ if (response.status >= 400) {
19531
+ return {
19532
+ status: "error",
19533
+ message: "Could not run i18n linter."
19534
+ };
19535
+ }
19536
+ return response.json();
19537
+ }, rejection => {
19538
+ return {
19539
+ status: "error",
19540
+ message: "Could not run i18n linter."
19541
+ };
19542
+ }).then(json => {
19543
+ if (json.status === "error") {
19544
+ onLintErrorsGenerated(["Some part of this text makes it untranslatable. " + "The specific message from the i18n linter was: " + json.message.replace(/\n/g, " ")]);
19545
+ } else {
19546
+ onLintErrorsGenerated([]);
19547
+ }
19548
+ });
19549
+ }, DEBOUNCE_TIMEOUT);
19550
+
19551
+ /**
19552
+ * Applies an array of errors generated by linters without position
19553
+ * information (TranslationLinter and the legacy getSaveWarnings()) to the
19554
+ * top of a Perseus tree.
19555
+ */
19556
+ applyLintErrors(parsedMarkdown, translationLintErrors) {
19557
+ // These lint errors do not have position data associated with
19558
+ // them, so we just plop them at the top.
19559
+ if (translationLintErrors.length) {
19560
+ const errorText = translationLintErrors.join("\n\n");
19561
+ parsedMarkdown.unshift({
19562
+ content: {
19563
+ type: "text",
19564
+ content: ""
19565
+ },
19566
+ insideTable: false,
19567
+ message: errorText,
19568
+ ruleName: "legacy-error",
19569
+ severity: PerseusLinter.Rule.Severity.ERROR,
19570
+ type: "lint"
19571
+ });
19572
+ }
19573
+ }
19574
+ }
19575
+
19576
+ /**
19577
+ * Preprocess TeX code to convert things that MathJax doesn't know how to handle
19578
+ * to things is does.
19579
+ */
19580
+ var preprocessTex = (texCode => texCode
19581
+ // Replace uses of \begin{align}...\end{align} which KaTeX doesn't
19582
+ // support (yet) with \begin{aligned}...\end{aligned} which renders
19583
+ // the same is supported by KaTeX. It does the same for align*.
19584
+ // TODO(kevinb) update content to use aligned instead of align.
19585
+ // TODO(LEMS-1608) Remove this replacement as MathJax supports the
19586
+ // "align" macro correctly (and, in fact, it is not synonymous with
19587
+ // "aligned").
19588
+ .replace(/\{align[*]?\}/g, "{aligned}")
19589
+ // Replace non-breaking spaces with regular spaces.
19590
+ .replace(/[\u00a0]/g, " "));
19591
+
19592
+ const {
19593
+ interactiveSizes
19594
+ } = constants$1;
19595
+
19596
+ // Note: these size cutoffs represent content-width cutoffs as specified in
19597
+ // http://zpl.io/1mVmvU (broken link - we no longer use Zeplin)
19598
+ // TODO(benkomalo): these values aren't used in JS outside of this file, but
19599
+ // are coupled to the values in styles/styles.less - DRY it up at some point
19600
+ const smMax = articleMaxWidthTableInPx;
19601
+ const mdMax = articleMaxWidthInPx;
19602
+ const containerSizeClass = {
19603
+ SMALL: "small",
19604
+ MEDIUM: "medium",
19605
+ LARGE: "large",
19606
+ XLARGE: "xlarge"
19607
+ };
19608
+ const getClassFromWidth = width => {
19609
+ if (!width) {
19610
+ return containerSizeClass.MEDIUM;
19611
+ }
19612
+ if (width <= smMax) {
19613
+ return containerSizeClass.SMALL;
19614
+ }
19615
+ if (width <= mdMax) {
19616
+ return containerSizeClass.MEDIUM;
19617
+ }
19618
+ return containerSizeClass.LARGE;
19619
+ };
19620
+ const getInteractiveBoxFromSizeClass = sizeClass => {
19621
+ if (sizeClass === containerSizeClass.SMALL) {
19622
+ return [interactiveSizes.defaultBoxSizeSmall, interactiveSizes.defaultBoxSizeSmall];
19623
+ }
19624
+ return [interactiveSizes.defaultBoxSize, interactiveSizes.defaultBoxSize];
19625
+ };
19626
+
19627
+ var sizingUtils = /*#__PURE__*/Object.freeze({
19628
+ __proto__: null,
19629
+ containerSizeClass: containerSizeClass,
19630
+ getClassFromWidth: getClassFromWidth,
19631
+ getInteractiveBoxFromSizeClass: getInteractiveBoxFromSizeClass
19632
+ });
19633
+
19588
19634
  class WidgetContainer extends React__namespace.Component {
19589
19635
  widgetRef = /*#__PURE__*/React__namespace.createRef();
19590
19636
  static defaultProps = {
@@ -19731,8 +19777,7 @@ class WidgetContainer extends React__namespace.Component {
19731
19777
  }
19732
19778
 
19733
19779
  const {
19734
- mapObject,
19735
- mapObjectFromArray
19780
+ mapObject
19736
19781
  } = objective_;
19737
19782
  const rContainsNonWhitespace = /\S/;
19738
19783
  const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
@@ -19759,6 +19804,7 @@ const isIdPathPrefix = function (prefixArray, wholeArray) {
19759
19804
  });
19760
19805
  };
19761
19806
  class Renderer extends React__namespace.Component {
19807
+ static contextType = PerseusI18nContext;
19762
19808
  _currentFocus;
19763
19809
  // @ts-expect-error - TS2564 - Property '_foundTextNodes' has no initializer and is not definitely assigned in the constructor.
19764
19810
  _foundTextNodes;
@@ -20062,11 +20108,7 @@ class Renderer extends React__namespace.Component {
20062
20108
  }
20063
20109
  let interactionTracker = this._interactionTrackers[id];
20064
20110
  if (!interactionTracker) {
20065
- interactionTracker = this._interactionTrackers[id] = new InteractionTracker(apiOptions.trackInteraction,
20066
- // @ts-expect-error - TS2345 - Argument of type 'string | null | undefined' is not assignable to parameter of type 'string'.
20067
- widgetInfo && widgetInfo.type, id,
20068
- // @ts-expect-error - TS2345 - Argument of type 'string | null | undefined' is not assignable to parameter of type 'string'.
20069
- getTracking(widgetInfo && widgetInfo.type));
20111
+ interactionTracker = this._interactionTrackers[id] = new InteractionTracker(apiOptions.trackInteraction, widgetInfo && widgetInfo.type, id, getTracking(widgetInfo && widgetInfo.type));
20070
20112
  }
20071
20113
  return {
20072
20114
  ...widgetProps,
@@ -20880,23 +20922,9 @@ class Renderer extends React__namespace.Component {
20880
20922
  }, this);
20881
20923
  return state;
20882
20924
  };
20883
- emptyWidgets = () => {
20884
- // @ts-expect-error - TS2345 - Argument of type '(id: string) => boolean | undefined' is not assignable to parameter of type 'Iteratee<string[], boolean, string>'.
20885
- return ___default["default"].filter(this.widgetIds, id => {
20886
- const widgetInfo = this._getWidgetInfo(id);
20887
- if (widgetInfo.static) {
20888
- // Static widgets shouldn't count as empty
20889
- return false;
20890
- }
20891
- const widget = this.getWidgetInstance(id);
20892
- if (widget && widget.simpleValidate) {
20893
- const score = widget.simpleValidate(widgetInfo.options,
20894
- // @ts-expect-error - TS2345 - Argument of type 'null' is not assignable to parameter of type '((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined'.
20895
- null);
20896
- return Util.scoreIsEmpty(score);
20897
- }
20898
- });
20899
- };
20925
+ emptyWidgets() {
20926
+ return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20927
+ }
20900
20928
  _setWidgetProps = (id, newProps, cb, silent) => {
20901
20929
  this.setState(prevState => {
20902
20930
  const widgetProps = {
@@ -20960,8 +20988,11 @@ class Renderer extends React__namespace.Component {
20960
20988
 
20961
20989
  /**
20962
20990
  * Returns an array of the widget `.getUserInput()` results
20991
+ *
20992
+ * TODO: can we remove this?
20993
+ * @deprecated use getUserInputMap
20963
20994
  */
20964
- getUserInput = () => {
20995
+ getUserInput() {
20965
20996
  return this.widgetIds.map(id => {
20966
20997
  const widget = this.getWidgetInstance(id);
20967
20998
  if (widget && widget.getUserInput) {
@@ -20970,7 +21001,24 @@ class Renderer extends React__namespace.Component {
20970
21001
  return widget.getUserInput();
20971
21002
  }
20972
21003
  });
20973
- };
21004
+ }
21005
+
21006
+ /**
21007
+ * Returns an object of the widget `.getUserInput()` results
21008
+ */
21009
+ getUserInputMap() {
21010
+ const userInputMap = {};
21011
+ this.widgetIds.forEach(id => {
21012
+ const widget = this.getWidgetInstance(id);
21013
+ // Handle Groups, which have their own sets of widgets
21014
+ if (widget?.getUserInputMap) {
21015
+ userInputMap[id] = widget.getUserInputMap();
21016
+ } else if (widget?.getUserInput) {
21017
+ userInputMap[id] = widget.getUserInput();
21018
+ }
21019
+ });
21020
+ return userInputMap;
21021
+ }
20974
21022
 
20975
21023
  /**
20976
21024
  * Returns an array of all widget IDs in the order they occur in
@@ -20980,59 +21028,23 @@ class Renderer extends React__namespace.Component {
20980
21028
  return this.widgetIds;
20981
21029
  };
20982
21030
 
20983
- /**
20984
- * Returns the result of `.getUserInput()` for each widget, in
20985
- * a map from widgetId to userInput.
20986
- * NOTE(jeremy): This function is hauntingly similar to `getUserInput` with
20987
- * the major difference being that this function returns a map of
20988
- * `widgetID` => UserInput and `getUserInput` simply returns an array. It
20989
- * would be trivial to map between the results of each of these functions,
20990
- * so we should aim to remove one of these functions.
20991
- */
20992
- getUserInputForWidgets = () => {
20993
- return mapObjectFromArray(this.widgetIds, id => {
20994
- const widget = this.getWidgetInstance(id);
20995
- if (widget && widget.getUserInput) {
20996
- return widget.getUserInput();
20997
- }
20998
- });
20999
- };
21000
-
21001
21031
  /**
21002
21032
  * Returns an object mapping from widget ID to perseus-style score.
21003
21033
  * The keys of this object are the values of the array returned
21004
21034
  * from `getWidgetIds`.
21005
21035
  */
21006
- scoreWidgets = () => {
21007
- const widgetProps = this.state.widgetInfo;
21008
- const gradedWidgetIds = ___default["default"].filter(this.widgetIds, id => {
21009
- const props = widgetProps[id];
21010
- const widgetIsGraded = props?.graded == null || props.graded;
21011
- const widgetIsStatic = !!props?.static;
21012
- // Ungraded widgets or widgets set to static shouldn't be graded.
21013
- return widgetIsGraded && !widgetIsStatic;
21014
- });
21015
- const widgetScores = {};
21016
- ___default["default"].each(gradedWidgetIds, id => {
21017
- const props = widgetProps[id];
21018
- const widget = this.getWidgetInstance(id);
21019
- // widget can be undefined if it hasn't yet been rendered
21020
- if (widget && widget.simpleValidate) {
21021
- widgetScores[id] = widget.simpleValidate({
21022
- ...props?.options,
21023
- scoring: true
21024
- });
21025
- }
21026
- });
21027
- return widgetScores;
21028
- };
21036
+ scoreWidgets() {
21037
+ return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
21038
+ }
21029
21039
 
21030
21040
  /**
21031
21041
  * Grades the content.
21032
21042
  */
21033
- score = () => {
21034
- return ___default["default"].reduce(this.scoreWidgets(), Util.combineScores, Util.noScore);
21035
- };
21043
+ score() {
21044
+ const scores = this.scoreWidgets();
21045
+ const combinedScore = Util.flattenScores(scores);
21046
+ return combinedScore;
21047
+ }
21036
21048
  guessAndScore = () => {
21037
21049
  const totalGuess = this.getUserInput();
21038
21050
  const totalScore = this.score();
@@ -21144,12 +21156,12 @@ class Renderer extends React__namespace.Component {
21144
21156
  // If highlightLint is true and lint is detected, this call
21145
21157
  // will modify the parse tree by adding lint nodes that will
21146
21158
  // serve to highlight the lint when rendered
21147
- const context = {
21159
+ const fullLinterContext = {
21148
21160
  content: this.props.content,
21149
21161
  widgets: this.props.widgets,
21150
21162
  ...this.props.linterContext
21151
21163
  };
21152
- PerseusLinter__namespace.runLinter(parsedMarkdown, context, true);
21164
+ PerseusLinter__namespace.runLinter(parsedMarkdown, fullLinterContext, true);
21153
21165
 
21154
21166
  // Apply the lint errors from the last TranslationLinter run.
21155
21167
  // TODO(joshuan): Support overlapping dots.
@@ -21971,9 +21983,6 @@ class InputNumber extends React__namespace.Component {
21971
21983
  apiOptions: ApiOptions.defaults,
21972
21984
  linterContext: PerseusLinter.linterContextDefault
21973
21985
  };
21974
- static validate(state, rubric, strings) {
21975
- return inputNumberValidator(state, rubric, strings);
21976
- }
21977
21986
  static getUserInputFromProps(props) {
21978
21987
  return {
21979
21988
  currentValue: props.currentValue
@@ -22044,9 +22053,6 @@ class InputNumber extends React__namespace.Component {
22044
22053
  getUserInput() {
22045
22054
  return InputNumber.getUserInputFromProps(this.props);
22046
22055
  }
22047
- simpleValidate(rubric) {
22048
- return inputNumberValidator(this.getUserInput(), rubric, this.context.strings);
22049
- }
22050
22056
  examples = () => {
22051
22057
  const {
22052
22058
  strings
@@ -22149,7 +22155,8 @@ var InputNumber$1 = {
22149
22155
  hidden: true,
22150
22156
  widget: InputNumber,
22151
22157
  transform: propTransform$3,
22152
- isLintable: true
22158
+ isLintable: true,
22159
+ validator: inputNumberValidator
22153
22160
  };
22154
22161
 
22155
22162
  const ParseTex = TexWrangler.parseTex;
@@ -22356,9 +22363,6 @@ class NumericInput extends React__namespace.Component {
22356
22363
  }
22357
22364
  return answerStrings[0];
22358
22365
  }
22359
- static validate(userInput, rubric, strings) {
22360
- return numericInputValidator(userInput, rubric, strings);
22361
- }
22362
22366
  state = {
22363
22367
  // keeps track of the other set of values when switching
22364
22368
  // between 0 and finite solutions
@@ -22391,9 +22395,6 @@ class NumericInput extends React__namespace.Component {
22391
22395
  const allFormsAccepted = answerFormNames.length >= Object.keys(formExamples).length;
22392
22396
  return !noFormsAccepted && !allFormsAccepted;
22393
22397
  };
22394
- simpleValidate(rubric) {
22395
- return numericInputValidator(this.getUserInput(), rubric, this.context.strings);
22396
- }
22397
22398
  focus = () => {
22398
22399
  this.inputRef?.focus();
22399
22400
  return true;
@@ -22547,7 +22548,8 @@ var NumericInput$1 = {
22547
22548
  accessible: true,
22548
22549
  widget: NumericInput,
22549
22550
  transform: propsTransform,
22550
- isLintable: true
22551
+ isLintable: true,
22552
+ validator: numericInputValidator
22551
22553
  };
22552
22554
 
22553
22555
  /**
@@ -24368,16 +24370,15 @@ class Passage extends React__namespace.Component {
24368
24370
  highlights: {},
24369
24371
  linterContext: PerseusLinter.linterContextDefault
24370
24372
  };
24373
+
24374
+ // this just helps with TS weak typing when a Widget
24375
+ // doesn't implement any Widget methods
24376
+ isWidget = true;
24371
24377
  state = {
24372
24378
  nLines: null,
24373
24379
  startLineNumbersAfter: 0,
24374
24380
  stylesAreApplied: false
24375
24381
  };
24376
-
24377
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24378
- static validate() {
24379
- return noopValidator();
24380
- }
24381
24382
  componentDidMount() {
24382
24383
  this._updateState();
24383
24384
 
@@ -24586,21 +24587,6 @@ class Passage extends React__namespace.Component {
24586
24587
  };
24587
24588
  }
24588
24589
 
24589
- /**
24590
- * Misc functions
24591
- *
24592
- * These are misc widget functions used for the widget API
24593
- */
24594
-
24595
- getUserInput() {
24596
- return null;
24597
- }
24598
-
24599
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24600
- simpleValidate() {
24601
- return noopValidator();
24602
- }
24603
-
24604
24590
  /**
24605
24591
  * Rendering
24606
24592
  *
@@ -24744,7 +24730,8 @@ var Passage$1 = {
24744
24730
  transform: editorProps => {
24745
24731
  return ___default["default"].pick(editorProps, "passageTitle", "passageText", "footnotes", "showLineNumbers");
24746
24732
  },
24747
- isLintable: true
24733
+ isLintable: true,
24734
+ validator: () => noopValidator()
24748
24735
  };
24749
24736
 
24750
24737
  /**
@@ -24774,15 +24761,14 @@ class PassageRef extends React__namespace.Component {
24774
24761
  referenceNumber: 1,
24775
24762
  summaryText: ""
24776
24763
  };
24764
+
24765
+ // this just helps with TS weak typing when a Widget
24766
+ // doesn't implement any Widget methods
24767
+ isWidget = true;
24777
24768
  state = {
24778
24769
  lineRange: null,
24779
24770
  content: null
24780
24771
  };
24781
-
24782
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24783
- static validate() {
24784
- return noopValidator();
24785
- }
24786
24772
  componentDidMount() {
24787
24773
  // TODO(scottgrant): This is a hack to remove the deprecated call to
24788
24774
  // this.isMounted() but is still considered an anti-pattern.
@@ -24801,11 +24787,6 @@ class PassageRef extends React__namespace.Component {
24801
24787
  window.removeEventListener("resize", this._throttledUpdateRange);
24802
24788
  this._isMounted = false;
24803
24789
  }
24804
-
24805
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24806
- getUserInput() {
24807
- return null;
24808
- }
24809
24790
  change = (() => {
24810
24791
  var _this = this;
24811
24792
  return function () {
@@ -24835,11 +24816,6 @@ class PassageRef extends React__namespace.Component {
24835
24816
  }
24836
24817
  }
24837
24818
  };
24838
-
24839
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24840
- simpleValidate() {
24841
- return noopValidator();
24842
- }
24843
24819
  render() {
24844
24820
  const {
24845
24821
  strings
@@ -24888,7 +24864,9 @@ var PassageRef$1 = {
24888
24864
  version: {
24889
24865
  major: 0,
24890
24866
  minor: 1
24891
- }
24867
+ },
24868
+ // TODO: things that aren't interactive shouldn't need validators
24869
+ validator: () => noopValidator()
24892
24870
  };
24893
24871
 
24894
24872
  /**
@@ -26307,9 +26285,6 @@ class Radio$1 extends React__namespace.Component {
26307
26285
  linterContext: PerseusLinter.linterContextDefault,
26308
26286
  showSolutions: "none"
26309
26287
  };
26310
- static validate(userInput, rubric, strings) {
26311
- return radioValidator(userInput, rubric, strings);
26312
- }
26313
26288
  static getUserInputFromProps(props) {
26314
26289
  // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
26315
26290
  // future timeline implementers: this used to be {value: i} before
@@ -26496,9 +26471,6 @@ class Radio$1 extends React__namespace.Component {
26496
26471
  getUserInput() {
26497
26472
  return Radio$1.getUserInputFromProps(this.props);
26498
26473
  }
26499
- simpleValidate(rubric) {
26500
- return radioValidator(this.getUserInput(), rubric, this.context.strings);
26501
- }
26502
26474
 
26503
26475
  /**
26504
26476
  * Turn on rationale display for the currently selected choices. Note that
@@ -26511,7 +26483,7 @@ class Radio$1 extends React__namespace.Component {
26511
26483
  choiceStates
26512
26484
  } = this.props;
26513
26485
  if (choiceStates) {
26514
- const score = this.simpleValidate(rubric);
26486
+ const score = radioValidator(this.getUserInput(), rubric, this.context.strings);
26515
26487
  const widgetCorrect = score.type === "points" && score.total === score.earned;
26516
26488
  const newStates = choiceStates.map(state => ({
26517
26489
  ...state,
@@ -26791,7 +26763,8 @@ var Radio = {
26791
26763
  minor: 0
26792
26764
  },
26793
26765
  propUpgrades: propUpgrades$1,
26794
- isLintable: true
26766
+ isLintable: true,
26767
+ validator: radioValidator
26795
26768
  };
26796
26769
 
26797
26770
  /* eslint-disable import/no-named-as-default */
@@ -26834,9 +26807,6 @@ class Categorizer extends React__namespace.Component {
26834
26807
  state = {
26835
26808
  uniqueId: ___default["default"].uniqueId("perseus_radio_")
26836
26809
  };
26837
- static validate(userInput, rubric, strings) {
26838
- return categorizerValidator(userInput, rubric, strings);
26839
- }
26840
26810
  static getUserInputFromProps(props) {
26841
26811
  return {
26842
26812
  values: props.values
@@ -26862,9 +26832,6 @@ class Categorizer extends React__namespace.Component {
26862
26832
  this.change("values", values);
26863
26833
  this.props.trackInteraction();
26864
26834
  }
26865
- simpleValidate(rubric) {
26866
- return categorizerValidator(this.getUserInput(), rubric, this.context.strings);
26867
- }
26868
26835
  render() {
26869
26836
  const self = this;
26870
26837
 
@@ -27003,7 +26970,8 @@ var Categorizer$1 = {
27003
26970
  staticTransform: editorProps => {
27004
26971
  return ___default["default"].pick(editorProps, "items", "categories", "values", "randomizeItems");
27005
26972
  },
27006
- isLintable: true
26973
+ isLintable: true,
26974
+ validator: categorizerValidator
27007
26975
  };
27008
26976
 
27009
26977
  /**
@@ -27108,11 +27076,6 @@ class CSProgram extends React__namespace.Component {
27108
27076
  // optional message
27109
27077
  message: null
27110
27078
  };
27111
-
27112
- // The widget's grading function
27113
- static validate(state) {
27114
- return csProgramValidator(state);
27115
- }
27116
27079
  componentDidMount() {
27117
27080
  $__default["default"](window).on("message", this.handleMessageEvent);
27118
27081
  }
@@ -27154,9 +27117,6 @@ class CSProgram extends React__namespace.Component {
27154
27117
  message: this.props.message
27155
27118
  };
27156
27119
  }
27157
- simpleValidate() {
27158
- return csProgramValidator(this.getUserInput());
27159
- }
27160
27120
  render() {
27161
27121
  if (!this.props.programID) {
27162
27122
  return /*#__PURE__*/React__namespace.createElement("div", null);
@@ -27231,7 +27191,8 @@ var CSProgram$1 = {
27231
27191
  displayName: "CS Program",
27232
27192
  supportedAlignments: ["block", "full-width"],
27233
27193
  widget: CSProgram,
27234
- hidden: true
27194
+ hidden: true,
27195
+ validator: csProgramValidator
27235
27196
  };
27236
27197
 
27237
27198
  class Definition extends React__namespace.Component {
@@ -27241,18 +27202,9 @@ class Definition extends React__namespace.Component {
27241
27202
  definition: "definition goes here"
27242
27203
  };
27243
27204
 
27244
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27245
- static validate() {
27246
- return noopValidator();
27247
- }
27248
- getUserInput() {
27249
- return {};
27250
- }
27251
-
27252
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27253
- simpleValidate() {
27254
- return noopValidator();
27255
- }
27205
+ // this just helps with TS weak typing when a Widget
27206
+ // doesn't implement any Widget methods
27207
+ isWidget = true;
27256
27208
  render() {
27257
27209
  return /*#__PURE__*/React__namespace.createElement(DefinitionConsumer, null, _ref => {
27258
27210
  let {
@@ -27308,24 +27260,18 @@ var Definition$1 = {
27308
27260
  accessible: true,
27309
27261
  defaultAlignment: "inline",
27310
27262
  widget: Definition,
27311
- transform: x => x
27263
+ transform: x => x,
27264
+ // TODO: things that aren't interactive shouldn't need validators
27265
+ validator: () => noopValidator()
27312
27266
  };
27313
27267
 
27314
27268
  // The props are type `any` on purpose so that this can receive props
27315
27269
  // from any deprecated widget
27316
27270
  class DeprecatedStandin extends React__default["default"].Component {
27317
27271
  static contextType = PerseusI18nContext;
27318
- static validate() {
27319
- // Since this mean to replace an existing widget the learner
27320
- // WILL earn points for this widget
27321
- return noopValidator(1);
27322
- }
27323
- getUserInput() {
27324
- return {};
27325
- }
27326
- simpleValidate() {
27327
- return noopValidator(1);
27328
- }
27272
+ // this just helps with TS weak typing when a Widget
27273
+ // doesn't implement any Widget methods
27274
+ isWidget = true;
27329
27275
  render() {
27330
27276
  return /*#__PURE__*/React__default["default"].createElement("div", {
27331
27277
  style: {
@@ -27343,7 +27289,9 @@ var DeprecatedStandin$1 = {
27343
27289
  name: "deprecated-standin",
27344
27290
  displayName: "Deprecated Standin",
27345
27291
  widget: DeprecatedStandin,
27346
- hidden: true
27292
+ hidden: true,
27293
+ // TODO: things that aren't interactive shouldn't need validators
27294
+ validator: () => noopValidator(1)
27347
27295
  };
27348
27296
 
27349
27297
  function dropdownValidator(userInput, rubric) {
@@ -27370,9 +27318,6 @@ class Dropdown extends React__namespace.Component {
27370
27318
  placeholder: "",
27371
27319
  apiOptions: ApiOptions.defaults
27372
27320
  };
27373
- static validate(userInput, rubric) {
27374
- return dropdownValidator(userInput, rubric);
27375
- }
27376
27321
  focus = () => {
27377
27322
  // TODO(LP-10797): This focus() call doesn't do anything because our
27378
27323
  // root element is a <div> and that cannot be focused without a
@@ -27395,9 +27340,6 @@ class Dropdown extends React__namespace.Component {
27395
27340
  value: this.props.selected
27396
27341
  };
27397
27342
  }
27398
- simpleValidate(rubric) {
27399
- return dropdownValidator(this.getUserInput(), rubric);
27400
- }
27401
27343
  render() {
27402
27344
  const children = [/*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.OptionItem, {
27403
27345
  key: "placeholder",
@@ -27438,7 +27380,8 @@ var Dropdown$1 = {
27438
27380
  defaultAlignment: "inline-block",
27439
27381
  accessible: true,
27440
27382
  widget: Dropdown,
27441
- transform: optionsTransform
27383
+ transform: optionsTransform,
27384
+ validator: dropdownValidator
27442
27385
  };
27443
27386
 
27444
27387
  // transform = _.identity
@@ -27451,10 +27394,6 @@ function mediaQueryIsMatched(mediaQuery) {
27451
27394
  }
27452
27395
  class Explanation extends React__namespace.Component {
27453
27396
  static contextType = PerseusI18nContext;
27454
- state = {
27455
- expanded: false
27456
- };
27457
- _mounted = false;
27458
27397
  static defaultProps = {
27459
27398
  showPrompt: "Explain",
27460
27399
  hidePrompt: "Hide explanation",
@@ -27463,10 +27402,13 @@ class Explanation extends React__namespace.Component {
27463
27402
  linterContext: PerseusLinter.linterContextDefault
27464
27403
  };
27465
27404
 
27466
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27467
- static validate() {
27468
- return noopValidator();
27469
- }
27405
+ // this just helps with TS weak typing when a Widget
27406
+ // doesn't implement any Widget methods
27407
+ isWidget = true;
27408
+ state = {
27409
+ expanded: false
27410
+ };
27411
+ _mounted = false;
27470
27412
  componentDidMount() {
27471
27413
  this._mounted = true;
27472
27414
  }
@@ -27488,14 +27430,6 @@ class Explanation extends React__namespace.Component {
27488
27430
  });
27489
27431
  this.props.trackInteraction();
27490
27432
  };
27491
- getUserInput() {
27492
- return {};
27493
- }
27494
-
27495
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27496
- simpleValidate() {
27497
- return noopValidator();
27498
- }
27499
27433
  render() {
27500
27434
  const promptText = this.state.expanded ? this.props.hidePrompt : this.props.showPrompt;
27501
27435
  const caretIcon = this.state.expanded ? caretUp__default["default"] : caretDown__default["default"];
@@ -27601,7 +27535,9 @@ var Explanation$1 = {
27601
27535
  defaultAlignment: "inline",
27602
27536
  widget: Explanation,
27603
27537
  transform: ___default["default"].identity,
27604
- isLintable: true
27538
+ isLintable: true,
27539
+ // TODO: things that aren't interactive shouldn't need validators
27540
+ validator: () => noopValidator()
27605
27541
  };
27606
27542
 
27607
27543
  class GradedGroupAnswerBar extends React__namespace.Component {
@@ -29133,9 +29069,6 @@ class Grapher extends React__namespace.Component {
29133
29069
  horizHairline;
29134
29070
  vertHairline;
29135
29071
  static defaultProps = DEFAULT_GRAPHER_PROPS;
29136
- static validate(state, rubric) {
29137
- return grapherValidator(state, rubric);
29138
- }
29139
29072
  static getUserInputFromProps(props) {
29140
29073
  return props.plot;
29141
29074
  }
@@ -29238,9 +29171,6 @@ class Grapher extends React__namespace.Component {
29238
29171
  this.vertHairline.hide();
29239
29172
  }
29240
29173
  };
29241
- simpleValidate(rubric) {
29242
- return grapherValidator(this.getUserInput(), rubric);
29243
- }
29244
29174
  getUserInput() {
29245
29175
  return Grapher.getUserInputFromProps(this.props);
29246
29176
  }
@@ -29332,7 +29262,8 @@ var Grapher$1 = {
29332
29262
  hidden: true,
29333
29263
  widget: Grapher,
29334
29264
  transform: propTransform$2,
29335
- staticTransform: staticTransform$4
29265
+ staticTransform: staticTransform$4,
29266
+ validator: grapherValidator
29336
29267
  };
29337
29268
 
29338
29269
  // exports has no 'transform'
@@ -29361,6 +29292,13 @@ class Group extends React__namespace.Component {
29361
29292
  return change.apply(_this, args);
29362
29293
  };
29363
29294
  })();
29295
+ getUserInputMap() {
29296
+ return this.rendererRef?.getUserInputMap();
29297
+ }
29298
+
29299
+ /**
29300
+ * @deprecated getUserInputMap should be used for Groups
29301
+ */
29364
29302
  getUserInput() {
29365
29303
  return this.rendererRef?.getUserInput();
29366
29304
  }
@@ -29373,11 +29311,6 @@ class Group extends React__namespace.Component {
29373
29311
  // (all our changes were in state):
29374
29312
  return null;
29375
29313
  };
29376
- simpleValidate() {
29377
- return this.rendererRef?.score() ?? {
29378
- type: "invalid"
29379
- };
29380
- }
29381
29314
 
29382
29315
  // Mobile API:
29383
29316
  getInputPaths() {
@@ -29527,9 +29460,6 @@ class Iframe extends React__namespace.Component {
29527
29460
  allowFullScreen: false,
29528
29461
  allowTopNavigation: false
29529
29462
  };
29530
- static validate(state) {
29531
- return iframeValidator(state);
29532
- }
29533
29463
  componentDidMount() {
29534
29464
  $__default["default"](window).on("message", this.handleMessageEvent);
29535
29465
  }
@@ -29571,9 +29501,6 @@ class Iframe extends React__namespace.Component {
29571
29501
  return change.apply(_this, args);
29572
29502
  };
29573
29503
  })();
29574
- simpleValidate() {
29575
- return iframeValidator(this.getUserInput());
29576
- }
29577
29504
  render() {
29578
29505
  const style = {
29579
29506
  width: String(this.props.width),
@@ -29641,7 +29568,8 @@ var Iframe$1 = {
29641
29568
  displayName: "Iframe (deprecated)",
29642
29569
  widget: Iframe,
29643
29570
  // Let's not expose it to all content creators yet
29644
- hidden: true
29571
+ hidden: true,
29572
+ validator: iframeValidator
29645
29573
  };
29646
29574
 
29647
29575
  const defaultBoxSize = 400;
@@ -29670,10 +29598,9 @@ class ImageWidget extends React__namespace.Component {
29670
29598
  linterContext: PerseusLinter.linterContextDefault
29671
29599
  };
29672
29600
 
29673
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
29674
- static validate() {
29675
- return noopValidator();
29676
- }
29601
+ // this just helps with TS weak typing when a Widget
29602
+ // doesn't implement any Widget methods
29603
+ isWidget = true;
29677
29604
  change = (() => {
29678
29605
  var _this = this;
29679
29606
  return function () {
@@ -29683,14 +29610,6 @@ class ImageWidget extends React__namespace.Component {
29683
29610
  return change.apply(_this, args);
29684
29611
  };
29685
29612
  })();
29686
- getUserInput() {
29687
- return null;
29688
- }
29689
-
29690
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
29691
- simpleValidate() {
29692
- return noopValidator();
29693
- }
29694
29613
  render() {
29695
29614
  let image;
29696
29615
  const alt = this.props.caption === this.props.alt ? "" : this.props.alt;
@@ -29836,7 +29755,9 @@ var Image$1 = {
29836
29755
  supportedAlignments: editorAlignments,
29837
29756
  displayName: "Image",
29838
29757
  widget: ImageWidget,
29839
- isLintable: true
29758
+ isLintable: true,
29759
+ // TODO: things that aren't interactive shouldn't need validators
29760
+ validator: () => noopValidator()
29840
29761
  };
29841
29762
 
29842
29763
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -29915,10 +29836,9 @@ class Interaction extends React__namespace.Component {
29915
29836
  elements: []
29916
29837
  };
29917
29838
 
29918
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
29919
- static validate() {
29920
- return noopValidator();
29921
- }
29839
+ // this just helps with TS weak typing when a Widget
29840
+ // doesn't implement any Widget methods
29841
+ isWidget = true;
29922
29842
  state = {
29923
29843
  variables: _getInitialVariables(this.props.elements),
29924
29844
  functions: _getInitialFunctions(this.props.elements)
@@ -30026,18 +29946,6 @@ class Interaction extends React__namespace.Component {
30026
29946
  return change.apply(_this, args);
30027
29947
  };
30028
29948
  })();
30029
- getUserInput() {
30030
- // TODO(eater): Perhaps we want to be able to record the state of the
30031
- // user's interaction. Unfortunately sending all the props will
30032
- // probably make the attempt payload too large. So for now, don't send
30033
- // anything.
30034
- return {};
30035
- }
30036
-
30037
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
30038
- simpleValidate() {
30039
- return noopValidator();
30040
- }
30041
29949
  render() {
30042
29950
  const range = this.props.graph.range;
30043
29951
  let labels = this.props.graph.labels;
@@ -30445,7 +30353,9 @@ var Interactive = {
30445
30353
  displayName: "Interaction",
30446
30354
  widget: Interaction,
30447
30355
  transform: ___default["default"].identity,
30448
- hidden: true
30356
+ hidden: true,
30357
+ // TODO: things that aren't interactive shouldn't need validators
30358
+ validator: () => noopValidator()
30449
30359
  };
30450
30360
 
30451
30361
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -30975,16 +30885,40 @@ function polar(r, th) {
30975
30885
  th = th * Math.PI / 180;
30976
30886
  return [r[0] * Math.cos(th), r[1] * Math.sin(th)];
30977
30887
  }
30978
- const findAngle = (point1, point2, vertex) => {
30979
- if (vertex === undefined) {
30980
- const x = point1[0] - point2[0];
30981
- const y = point1[1] - point2[1];
30982
- if (!x && !y) {
30983
- return 0;
30984
- }
30985
- return (180 + Math.atan2(-y, -x) * 180 / Math.PI + 360) % 360;
30888
+ // This function calculates the angle between two points and an optional vertex.
30889
+ // If the vertex is not provided, the angle is measured between the two points.
30890
+ // This does not account for reflex angles or clockwise position.
30891
+ const getAngleFromVertex = (point, vertex) => {
30892
+ const x = point[0] - vertex[0];
30893
+ const y = point[1] - vertex[1];
30894
+ if (!x && !y) {
30895
+ return 0;
30986
30896
  }
30987
- return findAngle(point1, vertex) - findAngle(point2, vertex);
30897
+ return (180 + Math.atan2(-y, -x) * 180 / Math.PI + 360) % 360;
30898
+ };
30899
+
30900
+ // This function calculates the clockwise angle between three points,
30901
+ // and is used to generate the labels and equation strings of the
30902
+ // current angle for the interactive graph.
30903
+ const getClockwiseAngle = function (coords) {
30904
+ let allowReflexAngles = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
30905
+ const coordsCopy = [...coords];
30906
+ // The coords are saved as [point1, vertex, point2] in the interactive graph
30907
+ const areClockwise = clockwise$1([coordsCopy[0], coordsCopy[2], coordsCopy[1]]);
30908
+
30909
+ // We may need to reverse the coordinates if we allow
30910
+ // reflex angles and the points are not in clockwise order.
30911
+ const shouldReverseCoords = !areClockwise || allowReflexAngles;
30912
+
30913
+ // Reverse the coordinates accordingly to ensure the angle is calculated correctly
30914
+ const clockwiseCoords = shouldReverseCoords ? coordsCopy.reverse() : coordsCopy;
30915
+
30916
+ // Calculate the angles between the two points and get the difference
30917
+ // between the two angles to get the clockwise angle.
30918
+ const startAngle = getAngleFromVertex(clockwiseCoords[0], clockwiseCoords[1]);
30919
+ const endAngle = getAngleFromVertex(clockwiseCoords[2], clockwiseCoords[1]);
30920
+ const angle = (startAngle + 360 - endAngle) % 360;
30921
+ return angle;
30988
30922
  };
30989
30923
 
30990
30924
  const defaultGraphConfig = {
@@ -33416,8 +33350,8 @@ const Angle = _ref2 => {
33416
33350
  const clockwiseCoords = shouldReverseCoords ? coords : coords.reverse();
33417
33351
 
33418
33352
  // Calculate the angles between the two points
33419
- const startAngle = findAngle(clockwiseCoords[0], vertex);
33420
- const endAngle = findAngle(clockwiseCoords[1], vertex);
33353
+ const startAngle = getAngleFromVertex(clockwiseCoords[0], vertex);
33354
+ const endAngle = getAngleFromVertex(clockwiseCoords[1], vertex);
33421
33355
  const angle = (startAngle + 360 - endAngle) % 360;
33422
33356
 
33423
33357
  // Check if the angle is reflexive
@@ -34506,7 +34440,9 @@ const MafsGraph = props => {
34506
34440
  analytics.onAnalyticsEvent({
34507
34441
  type: "perseus:interactive-graph-widget:rendered",
34508
34442
  payload: {
34509
- type
34443
+ type,
34444
+ widgetType: "INTERACTIVE_GRAPH",
34445
+ widgetId: "interactive-graph"
34510
34446
  }
34511
34447
  });
34512
34448
  });
@@ -35793,7 +35729,7 @@ function boundAndSnapAngleVertex(_ref2, _ref3) {
35793
35729
  for (const i of [0, 2]) {
35794
35730
  const oldPoint = coordsCopy[i];
35795
35731
  let newPoint = mafs.vec.add(oldPoint, delta);
35796
- let angle = findAngle(newVertex, newPoint);
35732
+ let angle = getAngleFromVertex(newVertex, newPoint);
35797
35733
  angle *= Math.PI / 180;
35798
35734
  newPoint = constrainToBoundsOnAngle(newPoint, angle, range, snapStep);
35799
35735
  newPoints[i] = newPoint;
@@ -35866,7 +35802,7 @@ function boundAndSnapAngleEndPoints(destinationPoint, _ref5, index) {
35866
35802
  const vertex = coords[1];
35867
35803
 
35868
35804
  // Gets the angle between the coords and the vertex
35869
- let angle = findAngle(coordsCopy[index], vertex);
35805
+ let angle = getAngleFromVertex(coordsCopy[index], vertex);
35870
35806
 
35871
35807
  // Snap the angle to the nearest multiple of snapDegrees (if provided)
35872
35808
  angle = Math.round((angle - offsetDegrees) / snap) * snap + offsetDegrees;
@@ -35913,8 +35849,8 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
35913
35849
  angles[rel(j)] = Math.round(angles[rel(j)]);
35914
35850
  });
35915
35851
  const getAngle = function (a, vertex, b) {
35916
- const angle = findAngle(coordsCopy[rel(a)], coordsCopy[rel(b)], coordsCopy[rel(vertex)]);
35917
- return (angle + 360) % 360;
35852
+ const angle = getClockwiseAngle([coordsCopy[rel(a)], coordsCopy[rel(vertex)], coordsCopy[rel(b)]]);
35853
+ return angle;
35918
35854
  };
35919
35855
  const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
35920
35856
  innerAngles[2] = 180 - (innerAngles[0] + innerAngles[1]);
@@ -35941,7 +35877,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
35941
35877
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
35942
35878
 
35943
35879
  // Angle at the second vertex of the polygon
35944
- const outerAngle = findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
35880
+ const outerAngle = getAngleFromVertex(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
35945
35881
 
35946
35882
  // Uses the length of the side of the polygon (radial coordinate)
35947
35883
  // and the angle between the first and second sides of the
@@ -35992,7 +35928,7 @@ function boundAndSnapToSides(destinationPoint, _ref7, index) {
35992
35928
  const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
35993
35929
 
35994
35930
  // Angle at the second vertex of the polygon
35995
- const outerAngle = findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
35931
+ const outerAngle = getAngleFromVertex(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
35996
35932
 
35997
35933
  // Returns true if the points form a counter-clockwise turn;
35998
35934
  // a.k.a if the point is on the left or right of the polygon.
@@ -36101,7 +36037,8 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
36101
36037
  const numPoints = graph.type === "point" ? graph.numPoints : null;
36102
36038
  const numSides = graph.type === "polygon" ? graph.numSides : null;
36103
36039
  const snapTo = graph.type === "polygon" ? graph.snapTo : null;
36104
- const showAngles = graph.type === "polygon" ? graph.showAngles : null;
36040
+ const showAngles = graph.type === "polygon" || graph.type === "angle" ? graph.showAngles : null;
36041
+ const allowReflexAngles = graph.type === "angle" ? graph.allowReflexAngles : null;
36105
36042
  const showSides = graph.type === "polygon" ? graph.showSides : null;
36106
36043
  const startCoords = "startCoords" in graph ? graph.startCoords : undefined;
36107
36044
  const originalPropsRef = React.useRef(props);
@@ -36114,7 +36051,7 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
36114
36051
  if (latestPropsRef.current !== originalPropsRef.current) {
36115
36052
  dispatch(reinitialize(latestPropsRef.current));
36116
36053
  }
36117
- }, [graph.type, numPoints, numSegments, numSides, snapTo, showAngles, showSides, latestPropsRef, startCoords]);
36054
+ }, [graph.type, numPoints, numSegments, numSides, snapTo, showAngles, showSides, latestPropsRef, startCoords, allowReflexAngles]);
36118
36055
 
36119
36056
  // If the graph is static, it always displays the correct answer. This is
36120
36057
  // standard behavior for Perseus widgets (e.g. compare the Radio widget).
@@ -36302,17 +36239,15 @@ function interactiveGraphValidator(userInput, rubric) {
36302
36239
  } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
36303
36240
  const guess = userInput.coords;
36304
36241
  const correct = rubric.correct.coords;
36242
+ const allowReflexAngles = rubric.correct.allowReflexAngles;
36305
36243
  let match;
36306
36244
  if (rubric.correct.match === "congruent") {
36307
36245
  const angles = ___default["default"].map([guess, correct], function (coords) {
36308
- const angle = GraphUtils.findAngle(
36309
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
36310
- coords[2],
36311
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
36312
- coords[0],
36313
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'.
36314
- coords[1]);
36315
- return (angle + 360) % 360;
36246
+ if (!coords) {
36247
+ return false;
36248
+ }
36249
+ const angle = getClockwiseAngle(coords, allowReflexAngles);
36250
+ return angle;
36316
36251
  });
36317
36252
  // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
36318
36253
  match = eq$1(...angles);
@@ -37215,7 +37150,7 @@ class LegacyInteractiveGraph extends React__namespace.Component {
37215
37150
  angles[rel(j)] = Math.round(angles[rel(j)]);
37216
37151
  });
37217
37152
  const getAngle = function (a, vertex, b) {
37218
- const angle = GraphUtils.findAngle(coords[rel(a)], coords[rel(b)], coords[rel(vertex)]);
37153
+ const angle = GraphUtils.findAngleDeprecated(coords[rel(a)], coords[rel(b)], coords[rel(vertex)]);
37219
37154
  return (angle + 360) % 360;
37220
37155
  };
37221
37156
  const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
@@ -37234,7 +37169,7 @@ class LegacyInteractiveGraph extends React__namespace.Component {
37234
37169
 
37235
37170
  // Solve for side by using the law of sines
37236
37171
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
37237
- const outerAngle = GraphUtils.findAngle(coords[rel(1)], coords[rel(-1)]);
37172
+ const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
37238
37173
  const offset = polar$1(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
37239
37174
  return this.graphie?.addPoints(coords[rel(-1)], offset);
37240
37175
  }
@@ -37257,7 +37192,7 @@ class LegacyInteractiveGraph extends React__namespace.Component {
37257
37192
 
37258
37193
  // Solve for angle by using the law of cosines
37259
37194
  const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
37260
- const outerAngle = GraphUtils.findAngle(coords[rel(1)], coords[rel(-1)]);
37195
+ const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
37261
37196
  const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
37262
37197
  const offset = polar$1(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
37263
37198
  return this.graphie?.addPoints(coords[rel(-1)], offset);
@@ -37577,9 +37512,6 @@ class LegacyInteractiveGraph extends React__namespace.Component {
37577
37512
  getUserInput() {
37578
37513
  return InteractiveGraph.getUserInputFromProps(this.props);
37579
37514
  }
37580
- simpleValidate(rubric) {
37581
- return interactiveGraphValidator(this.getUserInput(), rubric);
37582
- }
37583
37515
  focus = $__default["default"].noop;
37584
37516
  render() {
37585
37517
  const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
@@ -37652,9 +37584,6 @@ class InteractiveGraph extends React__namespace.Component {
37652
37584
  }
37653
37585
  throw new perseusCore.PerseusError("Cannot getUserInput from a graph that has never rendered", perseusCore.Errors.NotAllowed);
37654
37586
  }
37655
- simpleValidate(rubric) {
37656
- return interactiveGraphValidator(this.getUserInput(), rubric);
37657
- }
37658
37587
  render() {
37659
37588
  // Mafs shim
37660
37589
  const mafsFlags = this.props.apiOptions?.flags?.["mafs"];
@@ -37981,12 +37910,10 @@ class InteractiveGraph extends React__namespace.Component {
37981
37910
  throw makeInvalidTypeError("getAngleEquationString", "angle");
37982
37911
  }
37983
37912
  const coords = InteractiveGraph.getAngleCoords(props.graph, props);
37984
- const angle = GraphUtils.findAngle(coords[2], coords[0], coords[1]);
37913
+ const allowReflexAngles = props.graph.allowReflexAngles;
37914
+ const angle = getClockwiseAngle(coords, allowReflexAngles);
37985
37915
  return angle.toFixed(0) + "\u00B0 angle" + " at (" + coords[1].join(", ") + ")";
37986
37916
  }
37987
- static validate(userInput, rubric) {
37988
- return interactiveGraphValidator(userInput, rubric);
37989
- }
37990
37917
  static getUserInputFromProps(props) {
37991
37918
  return props.graph;
37992
37919
  }
@@ -38023,7 +37950,8 @@ var InteractiveGraph$1 = {
38023
37950
  name: "interactive-graph",
38024
37951
  displayName: "Interactive graph (Assessments only)",
38025
37952
  widget: InteractiveGraph,
38026
- staticTransform: staticTransform$3
37953
+ staticTransform: staticTransform$3,
37954
+ validator: interactiveGraphValidator
38027
37955
  };
38028
37956
 
38029
37957
  const black = "#000000";
@@ -39399,9 +39327,6 @@ class LabelImage extends React__namespace.Component {
39399
39327
  // The rendered markers on the question image for labeling.
39400
39328
  _markers;
39401
39329
  _mounted = false;
39402
- static validate(state, rubric) {
39403
- return labelImageValidator(state);
39404
- }
39405
39330
 
39406
39331
  /**
39407
39332
  * Test whether point is contained within triangle.
@@ -39594,9 +39519,6 @@ class LabelImage extends React__namespace.Component {
39594
39519
  componentWillUnmount() {
39595
39520
  this._mounted = false;
39596
39521
  }
39597
- simpleValidate(rubric) {
39598
- return labelImageValidator(this.getUserInput());
39599
- }
39600
39522
  getUserInput() {
39601
39523
  const {
39602
39524
  markers
@@ -39962,19 +39884,13 @@ const LabelImageWithDependencies = /*#__PURE__*/React__namespace.forwardRef((pro
39962
39884
  analytics: deps.analytics
39963
39885
  }, props));
39964
39886
  });
39965
-
39966
- // HACK: Propogate "static" methods onto our wrapper component.
39967
- // In the future we should adjust client apps to not depend on these static
39968
- // methods and instead adjust Peresus to provide these facilities through
39969
- // instance methods on our Renderers.
39970
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type
39971
- LabelImageWithDependencies.validate = labelImageValidator;
39972
39887
  var LabelImage$1 = {
39973
39888
  name: "label-image",
39974
39889
  displayName: "Label Image",
39975
39890
  widget: LabelImageWithDependencies,
39976
39891
  accessible: true,
39977
- isLintable: true
39892
+ isLintable: true,
39893
+ validator: labelImageValidator
39978
39894
  };
39979
39895
 
39980
39896
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -40716,9 +40632,6 @@ class Matcher extends React__namespace.Component {
40716
40632
  rightHeight: 0,
40717
40633
  texRendererLoaded: false
40718
40634
  };
40719
- static validate(state, rubric) {
40720
- return matcherValidator(state, rubric);
40721
- }
40722
40635
  changeAndTrack = e => {
40723
40636
  this.props.onChange(e);
40724
40637
  this.props.trackInteraction();
@@ -40770,9 +40683,6 @@ class Matcher extends React__namespace.Component {
40770
40683
  // @ts-expect-error - TS2339 - Property 'moveOptionToIndex' does not exist on type 'ReactInstance'.
40771
40684
  this.refs.right.moveOptionToIndex(option, index);
40772
40685
  };
40773
- simpleValidate(rubric) {
40774
- return matcherValidator(this.getUserInput(), rubric);
40775
- }
40776
40686
  render() {
40777
40687
  // To minimize layout shift, we display a spinner until our math
40778
40688
  // renderer is ready to render the math inside the matcher. To
@@ -40899,7 +40809,8 @@ var Matcher$1 = {
40899
40809
  name: "matcher",
40900
40810
  displayName: "Matcher (two column)",
40901
40811
  widget: Matcher,
40902
- isLintable: true
40812
+ isLintable: true,
40813
+ validator: matcherValidator
40903
40814
  };
40904
40815
 
40905
40816
  function matrixValidator(state, rubric, strings) {
@@ -41040,9 +40951,6 @@ class Matrix extends React__namespace.Component {
41040
40951
  apiOptions: ApiOptions.defaults,
41041
40952
  linterContext: PerseusLinter.linterContextDefault
41042
40953
  };
41043
- static validate(state, rubric, strings) {
41044
- return matrixValidator(state, rubric, strings);
41045
- }
41046
40954
  state = {
41047
40955
  enterTheMatrix: 0
41048
40956
  };
@@ -41185,9 +41093,6 @@ class Matrix extends React__namespace.Component {
41185
41093
  answers: this.props.answers
41186
41094
  };
41187
41095
  }
41188
- simpleValidate(rubric) {
41189
- return matrixValidator(this.getUserInput(), rubric, this.context.strings);
41190
- }
41191
41096
  render() {
41192
41097
  // Set the input sizes through JS so we can control the size of the
41193
41098
  // brackets. (If we set them in CSS we won't know values until the
@@ -41372,7 +41277,8 @@ var Matrix$1 = {
41372
41277
  widget: Matrix,
41373
41278
  transform: propTransform$1,
41374
41279
  staticTransform: staticTransform$2,
41375
- isLintable: true
41280
+ isLintable: true,
41281
+ validator: matrixValidator
41376
41282
  };
41377
41283
 
41378
41284
  const defaultImage = {
@@ -41381,7 +41287,6 @@ const defaultImage = {
41381
41287
  left: 0
41382
41288
  };
41383
41289
  class Measurer extends React__namespace.Component {
41384
- static validate = noopValidator;
41385
41290
  static defaultProps = {
41386
41291
  box: [480, 480],
41387
41292
  image: {},
@@ -41394,6 +41299,10 @@ class Measurer extends React__namespace.Component {
41394
41299
  rulerPixels: 40,
41395
41300
  rulerLength: 10
41396
41301
  };
41302
+
41303
+ // this just helps with TS weak typing when a Widget
41304
+ // doesn't implement any Widget methods
41305
+ isWidget = true;
41397
41306
  state = {};
41398
41307
  ruler;
41399
41308
  protractor;
@@ -41446,12 +41355,6 @@ class Measurer extends React__namespace.Component {
41446
41355
  });
41447
41356
  }
41448
41357
  }
41449
- getUserInput() {
41450
- return {};
41451
- }
41452
- simpleValidate() {
41453
- return noopValidator(1);
41454
- }
41455
41358
  render() {
41456
41359
  const image = ___default["default"].extend({}, defaultImage, this.props.image);
41457
41360
 
@@ -41498,7 +41401,9 @@ var Measurer$1 = {
41498
41401
  major: 1,
41499
41402
  minor: 0
41500
41403
  },
41501
- propUpgrades: propUpgrades
41404
+ propUpgrades: propUpgrades,
41405
+ // TODO: things that aren't interactive shouldn't need validators
41406
+ validator: () => noopValidator(1)
41502
41407
  };
41503
41408
 
41504
41409
  /**
@@ -42371,18 +42276,9 @@ class MoleculeWidget extends React__namespace.Component {
42371
42276
  rotationAngle: 0
42372
42277
  };
42373
42278
 
42374
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
42375
- simpleValidate() {
42376
- return noopValidator();
42377
- }
42378
- getUserInput() {
42379
- return [];
42380
- }
42381
-
42382
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
42383
- validate() {
42384
- return noopValidator();
42385
- }
42279
+ // this just helps with TS weak typing when a Widget
42280
+ // doesn't implement any Widget methods
42281
+ isWidget = true;
42386
42282
  render() {
42387
42283
  return /*#__PURE__*/React__namespace.createElement(Molecule, {
42388
42284
  widgetId: this.props.widgetId,
@@ -42395,7 +42291,8 @@ var Molecule$1 = {
42395
42291
  name: "molecule-renderer",
42396
42292
  displayName: "Molecule renderer",
42397
42293
  hidden: true,
42398
- widget: MoleculeWidget
42294
+ widget: MoleculeWidget,
42295
+ validator: () => noopValidator()
42399
42296
  };
42400
42297
 
42401
42298
  function numberLineValidator(state, rubric) {
@@ -42540,18 +42437,16 @@ const TickMarks = Graphie.createSimpleClass((graphie, props) => {
42540
42437
  results.push(graphie.line([x, -0.2], [x, 0.2]));
42541
42438
  const labelTicks = props.labelTicks;
42542
42439
  if (labelTicks || props.labelStyle === "decimal ticks") {
42543
- results.push(_label(graphie, props.labelStyle, x, x, base));
42440
+ if (x === leftLabel || x === rightLabel) {
42441
+ results.push(graphie.style({
42442
+ color: KhanColors.BLUE
42443
+ }, () => _label(graphie, props.labelStyle, x, x, base)));
42444
+ } else {
42445
+ results.push(_label(graphie, props.labelStyle, x, x, base));
42446
+ }
42544
42447
  }
42545
42448
  }
42546
42449
 
42547
- // Render the text labels
42548
- results.push(graphie.style(props.isMobile ? {
42549
- color: KhanColors.BLUE
42550
- } : {}, () => _label(graphie, props.labelStyle, leftLabel, leftLabel, base)));
42551
- results.push(graphie.style(props.isMobile ? {
42552
- color: KhanColors.BLUE
42553
- } : {}, () => _label(graphie, props.labelStyle, rightLabel, rightLabel, base)));
42554
-
42555
42450
  // Render the labels' lines
42556
42451
  graphie.style({
42557
42452
  stroke: KhanColors.BLUE,
@@ -42584,9 +42479,6 @@ class NumberLine extends React__namespace.Component {
42584
42479
  state = {
42585
42480
  numDivisionsEmpty: false
42586
42481
  };
42587
- static validate(state, rubric) {
42588
- return numberLineValidator(state, rubric);
42589
- }
42590
42482
  change = (() => {
42591
42483
  var _this = this;
42592
42484
  return function () {
@@ -42879,9 +42771,6 @@ class NumberLine extends React__namespace.Component {
42879
42771
  divisionRange: this.props.divisionRange
42880
42772
  };
42881
42773
  }
42882
- simpleValidate(rubric) {
42883
- return numberLineValidator(this.getUserInput(), rubric);
42884
- }
42885
42774
  render() {
42886
42775
  const {
42887
42776
  strings
@@ -42985,7 +42874,8 @@ var NumberLine$1 = {
42985
42874
  displayName: "Number line",
42986
42875
  widget: NumberLine,
42987
42876
  transform: numberLineTransform,
42988
- staticTransform: staticTransform$1
42877
+ staticTransform: staticTransform$1,
42878
+ validator: numberLineValidator
42989
42879
  };
42990
42880
 
42991
42881
  function ordererValidator(userInput, rubric) {
@@ -43195,9 +43085,6 @@ class Orderer extends React__namespace.Component {
43195
43085
  animateTo: null,
43196
43086
  grabPos: null
43197
43087
  };
43198
- static validate(userInput, rubric) {
43199
- return ordererValidator(userInput, rubric);
43200
- }
43201
43088
  UNSAFE_componentWillReceiveProps(nextProps) {
43202
43089
  if (!___default["default"].isEqual(this.props.current, nextProps.current)) {
43203
43090
  this.setState({
@@ -43437,9 +43324,6 @@ class Orderer extends React__namespace.Component {
43437
43324
  })
43438
43325
  };
43439
43326
  }
43440
- simpleValidate(rubric) {
43441
- return ordererValidator(this.getUserInput(), rubric);
43442
- }
43443
43327
  render() {
43444
43328
  // This is the card we are currently dragging
43445
43329
  const dragging = this.state.dragging && /*#__PURE__*/React__namespace.createElement(Card
@@ -43549,7 +43433,8 @@ var Orderer$1 = {
43549
43433
  displayName: "Orderer",
43550
43434
  hidden: true,
43551
43435
  widget: Orderer,
43552
- isLintable: true
43436
+ isLintable: true,
43437
+ validator: ordererValidator
43553
43438
  };
43554
43439
 
43555
43440
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -43560,15 +43445,9 @@ class PassageRefTarget extends React__namespace.Component {
43560
43445
  linterContext: PerseusLinter.linterContextDefault
43561
43446
  };
43562
43447
 
43563
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43564
- static validate() {
43565
- return noopValidator();
43566
- }
43567
-
43568
- // TODO passage-ref-target isn't interactive; remove
43569
- getUserInput() {
43570
- return null;
43571
- }
43448
+ // this just helps with TS weak typing when a Widget
43449
+ // doesn't implement any Widget methods
43450
+ isWidget = true;
43572
43451
 
43573
43452
  // TODO passage-ref-target isn't interactive; remove
43574
43453
  change = (() => {
@@ -43580,11 +43459,6 @@ class PassageRefTarget extends React__namespace.Component {
43580
43459
  return change.apply(_this, args);
43581
43460
  };
43582
43461
  })();
43583
-
43584
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43585
- simpleValidate() {
43586
- return noopValidator();
43587
- }
43588
43462
  render() {
43589
43463
  return /*#__PURE__*/React__namespace.createElement(Renderer, {
43590
43464
  content: this.props.content
@@ -43613,7 +43487,9 @@ var PassageRefTarget$1 = {
43613
43487
  major: 0,
43614
43488
  minor: 0
43615
43489
  },
43616
- isLintable: true
43490
+ isLintable: true,
43491
+ // TODO: things that aren't interactive shouldn't need validators
43492
+ validator: () => noopValidator()
43617
43493
  };
43618
43494
 
43619
43495
  /**
@@ -43625,6 +43501,10 @@ class PhetSimulation extends React__namespace.Component {
43625
43501
  static contextType = PerseusI18nContext;
43626
43502
  iframeRef = /*#__PURE__*/React__namespace.createRef();
43627
43503
  locale;
43504
+
43505
+ // this just helps with TS weak typing when a Widget
43506
+ // doesn't implement any Widget methods
43507
+ isWidget = true;
43628
43508
  state = {
43629
43509
  url: null,
43630
43510
  banner: null
@@ -43643,11 +43523,6 @@ class PhetSimulation extends React__namespace.Component {
43643
43523
  }
43644
43524
  }
43645
43525
 
43646
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43647
- getUserInput() {
43648
- return null;
43649
- }
43650
-
43651
43526
  // kaLocales and PhET locales use different formats and abbreviations.
43652
43527
  // PhET accepts different formats, i.e. kaLocale's hyphens, but it does not accept
43653
43528
  // different abbreviations, so in points of divergence of abbreviations, we need to
@@ -43867,9 +43742,6 @@ class Plotter extends React__namespace.Component {
43867
43742
  // bottom label.
43868
43743
  categoryHeights: {}
43869
43744
  };
43870
- static validate(userInput, rubric) {
43871
- return plotterValidator(userInput, rubric);
43872
- }
43873
43745
  componentDidMount() {
43874
43746
  this._isMounted = true;
43875
43747
  this.setupGraphie(this.state);
@@ -44614,9 +44486,6 @@ class Plotter extends React__namespace.Component {
44614
44486
  getUserInput() {
44615
44487
  return this.state.values;
44616
44488
  }
44617
- simpleValidate(rubric) {
44618
- return plotterValidator(this.getUserInput(), rubric);
44619
- }
44620
44489
  render() {
44621
44490
  // TODO(kevinb) actually compute the size of the graphie correctly and
44622
44491
  // make it that size so we don't have to add extra padding. The value
@@ -44642,7 +44511,8 @@ var Plotter$1 = {
44642
44511
  displayName: "Plotter",
44643
44512
  hidden: true,
44644
44513
  widget: Plotter,
44645
- staticTransform: staticTransform
44514
+ staticTransform: staticTransform,
44515
+ validator: plotterValidator
44646
44516
  };
44647
44517
 
44648
44518
  /**
@@ -44737,9 +44607,6 @@ class Sorter extends React__namespace.Component {
44737
44607
  onChange: function () {},
44738
44608
  linterContext: PerseusLinter.linterContextDefault
44739
44609
  };
44740
- static validate(userInput, rubric) {
44741
- return sorterValidator(userInput, rubric);
44742
- }
44743
44610
  state = {
44744
44611
  changed: false
44745
44612
  };
@@ -44782,9 +44649,6 @@ class Sorter extends React__namespace.Component {
44782
44649
  // @ts-expect-error - TS2339 - Property 'moveOptionToIndex' does not exist on type 'ReactInstance'.
44783
44650
  this.refs.sortable.moveOptionToIndex(option, index);
44784
44651
  };
44785
- simpleValidate(rubric) {
44786
- return sorterValidator(this.getUserInput(), rubric);
44787
- }
44788
44652
  render() {
44789
44653
  const options = shuffle(this.props.correct, this.props.problemNum, /* ensurePermuted */true);
44790
44654
  const {
@@ -44810,7 +44674,8 @@ var Sorter$1 = {
44810
44674
  name: "sorter",
44811
44675
  displayName: "Sorter",
44812
44676
  widget: Sorter,
44813
- isLintable: true
44677
+ isLintable: true,
44678
+ validator: sorterValidator
44814
44679
  };
44815
44680
 
44816
44681
  function tableValidator(state, rubric, strings) {
@@ -44918,9 +44783,6 @@ class Table extends React__namespace.Component {
44918
44783
  linterContext: PerseusLinter.linterContextDefault
44919
44784
  };
44920
44785
  }();
44921
- static validate(state, rubric, strings) {
44922
- return tableValidator(state, rubric, strings);
44923
- }
44924
44786
  _getRows = () => {
44925
44787
  return this.props.answers.length;
44926
44788
  };
@@ -44949,9 +44811,6 @@ class Table extends React__namespace.Component {
44949
44811
  headers: headers
44950
44812
  });
44951
44813
  };
44952
- simpleValidate(rubric) {
44953
- return tableValidator(this.getUserInput(), rubric, this.context.strings);
44954
- }
44955
44814
  _handleFocus = inputPath => {
44956
44815
  this.props.onFocus(inputPath);
44957
44816
  };
@@ -45095,7 +44954,8 @@ var Table$1 = {
45095
44954
  widget: Table,
45096
44955
  transform: propTransform,
45097
44956
  hidden: true,
45098
- isLintable: true
44957
+ isLintable: true,
44958
+ validator: tableValidator
45099
44959
  };
45100
44960
 
45101
44961
  /**
@@ -45181,22 +45041,9 @@ const IS_VIMEO = /(vimeo\.com)/;
45181
45041
  */
45182
45042
  class Video extends React__namespace.Component {
45183
45043
  static contextType = PerseusI18nContext;
45184
- /**
45185
- * This is the widget's grading function.
45186
- * Points for videos are tallied by the embedded video itself, in the case
45187
- * of Khan Academy videos.
45188
- */ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
45189
- static validate() {
45190
- return noopValidator();
45191
- }
45192
- getUserInput = () => {
45193
- return null;
45194
- };
45195
-
45196
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
45197
- simpleValidate() {
45198
- return noopValidator();
45199
- }
45044
+ // this just helps with TS weak typing when a Widget
45045
+ // doesn't implement any Widget methods
45046
+ isWidget = true;
45200
45047
  change = (() => {
45201
45048
  var _this = this;
45202
45049
  return function () {
@@ -45265,7 +45112,9 @@ var Video$1 = {
45265
45112
  displayName: "Video",
45266
45113
  defaultAlignment: "block",
45267
45114
  supportedAlignments: ["block", "float-left", "float-right", "full-width"],
45268
- widget: Video
45115
+ widget: Video,
45116
+ // TODO: things that aren't interactive shouldn't need validators
45117
+ validator: () => noopValidator()
45269
45118
  };
45270
45119
 
45271
45120
  /* eslint-disable import/no-named-as-default */
@@ -45309,7 +45158,7 @@ const init = function (options) {
45309
45158
 
45310
45159
  // This file is processed by a Rollup plugin (replace) to inject the production
45311
45160
  const libName = "@khanacademy/perseus";
45312
- const libVersion = "36.1.1";
45161
+ const libVersion = "37.0.0";
45313
45162
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45314
45163
 
45315
45164
  const apiVersion = {
@@ -46035,7 +45884,7 @@ class ServerItemRenderer extends React__namespace.Component {
46035
45884
  });
46036
45885
 
46037
45886
  // Call the interactionCallback, if it exists, with the current user input data
46038
- this.props.apiOptions?.interactionCallback?.(this.questionRenderer.getUserInputForWidgets());
45887
+ this.props.apiOptions?.interactionCallback?.(this.questionRenderer.getUserInputMap());
46039
45888
  };
46040
45889
  focus() {
46041
45890
  return this.questionRenderer.focus();
@@ -48661,11 +48510,12 @@ function parsePerseusItem(json) {
48661
48510
  throw new Error("Something went wrong.");
48662
48511
  }
48663
48512
  function buildRandomString() {
48513
+ let capitalize = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
48664
48514
  let randomString = "";
48665
48515
  const randomLength = Math.floor(Math.random() * 8) + 3;
48666
48516
  for (let i = 0; i < randomLength; i++) {
48667
48517
  const randomLetter = String.fromCharCode(97 + Math.floor(Math.random() * 26));
48668
- randomString += randomLetter;
48518
+ randomString += capitalize && i === 0 ? randomLetter.toUpperCase() : randomLetter;
48669
48519
  }
48670
48520
  return randomString;
48671
48521
  }
@@ -48673,9 +48523,12 @@ function buildRandomPhrase() {
48673
48523
  const phrases = [];
48674
48524
  const randomLength = Math.floor(Math.random() * 10) + 5;
48675
48525
  for (let i = 0; i < randomLength; i++) {
48676
- phrases.push(buildRandomString());
48526
+ phrases.push(buildRandomString(i === 0));
48677
48527
  }
48678
- return phrases.join(" ");
48528
+ const modifierStart = ["**", "$"];
48529
+ const modifierEnd = ["**", "$"];
48530
+ const modifierIndex = Math.floor(Math.random() * modifierStart.length);
48531
+ return `${modifierStart[modifierIndex]}${phrases.join(" ")}${modifierEnd[modifierIndex]}`;
48679
48532
  }
48680
48533
  function buildTestData(testObject) {
48681
48534
  return `{"data":{"assessmentItem":{"__typename":"AssessmentItemOrError","error":null,"item":{"__typename":"AssessmentItem","id":"x890b3c70f3e8f4a6","itemData":"${testObject}","problemType":"Type 1","sha":"c7284a3ad65214b4e62bccce236d92f7f5d35941"}}}}`;
@@ -48942,6 +48795,7 @@ exports.getAngleCoords = getAngleCoords;
48942
48795
  exports.getAnswerFromUserInput = getAnswerFromUserInput;
48943
48796
  exports.getAnswersFromWidgets = getAnswersFromWidgets;
48944
48797
  exports.getCircleCoords = getCircleCoords;
48798
+ exports.getClockwiseAngle = getClockwiseAngle;
48945
48799
  exports.getCorrectAnswerForWidgetId = getCorrectAnswerForWidgetId;
48946
48800
  exports.getImagesWithoutAltData = getImagesWithoutAltData;
48947
48801
  exports.getLineCoords = getLineCoords;