@khanacademy/perseus 36.1.2 → 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 (53) 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 +447 -612
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/extra-widgets.d.ts +1 -0
  7. package/dist/index.js +412 -588
  8. package/dist/index.js.map +1 -1
  9. package/dist/renderer-util.d.ts +8 -0
  10. package/dist/renderer.d.ts +19 -20
  11. package/dist/server-item-renderer.d.ts +3 -1
  12. package/dist/types.d.ts +11 -3
  13. package/dist/util.d.ts +4 -0
  14. package/dist/validation.types.d.ts +14 -6
  15. package/dist/widgets/categorizer/categorizer.d.ts +3 -5
  16. package/dist/widgets/cs-program/cs-program.d.ts +2 -3
  17. package/dist/widgets/definition/definition.d.ts +3 -4
  18. package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +35 -0
  19. package/dist/widgets/deprecated-standin/index.d.ts +1 -37
  20. package/dist/widgets/dropdown/dropdown.d.ts +2 -3
  21. package/dist/widgets/explanation/explanation.d.ts +4 -5
  22. package/dist/widgets/expression/expression.d.ts +4 -8
  23. package/dist/widgets/graded-group/graded-group.d.ts +1 -0
  24. package/dist/widgets/graded-group-set/graded-group-set.d.ts +1 -0
  25. package/dist/widgets/grapher/grapher.d.ts +2 -3
  26. package/dist/widgets/group/group.d.ts +8 -5
  27. package/dist/widgets/iframe/iframe.d.ts +2 -3
  28. package/dist/widgets/image/image.d.ts +3 -4
  29. package/dist/widgets/input-number/input-number.d.ts +2 -5
  30. package/dist/widgets/interaction/interaction.d.ts +3 -4
  31. package/dist/widgets/interactive-graph.d.ts +2 -4
  32. package/dist/widgets/label-image/label-image.d.ts +3 -4
  33. package/dist/widgets/matcher/matcher.d.ts +2 -3
  34. package/dist/widgets/matrix/matrix.d.ts +3 -5
  35. package/dist/widgets/measurer/measurer.d.ts +2 -4
  36. package/dist/widgets/molecule/molecule.d.ts +2 -4
  37. package/dist/widgets/number-line/number-line.d.ts +3 -4
  38. package/dist/widgets/numeric-input/numeric-input.d.ts +2 -3
  39. package/dist/widgets/orderer/orderer.d.ts +2 -3
  40. package/dist/widgets/passage/passage.d.ts +3 -9
  41. package/dist/widgets/passage-ref/passage-ref.d.ts +3 -4
  42. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +2 -4
  43. package/dist/widgets/phet-simulation/phet-simulation.d.ts +2 -2
  44. package/dist/widgets/plotter/plotter.d.ts +2 -3
  45. package/dist/widgets/python-program/python-program.d.ts +1 -0
  46. package/dist/widgets/radio/radio-component.d.ts +2 -5
  47. package/dist/widgets/radio/radio.d.ts +1 -0
  48. package/dist/widgets/sorter/sorter.d.ts +2 -3
  49. package/dist/widgets/table/table.d.ts +3 -5
  50. package/dist/widgets/video/video.d.ts +2 -8
  51. package/dist/widgets.d.ts +2 -1
  52. package/package.json +2 -2
  53. 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"]) ?? {
@@ -14777,7 +14763,7 @@ const mapObject$1 = function (obj, lambda) {
14777
14763
  * });
14778
14764
  * {a: 'aa', b: 'bb'}
14779
14765
  */
14780
- const mapObjectFromArray$1 = function (arr, lambda) {
14766
+ const mapObjectFromArray = function (arr, lambda) {
14781
14767
  const result = {};
14782
14768
  ___default["default"].each(arr, function (elem) {
14783
14769
  result[elem] = lambda(elem);
@@ -14787,7 +14773,7 @@ const mapObjectFromArray$1 = function (arr, lambda) {
14787
14773
  var objective_ = {
14788
14774
  pluck: pluck,
14789
14775
  mapObject: mapObject$1,
14790
- mapObjectFromArray: mapObjectFromArray$1
14776
+ mapObjectFromArray: mapObjectFromArray
14791
14777
  };
14792
14778
 
14793
14779
  /**
@@ -19003,169 +18989,6 @@ class QuestionParagraph extends React__namespace.Component {
19003
18989
  }
19004
18990
  }
19005
18991
 
19006
- /**
19007
- * TranslationLinter asynchronously calls Khan Academy's poentry linter.
19008
- *
19009
- * The poentry linter checks for issues that prevent content from being
19010
- * translated. This linter is KA-specific and so TranslationLinter does not do
19011
- * anything in non-KA environments.
19012
- */
19013
-
19014
- // A LintCB is called once the linter has been run.
19015
-
19016
- // msec to wait before we actually call the linter after receiving the
19017
- // last call. The timer is reset if the lint is called during the wait.
19018
- const DEBOUNCE_TIMEOUT = 1000;
19019
- class TranslationLinter {
19020
- // To avoid linting when nothing has changed.
19021
- previousContent = null;
19022
-
19023
- /**
19024
- * Runs the poentry linter on the given perseus string asynchronously.
19025
- *
19026
- * It doesn't do anything unless we're in a KA environment.
19027
- *
19028
- * onLintErrorsGenerated is called even if there are no errors. It is not
19029
- * called if perseusStr is unchanged from the last call.
19030
- *
19031
- * Linting is relatively expensive -- so we debounce linting each instance
19032
- * for some amount of time.
19033
- */
19034
- runLinter = _.debounce((perseusStr, onLintErrorsGenerated) => {
19035
- // TODO(FEI-5003): Do we still need to check for 'KA'?
19036
- if (typeof KA === "undefined") {
19037
- return;
19038
- }
19039
- if (perseusStr === this.previousContent) {
19040
- return;
19041
- }
19042
- this.previousContent = perseusStr;
19043
- if (perseusStr === "") {
19044
- onLintErrorsGenerated([]);
19045
- return;
19046
- }
19047
- fetch("/api/internal/translations/lint_poentry?preview=1&lang=en", {
19048
- headers: {
19049
- "Content-Type": "application/json"
19050
- },
19051
- body: JSON.stringify({
19052
- // The poentry linter verifies that the translation from
19053
- // the original text to the translated text is sane. We
19054
- // want to ensure that the translation from English to
19055
- // English is sane, so msgid === mstr.
19056
- msgid: perseusStr,
19057
- // msgid is the original text
19058
- msgstr: perseusStr,
19059
- // msgstr is the translated text
19060
- format: "perseus_text",
19061
- filename: ""
19062
- }),
19063
- method: "POST"
19064
- }).then(response => {
19065
- if (response.status >= 400) {
19066
- return {
19067
- status: "error",
19068
- message: "Could not run i18n linter."
19069
- };
19070
- }
19071
- return response.json();
19072
- }, rejection => {
19073
- return {
19074
- status: "error",
19075
- message: "Could not run i18n linter."
19076
- };
19077
- }).then(json => {
19078
- if (json.status === "error") {
19079
- onLintErrorsGenerated(["Some part of this text makes it untranslatable. " + "The specific message from the i18n linter was: " + json.message.replace(/\n/g, " ")]);
19080
- } else {
19081
- onLintErrorsGenerated([]);
19082
- }
19083
- });
19084
- }, DEBOUNCE_TIMEOUT);
19085
-
19086
- /**
19087
- * Applies an array of errors generated by linters without position
19088
- * information (TranslationLinter and the legacy getSaveWarnings()) to the
19089
- * top of a Perseus tree.
19090
- */
19091
- applyLintErrors(parsedMarkdown, translationLintErrors) {
19092
- // These lint errors do not have position data associated with
19093
- // them, so we just plop them at the top.
19094
- if (translationLintErrors.length) {
19095
- const errorText = translationLintErrors.join("\n\n");
19096
- parsedMarkdown.unshift({
19097
- content: {
19098
- type: "text",
19099
- content: ""
19100
- },
19101
- insideTable: false,
19102
- message: errorText,
19103
- ruleName: "legacy-error",
19104
- severity: PerseusLinter.Rule.Severity.ERROR,
19105
- type: "lint"
19106
- });
19107
- }
19108
- }
19109
- }
19110
-
19111
- /**
19112
- * Preprocess TeX code to convert things that MathJax doesn't know how to handle
19113
- * to things is does.
19114
- */
19115
- var preprocessTex = (texCode => texCode
19116
- // Replace uses of \begin{align}...\end{align} which KaTeX doesn't
19117
- // support (yet) with \begin{aligned}...\end{aligned} which renders
19118
- // the same is supported by KaTeX. It does the same for align*.
19119
- // TODO(kevinb) update content to use aligned instead of align.
19120
- // TODO(LEMS-1608) Remove this replacement as MathJax supports the
19121
- // "align" macro correctly (and, in fact, it is not synonymous with
19122
- // "aligned").
19123
- .replace(/\{align[*]?\}/g, "{aligned}")
19124
- // Replace non-breaking spaces with regular spaces.
19125
- .replace(/[\u00a0]/g, " "));
19126
-
19127
- const {
19128
- interactiveSizes
19129
- } = constants$1;
19130
-
19131
- // Note: these size cutoffs represent content-width cutoffs as specified in
19132
- // http://zpl.io/1mVmvU (broken link - we no longer use Zeplin)
19133
- // TODO(benkomalo): these values aren't used in JS outside of this file, but
19134
- // are coupled to the values in styles/styles.less - DRY it up at some point
19135
- const smMax = articleMaxWidthTableInPx;
19136
- const mdMax = articleMaxWidthInPx;
19137
- const containerSizeClass = {
19138
- SMALL: "small",
19139
- MEDIUM: "medium",
19140
- LARGE: "large",
19141
- XLARGE: "xlarge"
19142
- };
19143
- const getClassFromWidth = width => {
19144
- if (!width) {
19145
- return containerSizeClass.MEDIUM;
19146
- }
19147
- if (width <= smMax) {
19148
- return containerSizeClass.SMALL;
19149
- }
19150
- if (width <= mdMax) {
19151
- return containerSizeClass.MEDIUM;
19152
- }
19153
- return containerSizeClass.LARGE;
19154
- };
19155
- const getInteractiveBoxFromSizeClass = sizeClass => {
19156
- if (sizeClass === containerSizeClass.SMALL) {
19157
- return [interactiveSizes.defaultBoxSizeSmall, interactiveSizes.defaultBoxSizeSmall];
19158
- }
19159
- return [interactiveSizes.defaultBoxSize, interactiveSizes.defaultBoxSize];
19160
- };
19161
-
19162
- var sizingUtils = /*#__PURE__*/Object.freeze({
19163
- __proto__: null,
19164
- containerSizeClass: containerSizeClass,
19165
- getClassFromWidth: getClassFromWidth,
19166
- getInteractiveBoxFromSizeClass: getInteractiveBoxFromSizeClass
19167
- });
19168
-
19169
18992
  const DEFAULT_ALIGNMENT$1 = "block";
19170
18993
  // NOTE(kevinb): "default" is not one in `validAlignments`.
19171
18994
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -19264,6 +19087,9 @@ const getWidget = name => {
19264
19087
  }
19265
19088
  return widgets[name].widget;
19266
19089
  };
19090
+ const getWidgetValidator = name => {
19091
+ return widgets[name]?.validator ?? null;
19092
+ };
19267
19093
  const getEditor = name => {
19268
19094
  return ___default["default"].has(editors, name) ? editors[name] : null;
19269
19095
  };
@@ -19569,6 +19395,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19569
19395
  replaceEditor: replaceEditor,
19570
19396
  replaceDeprecatedEditors: replaceDeprecatedEditors,
19571
19397
  getWidget: getWidget,
19398
+ getWidgetValidator: getWidgetValidator,
19572
19399
  getEditor: getEditor,
19573
19400
  getTransform: getTransform,
19574
19401
  getVersion: getVersion,
@@ -19588,6 +19415,222 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19588
19415
  isLintable: isLintable
19589
19416
  });
19590
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
+
19591
19634
  class WidgetContainer extends React__namespace.Component {
19592
19635
  widgetRef = /*#__PURE__*/React__namespace.createRef();
19593
19636
  static defaultProps = {
@@ -19734,8 +19777,7 @@ class WidgetContainer extends React__namespace.Component {
19734
19777
  }
19735
19778
 
19736
19779
  const {
19737
- mapObject,
19738
- mapObjectFromArray
19780
+ mapObject
19739
19781
  } = objective_;
19740
19782
  const rContainsNonWhitespace = /\S/;
19741
19783
  const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
@@ -19762,6 +19804,7 @@ const isIdPathPrefix = function (prefixArray, wholeArray) {
19762
19804
  });
19763
19805
  };
19764
19806
  class Renderer extends React__namespace.Component {
19807
+ static contextType = PerseusI18nContext;
19765
19808
  _currentFocus;
19766
19809
  // @ts-expect-error - TS2564 - Property '_foundTextNodes' has no initializer and is not definitely assigned in the constructor.
19767
19810
  _foundTextNodes;
@@ -20065,11 +20108,7 @@ class Renderer extends React__namespace.Component {
20065
20108
  }
20066
20109
  let interactionTracker = this._interactionTrackers[id];
20067
20110
  if (!interactionTracker) {
20068
- interactionTracker = this._interactionTrackers[id] = new InteractionTracker(apiOptions.trackInteraction,
20069
- // @ts-expect-error - TS2345 - Argument of type 'string | null | undefined' is not assignable to parameter of type 'string'.
20070
- widgetInfo && widgetInfo.type, id,
20071
- // @ts-expect-error - TS2345 - Argument of type 'string | null | undefined' is not assignable to parameter of type 'string'.
20072
- getTracking(widgetInfo && widgetInfo.type));
20111
+ interactionTracker = this._interactionTrackers[id] = new InteractionTracker(apiOptions.trackInteraction, widgetInfo && widgetInfo.type, id, getTracking(widgetInfo && widgetInfo.type));
20073
20112
  }
20074
20113
  return {
20075
20114
  ...widgetProps,
@@ -20883,23 +20922,9 @@ class Renderer extends React__namespace.Component {
20883
20922
  }, this);
20884
20923
  return state;
20885
20924
  };
20886
- emptyWidgets = () => {
20887
- // @ts-expect-error - TS2345 - Argument of type '(id: string) => boolean | undefined' is not assignable to parameter of type 'Iteratee<string[], boolean, string>'.
20888
- return ___default["default"].filter(this.widgetIds, id => {
20889
- const widgetInfo = this._getWidgetInfo(id);
20890
- if (widgetInfo.static) {
20891
- // Static widgets shouldn't count as empty
20892
- return false;
20893
- }
20894
- const widget = this.getWidgetInstance(id);
20895
- if (widget && widget.simpleValidate) {
20896
- const score = widget.simpleValidate(widgetInfo.options,
20897
- // @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'.
20898
- null);
20899
- return Util.scoreIsEmpty(score);
20900
- }
20901
- });
20902
- };
20925
+ emptyWidgets() {
20926
+ return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20927
+ }
20903
20928
  _setWidgetProps = (id, newProps, cb, silent) => {
20904
20929
  this.setState(prevState => {
20905
20930
  const widgetProps = {
@@ -20963,8 +20988,11 @@ class Renderer extends React__namespace.Component {
20963
20988
 
20964
20989
  /**
20965
20990
  * Returns an array of the widget `.getUserInput()` results
20991
+ *
20992
+ * TODO: can we remove this?
20993
+ * @deprecated use getUserInputMap
20966
20994
  */
20967
- getUserInput = () => {
20995
+ getUserInput() {
20968
20996
  return this.widgetIds.map(id => {
20969
20997
  const widget = this.getWidgetInstance(id);
20970
20998
  if (widget && widget.getUserInput) {
@@ -20973,7 +21001,24 @@ class Renderer extends React__namespace.Component {
20973
21001
  return widget.getUserInput();
20974
21002
  }
20975
21003
  });
20976
- };
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
+ }
20977
21022
 
20978
21023
  /**
20979
21024
  * Returns an array of all widget IDs in the order they occur in
@@ -20983,59 +21028,23 @@ class Renderer extends React__namespace.Component {
20983
21028
  return this.widgetIds;
20984
21029
  };
20985
21030
 
20986
- /**
20987
- * Returns the result of `.getUserInput()` for each widget, in
20988
- * a map from widgetId to userInput.
20989
- * NOTE(jeremy): This function is hauntingly similar to `getUserInput` with
20990
- * the major difference being that this function returns a map of
20991
- * `widgetID` => UserInput and `getUserInput` simply returns an array. It
20992
- * would be trivial to map between the results of each of these functions,
20993
- * so we should aim to remove one of these functions.
20994
- */
20995
- getUserInputForWidgets = () => {
20996
- return mapObjectFromArray(this.widgetIds, id => {
20997
- const widget = this.getWidgetInstance(id);
20998
- if (widget && widget.getUserInput) {
20999
- return widget.getUserInput();
21000
- }
21001
- });
21002
- };
21003
-
21004
21031
  /**
21005
21032
  * Returns an object mapping from widget ID to perseus-style score.
21006
21033
  * The keys of this object are the values of the array returned
21007
21034
  * from `getWidgetIds`.
21008
21035
  */
21009
- scoreWidgets = () => {
21010
- const widgetProps = this.state.widgetInfo;
21011
- const gradedWidgetIds = ___default["default"].filter(this.widgetIds, id => {
21012
- const props = widgetProps[id];
21013
- const widgetIsGraded = props?.graded == null || props.graded;
21014
- const widgetIsStatic = !!props?.static;
21015
- // Ungraded widgets or widgets set to static shouldn't be graded.
21016
- return widgetIsGraded && !widgetIsStatic;
21017
- });
21018
- const widgetScores = {};
21019
- ___default["default"].each(gradedWidgetIds, id => {
21020
- const props = widgetProps[id];
21021
- const widget = this.getWidgetInstance(id);
21022
- // widget can be undefined if it hasn't yet been rendered
21023
- if (widget && widget.simpleValidate) {
21024
- widgetScores[id] = widget.simpleValidate({
21025
- ...props?.options,
21026
- scoring: true
21027
- });
21028
- }
21029
- });
21030
- return widgetScores;
21031
- };
21036
+ scoreWidgets() {
21037
+ return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
21038
+ }
21032
21039
 
21033
21040
  /**
21034
21041
  * Grades the content.
21035
21042
  */
21036
- score = () => {
21037
- return ___default["default"].reduce(this.scoreWidgets(), Util.combineScores, Util.noScore);
21038
- };
21043
+ score() {
21044
+ const scores = this.scoreWidgets();
21045
+ const combinedScore = Util.flattenScores(scores);
21046
+ return combinedScore;
21047
+ }
21039
21048
  guessAndScore = () => {
21040
21049
  const totalGuess = this.getUserInput();
21041
21050
  const totalScore = this.score();
@@ -21147,12 +21156,12 @@ class Renderer extends React__namespace.Component {
21147
21156
  // If highlightLint is true and lint is detected, this call
21148
21157
  // will modify the parse tree by adding lint nodes that will
21149
21158
  // serve to highlight the lint when rendered
21150
- const context = {
21159
+ const fullLinterContext = {
21151
21160
  content: this.props.content,
21152
21161
  widgets: this.props.widgets,
21153
21162
  ...this.props.linterContext
21154
21163
  };
21155
- PerseusLinter__namespace.runLinter(parsedMarkdown, context, true);
21164
+ PerseusLinter__namespace.runLinter(parsedMarkdown, fullLinterContext, true);
21156
21165
 
21157
21166
  // Apply the lint errors from the last TranslationLinter run.
21158
21167
  // TODO(joshuan): Support overlapping dots.
@@ -21974,9 +21983,6 @@ class InputNumber extends React__namespace.Component {
21974
21983
  apiOptions: ApiOptions.defaults,
21975
21984
  linterContext: PerseusLinter.linterContextDefault
21976
21985
  };
21977
- static validate(state, rubric, strings) {
21978
- return inputNumberValidator(state, rubric, strings);
21979
- }
21980
21986
  static getUserInputFromProps(props) {
21981
21987
  return {
21982
21988
  currentValue: props.currentValue
@@ -22047,9 +22053,6 @@ class InputNumber extends React__namespace.Component {
22047
22053
  getUserInput() {
22048
22054
  return InputNumber.getUserInputFromProps(this.props);
22049
22055
  }
22050
- simpleValidate(rubric) {
22051
- return inputNumberValidator(this.getUserInput(), rubric, this.context.strings);
22052
- }
22053
22056
  examples = () => {
22054
22057
  const {
22055
22058
  strings
@@ -22152,7 +22155,8 @@ var InputNumber$1 = {
22152
22155
  hidden: true,
22153
22156
  widget: InputNumber,
22154
22157
  transform: propTransform$3,
22155
- isLintable: true
22158
+ isLintable: true,
22159
+ validator: inputNumberValidator
22156
22160
  };
22157
22161
 
22158
22162
  const ParseTex = TexWrangler.parseTex;
@@ -22359,9 +22363,6 @@ class NumericInput extends React__namespace.Component {
22359
22363
  }
22360
22364
  return answerStrings[0];
22361
22365
  }
22362
- static validate(userInput, rubric, strings) {
22363
- return numericInputValidator(userInput, rubric, strings);
22364
- }
22365
22366
  state = {
22366
22367
  // keeps track of the other set of values when switching
22367
22368
  // between 0 and finite solutions
@@ -22394,9 +22395,6 @@ class NumericInput extends React__namespace.Component {
22394
22395
  const allFormsAccepted = answerFormNames.length >= Object.keys(formExamples).length;
22395
22396
  return !noFormsAccepted && !allFormsAccepted;
22396
22397
  };
22397
- simpleValidate(rubric) {
22398
- return numericInputValidator(this.getUserInput(), rubric, this.context.strings);
22399
- }
22400
22398
  focus = () => {
22401
22399
  this.inputRef?.focus();
22402
22400
  return true;
@@ -22550,7 +22548,8 @@ var NumericInput$1 = {
22550
22548
  accessible: true,
22551
22549
  widget: NumericInput,
22552
22550
  transform: propsTransform,
22553
- isLintable: true
22551
+ isLintable: true,
22552
+ validator: numericInputValidator
22554
22553
  };
22555
22554
 
22556
22555
  /**
@@ -24371,16 +24370,15 @@ class Passage extends React__namespace.Component {
24371
24370
  highlights: {},
24372
24371
  linterContext: PerseusLinter.linterContextDefault
24373
24372
  };
24373
+
24374
+ // this just helps with TS weak typing when a Widget
24375
+ // doesn't implement any Widget methods
24376
+ isWidget = true;
24374
24377
  state = {
24375
24378
  nLines: null,
24376
24379
  startLineNumbersAfter: 0,
24377
24380
  stylesAreApplied: false
24378
24381
  };
24379
-
24380
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24381
- static validate() {
24382
- return noopValidator();
24383
- }
24384
24382
  componentDidMount() {
24385
24383
  this._updateState();
24386
24384
 
@@ -24589,21 +24587,6 @@ class Passage extends React__namespace.Component {
24589
24587
  };
24590
24588
  }
24591
24589
 
24592
- /**
24593
- * Misc functions
24594
- *
24595
- * These are misc widget functions used for the widget API
24596
- */
24597
-
24598
- getUserInput() {
24599
- return null;
24600
- }
24601
-
24602
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24603
- simpleValidate() {
24604
- return noopValidator();
24605
- }
24606
-
24607
24590
  /**
24608
24591
  * Rendering
24609
24592
  *
@@ -24747,7 +24730,8 @@ var Passage$1 = {
24747
24730
  transform: editorProps => {
24748
24731
  return ___default["default"].pick(editorProps, "passageTitle", "passageText", "footnotes", "showLineNumbers");
24749
24732
  },
24750
- isLintable: true
24733
+ isLintable: true,
24734
+ validator: () => noopValidator()
24751
24735
  };
24752
24736
 
24753
24737
  /**
@@ -24777,15 +24761,14 @@ class PassageRef extends React__namespace.Component {
24777
24761
  referenceNumber: 1,
24778
24762
  summaryText: ""
24779
24763
  };
24764
+
24765
+ // this just helps with TS weak typing when a Widget
24766
+ // doesn't implement any Widget methods
24767
+ isWidget = true;
24780
24768
  state = {
24781
24769
  lineRange: null,
24782
24770
  content: null
24783
24771
  };
24784
-
24785
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24786
- static validate() {
24787
- return noopValidator();
24788
- }
24789
24772
  componentDidMount() {
24790
24773
  // TODO(scottgrant): This is a hack to remove the deprecated call to
24791
24774
  // this.isMounted() but is still considered an anti-pattern.
@@ -24804,11 +24787,6 @@ class PassageRef extends React__namespace.Component {
24804
24787
  window.removeEventListener("resize", this._throttledUpdateRange);
24805
24788
  this._isMounted = false;
24806
24789
  }
24807
-
24808
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24809
- getUserInput() {
24810
- return null;
24811
- }
24812
24790
  change = (() => {
24813
24791
  var _this = this;
24814
24792
  return function () {
@@ -24838,11 +24816,6 @@ class PassageRef extends React__namespace.Component {
24838
24816
  }
24839
24817
  }
24840
24818
  };
24841
-
24842
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24843
- simpleValidate() {
24844
- return noopValidator();
24845
- }
24846
24819
  render() {
24847
24820
  const {
24848
24821
  strings
@@ -24891,7 +24864,9 @@ var PassageRef$1 = {
24891
24864
  version: {
24892
24865
  major: 0,
24893
24866
  minor: 1
24894
- }
24867
+ },
24868
+ // TODO: things that aren't interactive shouldn't need validators
24869
+ validator: () => noopValidator()
24895
24870
  };
24896
24871
 
24897
24872
  /**
@@ -26310,9 +26285,6 @@ class Radio$1 extends React__namespace.Component {
26310
26285
  linterContext: PerseusLinter.linterContextDefault,
26311
26286
  showSolutions: "none"
26312
26287
  };
26313
- static validate(userInput, rubric, strings) {
26314
- return radioValidator(userInput, rubric, strings);
26315
- }
26316
26288
  static getUserInputFromProps(props) {
26317
26289
  // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
26318
26290
  // future timeline implementers: this used to be {value: i} before
@@ -26499,9 +26471,6 @@ class Radio$1 extends React__namespace.Component {
26499
26471
  getUserInput() {
26500
26472
  return Radio$1.getUserInputFromProps(this.props);
26501
26473
  }
26502
- simpleValidate(rubric) {
26503
- return radioValidator(this.getUserInput(), rubric, this.context.strings);
26504
- }
26505
26474
 
26506
26475
  /**
26507
26476
  * Turn on rationale display for the currently selected choices. Note that
@@ -26514,7 +26483,7 @@ class Radio$1 extends React__namespace.Component {
26514
26483
  choiceStates
26515
26484
  } = this.props;
26516
26485
  if (choiceStates) {
26517
- const score = this.simpleValidate(rubric);
26486
+ const score = radioValidator(this.getUserInput(), rubric, this.context.strings);
26518
26487
  const widgetCorrect = score.type === "points" && score.total === score.earned;
26519
26488
  const newStates = choiceStates.map(state => ({
26520
26489
  ...state,
@@ -26794,7 +26763,8 @@ var Radio = {
26794
26763
  minor: 0
26795
26764
  },
26796
26765
  propUpgrades: propUpgrades$1,
26797
- isLintable: true
26766
+ isLintable: true,
26767
+ validator: radioValidator
26798
26768
  };
26799
26769
 
26800
26770
  /* eslint-disable import/no-named-as-default */
@@ -26837,9 +26807,6 @@ class Categorizer extends React__namespace.Component {
26837
26807
  state = {
26838
26808
  uniqueId: ___default["default"].uniqueId("perseus_radio_")
26839
26809
  };
26840
- static validate(userInput, rubric, strings) {
26841
- return categorizerValidator(userInput, rubric, strings);
26842
- }
26843
26810
  static getUserInputFromProps(props) {
26844
26811
  return {
26845
26812
  values: props.values
@@ -26865,9 +26832,6 @@ class Categorizer extends React__namespace.Component {
26865
26832
  this.change("values", values);
26866
26833
  this.props.trackInteraction();
26867
26834
  }
26868
- simpleValidate(rubric) {
26869
- return categorizerValidator(this.getUserInput(), rubric, this.context.strings);
26870
- }
26871
26835
  render() {
26872
26836
  const self = this;
26873
26837
 
@@ -27006,7 +26970,8 @@ var Categorizer$1 = {
27006
26970
  staticTransform: editorProps => {
27007
26971
  return ___default["default"].pick(editorProps, "items", "categories", "values", "randomizeItems");
27008
26972
  },
27009
- isLintable: true
26973
+ isLintable: true,
26974
+ validator: categorizerValidator
27010
26975
  };
27011
26976
 
27012
26977
  /**
@@ -27111,11 +27076,6 @@ class CSProgram extends React__namespace.Component {
27111
27076
  // optional message
27112
27077
  message: null
27113
27078
  };
27114
-
27115
- // The widget's grading function
27116
- static validate(state) {
27117
- return csProgramValidator(state);
27118
- }
27119
27079
  componentDidMount() {
27120
27080
  $__default["default"](window).on("message", this.handleMessageEvent);
27121
27081
  }
@@ -27157,9 +27117,6 @@ class CSProgram extends React__namespace.Component {
27157
27117
  message: this.props.message
27158
27118
  };
27159
27119
  }
27160
- simpleValidate() {
27161
- return csProgramValidator(this.getUserInput());
27162
- }
27163
27120
  render() {
27164
27121
  if (!this.props.programID) {
27165
27122
  return /*#__PURE__*/React__namespace.createElement("div", null);
@@ -27234,7 +27191,8 @@ var CSProgram$1 = {
27234
27191
  displayName: "CS Program",
27235
27192
  supportedAlignments: ["block", "full-width"],
27236
27193
  widget: CSProgram,
27237
- hidden: true
27194
+ hidden: true,
27195
+ validator: csProgramValidator
27238
27196
  };
27239
27197
 
27240
27198
  class Definition extends React__namespace.Component {
@@ -27244,18 +27202,9 @@ class Definition extends React__namespace.Component {
27244
27202
  definition: "definition goes here"
27245
27203
  };
27246
27204
 
27247
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27248
- static validate() {
27249
- return noopValidator();
27250
- }
27251
- getUserInput() {
27252
- return {};
27253
- }
27254
-
27255
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27256
- simpleValidate() {
27257
- return noopValidator();
27258
- }
27205
+ // this just helps with TS weak typing when a Widget
27206
+ // doesn't implement any Widget methods
27207
+ isWidget = true;
27259
27208
  render() {
27260
27209
  return /*#__PURE__*/React__namespace.createElement(DefinitionConsumer, null, _ref => {
27261
27210
  let {
@@ -27311,24 +27260,18 @@ var Definition$1 = {
27311
27260
  accessible: true,
27312
27261
  defaultAlignment: "inline",
27313
27262
  widget: Definition,
27314
- transform: x => x
27263
+ transform: x => x,
27264
+ // TODO: things that aren't interactive shouldn't need validators
27265
+ validator: () => noopValidator()
27315
27266
  };
27316
27267
 
27317
27268
  // The props are type `any` on purpose so that this can receive props
27318
27269
  // from any deprecated widget
27319
27270
  class DeprecatedStandin extends React__default["default"].Component {
27320
27271
  static contextType = PerseusI18nContext;
27321
- static validate() {
27322
- // Since this mean to replace an existing widget the learner
27323
- // WILL earn points for this widget
27324
- return noopValidator(1);
27325
- }
27326
- getUserInput() {
27327
- return {};
27328
- }
27329
- simpleValidate() {
27330
- return noopValidator(1);
27331
- }
27272
+ // this just helps with TS weak typing when a Widget
27273
+ // doesn't implement any Widget methods
27274
+ isWidget = true;
27332
27275
  render() {
27333
27276
  return /*#__PURE__*/React__default["default"].createElement("div", {
27334
27277
  style: {
@@ -27346,7 +27289,9 @@ var DeprecatedStandin$1 = {
27346
27289
  name: "deprecated-standin",
27347
27290
  displayName: "Deprecated Standin",
27348
27291
  widget: DeprecatedStandin,
27349
- hidden: true
27292
+ hidden: true,
27293
+ // TODO: things that aren't interactive shouldn't need validators
27294
+ validator: () => noopValidator(1)
27350
27295
  };
27351
27296
 
27352
27297
  function dropdownValidator(userInput, rubric) {
@@ -27373,9 +27318,6 @@ class Dropdown extends React__namespace.Component {
27373
27318
  placeholder: "",
27374
27319
  apiOptions: ApiOptions.defaults
27375
27320
  };
27376
- static validate(userInput, rubric) {
27377
- return dropdownValidator(userInput, rubric);
27378
- }
27379
27321
  focus = () => {
27380
27322
  // TODO(LP-10797): This focus() call doesn't do anything because our
27381
27323
  // root element is a <div> and that cannot be focused without a
@@ -27398,9 +27340,6 @@ class Dropdown extends React__namespace.Component {
27398
27340
  value: this.props.selected
27399
27341
  };
27400
27342
  }
27401
- simpleValidate(rubric) {
27402
- return dropdownValidator(this.getUserInput(), rubric);
27403
- }
27404
27343
  render() {
27405
27344
  const children = [/*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.OptionItem, {
27406
27345
  key: "placeholder",
@@ -27441,7 +27380,8 @@ var Dropdown$1 = {
27441
27380
  defaultAlignment: "inline-block",
27442
27381
  accessible: true,
27443
27382
  widget: Dropdown,
27444
- transform: optionsTransform
27383
+ transform: optionsTransform,
27384
+ validator: dropdownValidator
27445
27385
  };
27446
27386
 
27447
27387
  // transform = _.identity
@@ -27454,10 +27394,6 @@ function mediaQueryIsMatched(mediaQuery) {
27454
27394
  }
27455
27395
  class Explanation extends React__namespace.Component {
27456
27396
  static contextType = PerseusI18nContext;
27457
- state = {
27458
- expanded: false
27459
- };
27460
- _mounted = false;
27461
27397
  static defaultProps = {
27462
27398
  showPrompt: "Explain",
27463
27399
  hidePrompt: "Hide explanation",
@@ -27466,10 +27402,13 @@ class Explanation extends React__namespace.Component {
27466
27402
  linterContext: PerseusLinter.linterContextDefault
27467
27403
  };
27468
27404
 
27469
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27470
- static validate() {
27471
- return noopValidator();
27472
- }
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;
27473
27412
  componentDidMount() {
27474
27413
  this._mounted = true;
27475
27414
  }
@@ -27491,14 +27430,6 @@ class Explanation extends React__namespace.Component {
27491
27430
  });
27492
27431
  this.props.trackInteraction();
27493
27432
  };
27494
- getUserInput() {
27495
- return {};
27496
- }
27497
-
27498
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27499
- simpleValidate() {
27500
- return noopValidator();
27501
- }
27502
27433
  render() {
27503
27434
  const promptText = this.state.expanded ? this.props.hidePrompt : this.props.showPrompt;
27504
27435
  const caretIcon = this.state.expanded ? caretUp__default["default"] : caretDown__default["default"];
@@ -27604,7 +27535,9 @@ var Explanation$1 = {
27604
27535
  defaultAlignment: "inline",
27605
27536
  widget: Explanation,
27606
27537
  transform: ___default["default"].identity,
27607
- isLintable: true
27538
+ isLintable: true,
27539
+ // TODO: things that aren't interactive shouldn't need validators
27540
+ validator: () => noopValidator()
27608
27541
  };
27609
27542
 
27610
27543
  class GradedGroupAnswerBar extends React__namespace.Component {
@@ -29136,9 +29069,6 @@ class Grapher extends React__namespace.Component {
29136
29069
  horizHairline;
29137
29070
  vertHairline;
29138
29071
  static defaultProps = DEFAULT_GRAPHER_PROPS;
29139
- static validate(state, rubric) {
29140
- return grapherValidator(state, rubric);
29141
- }
29142
29072
  static getUserInputFromProps(props) {
29143
29073
  return props.plot;
29144
29074
  }
@@ -29241,9 +29171,6 @@ class Grapher extends React__namespace.Component {
29241
29171
  this.vertHairline.hide();
29242
29172
  }
29243
29173
  };
29244
- simpleValidate(rubric) {
29245
- return grapherValidator(this.getUserInput(), rubric);
29246
- }
29247
29174
  getUserInput() {
29248
29175
  return Grapher.getUserInputFromProps(this.props);
29249
29176
  }
@@ -29335,7 +29262,8 @@ var Grapher$1 = {
29335
29262
  hidden: true,
29336
29263
  widget: Grapher,
29337
29264
  transform: propTransform$2,
29338
- staticTransform: staticTransform$4
29265
+ staticTransform: staticTransform$4,
29266
+ validator: grapherValidator
29339
29267
  };
29340
29268
 
29341
29269
  // exports has no 'transform'
@@ -29364,6 +29292,13 @@ class Group extends React__namespace.Component {
29364
29292
  return change.apply(_this, args);
29365
29293
  };
29366
29294
  })();
29295
+ getUserInputMap() {
29296
+ return this.rendererRef?.getUserInputMap();
29297
+ }
29298
+
29299
+ /**
29300
+ * @deprecated getUserInputMap should be used for Groups
29301
+ */
29367
29302
  getUserInput() {
29368
29303
  return this.rendererRef?.getUserInput();
29369
29304
  }
@@ -29376,11 +29311,6 @@ class Group extends React__namespace.Component {
29376
29311
  // (all our changes were in state):
29377
29312
  return null;
29378
29313
  };
29379
- simpleValidate() {
29380
- return this.rendererRef?.score() ?? {
29381
- type: "invalid"
29382
- };
29383
- }
29384
29314
 
29385
29315
  // Mobile API:
29386
29316
  getInputPaths() {
@@ -29530,9 +29460,6 @@ class Iframe extends React__namespace.Component {
29530
29460
  allowFullScreen: false,
29531
29461
  allowTopNavigation: false
29532
29462
  };
29533
- static validate(state) {
29534
- return iframeValidator(state);
29535
- }
29536
29463
  componentDidMount() {
29537
29464
  $__default["default"](window).on("message", this.handleMessageEvent);
29538
29465
  }
@@ -29574,9 +29501,6 @@ class Iframe extends React__namespace.Component {
29574
29501
  return change.apply(_this, args);
29575
29502
  };
29576
29503
  })();
29577
- simpleValidate() {
29578
- return iframeValidator(this.getUserInput());
29579
- }
29580
29504
  render() {
29581
29505
  const style = {
29582
29506
  width: String(this.props.width),
@@ -29644,7 +29568,8 @@ var Iframe$1 = {
29644
29568
  displayName: "Iframe (deprecated)",
29645
29569
  widget: Iframe,
29646
29570
  // Let's not expose it to all content creators yet
29647
- hidden: true
29571
+ hidden: true,
29572
+ validator: iframeValidator
29648
29573
  };
29649
29574
 
29650
29575
  const defaultBoxSize = 400;
@@ -29673,10 +29598,9 @@ class ImageWidget extends React__namespace.Component {
29673
29598
  linterContext: PerseusLinter.linterContextDefault
29674
29599
  };
29675
29600
 
29676
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
29677
- static validate() {
29678
- return noopValidator();
29679
- }
29601
+ // this just helps with TS weak typing when a Widget
29602
+ // doesn't implement any Widget methods
29603
+ isWidget = true;
29680
29604
  change = (() => {
29681
29605
  var _this = this;
29682
29606
  return function () {
@@ -29686,14 +29610,6 @@ class ImageWidget extends React__namespace.Component {
29686
29610
  return change.apply(_this, args);
29687
29611
  };
29688
29612
  })();
29689
- getUserInput() {
29690
- return null;
29691
- }
29692
-
29693
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
29694
- simpleValidate() {
29695
- return noopValidator();
29696
- }
29697
29613
  render() {
29698
29614
  let image;
29699
29615
  const alt = this.props.caption === this.props.alt ? "" : this.props.alt;
@@ -29839,7 +29755,9 @@ var Image$1 = {
29839
29755
  supportedAlignments: editorAlignments,
29840
29756
  displayName: "Image",
29841
29757
  widget: ImageWidget,
29842
- isLintable: true
29758
+ isLintable: true,
29759
+ // TODO: things that aren't interactive shouldn't need validators
29760
+ validator: () => noopValidator()
29843
29761
  };
29844
29762
 
29845
29763
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -29918,10 +29836,9 @@ class Interaction extends React__namespace.Component {
29918
29836
  elements: []
29919
29837
  };
29920
29838
 
29921
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
29922
- static validate() {
29923
- return noopValidator();
29924
- }
29839
+ // this just helps with TS weak typing when a Widget
29840
+ // doesn't implement any Widget methods
29841
+ isWidget = true;
29925
29842
  state = {
29926
29843
  variables: _getInitialVariables(this.props.elements),
29927
29844
  functions: _getInitialFunctions(this.props.elements)
@@ -30029,18 +29946,6 @@ class Interaction extends React__namespace.Component {
30029
29946
  return change.apply(_this, args);
30030
29947
  };
30031
29948
  })();
30032
- getUserInput() {
30033
- // TODO(eater): Perhaps we want to be able to record the state of the
30034
- // user's interaction. Unfortunately sending all the props will
30035
- // probably make the attempt payload too large. So for now, don't send
30036
- // anything.
30037
- return {};
30038
- }
30039
-
30040
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
30041
- simpleValidate() {
30042
- return noopValidator();
30043
- }
30044
29949
  render() {
30045
29950
  const range = this.props.graph.range;
30046
29951
  let labels = this.props.graph.labels;
@@ -30448,7 +30353,9 @@ var Interactive = {
30448
30353
  displayName: "Interaction",
30449
30354
  widget: Interaction,
30450
30355
  transform: ___default["default"].identity,
30451
- hidden: true
30356
+ hidden: true,
30357
+ // TODO: things that aren't interactive shouldn't need validators
30358
+ validator: () => noopValidator()
30452
30359
  };
30453
30360
 
30454
30361
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -37605,9 +37512,6 @@ class LegacyInteractiveGraph extends React__namespace.Component {
37605
37512
  getUserInput() {
37606
37513
  return InteractiveGraph.getUserInputFromProps(this.props);
37607
37514
  }
37608
- simpleValidate(rubric) {
37609
- return interactiveGraphValidator(this.getUserInput(), rubric);
37610
- }
37611
37515
  focus = $__default["default"].noop;
37612
37516
  render() {
37613
37517
  const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
@@ -37680,9 +37584,6 @@ class InteractiveGraph extends React__namespace.Component {
37680
37584
  }
37681
37585
  throw new perseusCore.PerseusError("Cannot getUserInput from a graph that has never rendered", perseusCore.Errors.NotAllowed);
37682
37586
  }
37683
- simpleValidate(rubric) {
37684
- return interactiveGraphValidator(this.getUserInput(), rubric);
37685
- }
37686
37587
  render() {
37687
37588
  // Mafs shim
37688
37589
  const mafsFlags = this.props.apiOptions?.flags?.["mafs"];
@@ -38013,9 +37914,6 @@ class InteractiveGraph extends React__namespace.Component {
38013
37914
  const angle = getClockwiseAngle(coords, allowReflexAngles);
38014
37915
  return angle.toFixed(0) + "\u00B0 angle" + " at (" + coords[1].join(", ") + ")";
38015
37916
  }
38016
- static validate(userInput, rubric) {
38017
- return interactiveGraphValidator(userInput, rubric);
38018
- }
38019
37917
  static getUserInputFromProps(props) {
38020
37918
  return props.graph;
38021
37919
  }
@@ -38052,7 +37950,8 @@ var InteractiveGraph$1 = {
38052
37950
  name: "interactive-graph",
38053
37951
  displayName: "Interactive graph (Assessments only)",
38054
37952
  widget: InteractiveGraph,
38055
- staticTransform: staticTransform$3
37953
+ staticTransform: staticTransform$3,
37954
+ validator: interactiveGraphValidator
38056
37955
  };
38057
37956
 
38058
37957
  const black = "#000000";
@@ -39428,9 +39327,6 @@ class LabelImage extends React__namespace.Component {
39428
39327
  // The rendered markers on the question image for labeling.
39429
39328
  _markers;
39430
39329
  _mounted = false;
39431
- static validate(state, rubric) {
39432
- return labelImageValidator(state);
39433
- }
39434
39330
 
39435
39331
  /**
39436
39332
  * Test whether point is contained within triangle.
@@ -39623,9 +39519,6 @@ class LabelImage extends React__namespace.Component {
39623
39519
  componentWillUnmount() {
39624
39520
  this._mounted = false;
39625
39521
  }
39626
- simpleValidate(rubric) {
39627
- return labelImageValidator(this.getUserInput());
39628
- }
39629
39522
  getUserInput() {
39630
39523
  const {
39631
39524
  markers
@@ -39991,19 +39884,13 @@ const LabelImageWithDependencies = /*#__PURE__*/React__namespace.forwardRef((pro
39991
39884
  analytics: deps.analytics
39992
39885
  }, props));
39993
39886
  });
39994
-
39995
- // HACK: Propogate "static" methods onto our wrapper component.
39996
- // In the future we should adjust client apps to not depend on these static
39997
- // methods and instead adjust Peresus to provide these facilities through
39998
- // instance methods on our Renderers.
39999
- // @ts-expect-error - TS2339 - Property 'validate' does not exist on type
40000
- LabelImageWithDependencies.validate = labelImageValidator;
40001
39887
  var LabelImage$1 = {
40002
39888
  name: "label-image",
40003
39889
  displayName: "Label Image",
40004
39890
  widget: LabelImageWithDependencies,
40005
39891
  accessible: true,
40006
- isLintable: true
39892
+ isLintable: true,
39893
+ validator: labelImageValidator
40007
39894
  };
40008
39895
 
40009
39896
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -40745,9 +40632,6 @@ class Matcher extends React__namespace.Component {
40745
40632
  rightHeight: 0,
40746
40633
  texRendererLoaded: false
40747
40634
  };
40748
- static validate(state, rubric) {
40749
- return matcherValidator(state, rubric);
40750
- }
40751
40635
  changeAndTrack = e => {
40752
40636
  this.props.onChange(e);
40753
40637
  this.props.trackInteraction();
@@ -40799,9 +40683,6 @@ class Matcher extends React__namespace.Component {
40799
40683
  // @ts-expect-error - TS2339 - Property 'moveOptionToIndex' does not exist on type 'ReactInstance'.
40800
40684
  this.refs.right.moveOptionToIndex(option, index);
40801
40685
  };
40802
- simpleValidate(rubric) {
40803
- return matcherValidator(this.getUserInput(), rubric);
40804
- }
40805
40686
  render() {
40806
40687
  // To minimize layout shift, we display a spinner until our math
40807
40688
  // renderer is ready to render the math inside the matcher. To
@@ -40928,7 +40809,8 @@ var Matcher$1 = {
40928
40809
  name: "matcher",
40929
40810
  displayName: "Matcher (two column)",
40930
40811
  widget: Matcher,
40931
- isLintable: true
40812
+ isLintable: true,
40813
+ validator: matcherValidator
40932
40814
  };
40933
40815
 
40934
40816
  function matrixValidator(state, rubric, strings) {
@@ -41069,9 +40951,6 @@ class Matrix extends React__namespace.Component {
41069
40951
  apiOptions: ApiOptions.defaults,
41070
40952
  linterContext: PerseusLinter.linterContextDefault
41071
40953
  };
41072
- static validate(state, rubric, strings) {
41073
- return matrixValidator(state, rubric, strings);
41074
- }
41075
40954
  state = {
41076
40955
  enterTheMatrix: 0
41077
40956
  };
@@ -41214,9 +41093,6 @@ class Matrix extends React__namespace.Component {
41214
41093
  answers: this.props.answers
41215
41094
  };
41216
41095
  }
41217
- simpleValidate(rubric) {
41218
- return matrixValidator(this.getUserInput(), rubric, this.context.strings);
41219
- }
41220
41096
  render() {
41221
41097
  // Set the input sizes through JS so we can control the size of the
41222
41098
  // brackets. (If we set them in CSS we won't know values until the
@@ -41401,7 +41277,8 @@ var Matrix$1 = {
41401
41277
  widget: Matrix,
41402
41278
  transform: propTransform$1,
41403
41279
  staticTransform: staticTransform$2,
41404
- isLintable: true
41280
+ isLintable: true,
41281
+ validator: matrixValidator
41405
41282
  };
41406
41283
 
41407
41284
  const defaultImage = {
@@ -41410,7 +41287,6 @@ const defaultImage = {
41410
41287
  left: 0
41411
41288
  };
41412
41289
  class Measurer extends React__namespace.Component {
41413
- static validate = noopValidator;
41414
41290
  static defaultProps = {
41415
41291
  box: [480, 480],
41416
41292
  image: {},
@@ -41423,6 +41299,10 @@ class Measurer extends React__namespace.Component {
41423
41299
  rulerPixels: 40,
41424
41300
  rulerLength: 10
41425
41301
  };
41302
+
41303
+ // this just helps with TS weak typing when a Widget
41304
+ // doesn't implement any Widget methods
41305
+ isWidget = true;
41426
41306
  state = {};
41427
41307
  ruler;
41428
41308
  protractor;
@@ -41475,12 +41355,6 @@ class Measurer extends React__namespace.Component {
41475
41355
  });
41476
41356
  }
41477
41357
  }
41478
- getUserInput() {
41479
- return {};
41480
- }
41481
- simpleValidate() {
41482
- return noopValidator(1);
41483
- }
41484
41358
  render() {
41485
41359
  const image = ___default["default"].extend({}, defaultImage, this.props.image);
41486
41360
 
@@ -41527,7 +41401,9 @@ var Measurer$1 = {
41527
41401
  major: 1,
41528
41402
  minor: 0
41529
41403
  },
41530
- propUpgrades: propUpgrades
41404
+ propUpgrades: propUpgrades,
41405
+ // TODO: things that aren't interactive shouldn't need validators
41406
+ validator: () => noopValidator(1)
41531
41407
  };
41532
41408
 
41533
41409
  /**
@@ -42400,18 +42276,9 @@ class MoleculeWidget extends React__namespace.Component {
42400
42276
  rotationAngle: 0
42401
42277
  };
42402
42278
 
42403
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
42404
- simpleValidate() {
42405
- return noopValidator();
42406
- }
42407
- getUserInput() {
42408
- return [];
42409
- }
42410
-
42411
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
42412
- validate() {
42413
- return noopValidator();
42414
- }
42279
+ // this just helps with TS weak typing when a Widget
42280
+ // doesn't implement any Widget methods
42281
+ isWidget = true;
42415
42282
  render() {
42416
42283
  return /*#__PURE__*/React__namespace.createElement(Molecule, {
42417
42284
  widgetId: this.props.widgetId,
@@ -42424,7 +42291,8 @@ var Molecule$1 = {
42424
42291
  name: "molecule-renderer",
42425
42292
  displayName: "Molecule renderer",
42426
42293
  hidden: true,
42427
- widget: MoleculeWidget
42294
+ widget: MoleculeWidget,
42295
+ validator: () => noopValidator()
42428
42296
  };
42429
42297
 
42430
42298
  function numberLineValidator(state, rubric) {
@@ -42569,18 +42437,16 @@ const TickMarks = Graphie.createSimpleClass((graphie, props) => {
42569
42437
  results.push(graphie.line([x, -0.2], [x, 0.2]));
42570
42438
  const labelTicks = props.labelTicks;
42571
42439
  if (labelTicks || props.labelStyle === "decimal ticks") {
42572
- 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
+ }
42573
42447
  }
42574
42448
  }
42575
42449
 
42576
- // Render the text labels
42577
- results.push(graphie.style(props.isMobile ? {
42578
- color: KhanColors.BLUE
42579
- } : {}, () => _label(graphie, props.labelStyle, leftLabel, leftLabel, base)));
42580
- results.push(graphie.style(props.isMobile ? {
42581
- color: KhanColors.BLUE
42582
- } : {}, () => _label(graphie, props.labelStyle, rightLabel, rightLabel, base)));
42583
-
42584
42450
  // Render the labels' lines
42585
42451
  graphie.style({
42586
42452
  stroke: KhanColors.BLUE,
@@ -42613,9 +42479,6 @@ class NumberLine extends React__namespace.Component {
42613
42479
  state = {
42614
42480
  numDivisionsEmpty: false
42615
42481
  };
42616
- static validate(state, rubric) {
42617
- return numberLineValidator(state, rubric);
42618
- }
42619
42482
  change = (() => {
42620
42483
  var _this = this;
42621
42484
  return function () {
@@ -42908,9 +42771,6 @@ class NumberLine extends React__namespace.Component {
42908
42771
  divisionRange: this.props.divisionRange
42909
42772
  };
42910
42773
  }
42911
- simpleValidate(rubric) {
42912
- return numberLineValidator(this.getUserInput(), rubric);
42913
- }
42914
42774
  render() {
42915
42775
  const {
42916
42776
  strings
@@ -43014,7 +42874,8 @@ var NumberLine$1 = {
43014
42874
  displayName: "Number line",
43015
42875
  widget: NumberLine,
43016
42876
  transform: numberLineTransform,
43017
- staticTransform: staticTransform$1
42877
+ staticTransform: staticTransform$1,
42878
+ validator: numberLineValidator
43018
42879
  };
43019
42880
 
43020
42881
  function ordererValidator(userInput, rubric) {
@@ -43224,9 +43085,6 @@ class Orderer extends React__namespace.Component {
43224
43085
  animateTo: null,
43225
43086
  grabPos: null
43226
43087
  };
43227
- static validate(userInput, rubric) {
43228
- return ordererValidator(userInput, rubric);
43229
- }
43230
43088
  UNSAFE_componentWillReceiveProps(nextProps) {
43231
43089
  if (!___default["default"].isEqual(this.props.current, nextProps.current)) {
43232
43090
  this.setState({
@@ -43466,9 +43324,6 @@ class Orderer extends React__namespace.Component {
43466
43324
  })
43467
43325
  };
43468
43326
  }
43469
- simpleValidate(rubric) {
43470
- return ordererValidator(this.getUserInput(), rubric);
43471
- }
43472
43327
  render() {
43473
43328
  // This is the card we are currently dragging
43474
43329
  const dragging = this.state.dragging && /*#__PURE__*/React__namespace.createElement(Card
@@ -43578,7 +43433,8 @@ var Orderer$1 = {
43578
43433
  displayName: "Orderer",
43579
43434
  hidden: true,
43580
43435
  widget: Orderer,
43581
- isLintable: true
43436
+ isLintable: true,
43437
+ validator: ordererValidator
43582
43438
  };
43583
43439
 
43584
43440
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -43589,15 +43445,9 @@ class PassageRefTarget extends React__namespace.Component {
43589
43445
  linterContext: PerseusLinter.linterContextDefault
43590
43446
  };
43591
43447
 
43592
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43593
- static validate() {
43594
- return noopValidator();
43595
- }
43596
-
43597
- // TODO passage-ref-target isn't interactive; remove
43598
- getUserInput() {
43599
- return null;
43600
- }
43448
+ // this just helps with TS weak typing when a Widget
43449
+ // doesn't implement any Widget methods
43450
+ isWidget = true;
43601
43451
 
43602
43452
  // TODO passage-ref-target isn't interactive; remove
43603
43453
  change = (() => {
@@ -43609,11 +43459,6 @@ class PassageRefTarget extends React__namespace.Component {
43609
43459
  return change.apply(_this, args);
43610
43460
  };
43611
43461
  })();
43612
-
43613
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43614
- simpleValidate() {
43615
- return noopValidator();
43616
- }
43617
43462
  render() {
43618
43463
  return /*#__PURE__*/React__namespace.createElement(Renderer, {
43619
43464
  content: this.props.content
@@ -43642,7 +43487,9 @@ var PassageRefTarget$1 = {
43642
43487
  major: 0,
43643
43488
  minor: 0
43644
43489
  },
43645
- isLintable: true
43490
+ isLintable: true,
43491
+ // TODO: things that aren't interactive shouldn't need validators
43492
+ validator: () => noopValidator()
43646
43493
  };
43647
43494
 
43648
43495
  /**
@@ -43654,6 +43501,10 @@ class PhetSimulation extends React__namespace.Component {
43654
43501
  static contextType = PerseusI18nContext;
43655
43502
  iframeRef = /*#__PURE__*/React__namespace.createRef();
43656
43503
  locale;
43504
+
43505
+ // this just helps with TS weak typing when a Widget
43506
+ // doesn't implement any Widget methods
43507
+ isWidget = true;
43657
43508
  state = {
43658
43509
  url: null,
43659
43510
  banner: null
@@ -43672,11 +43523,6 @@ class PhetSimulation extends React__namespace.Component {
43672
43523
  }
43673
43524
  }
43674
43525
 
43675
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43676
- getUserInput() {
43677
- return null;
43678
- }
43679
-
43680
43526
  // kaLocales and PhET locales use different formats and abbreviations.
43681
43527
  // PhET accepts different formats, i.e. kaLocale's hyphens, but it does not accept
43682
43528
  // different abbreviations, so in points of divergence of abbreviations, we need to
@@ -43896,9 +43742,6 @@ class Plotter extends React__namespace.Component {
43896
43742
  // bottom label.
43897
43743
  categoryHeights: {}
43898
43744
  };
43899
- static validate(userInput, rubric) {
43900
- return plotterValidator(userInput, rubric);
43901
- }
43902
43745
  componentDidMount() {
43903
43746
  this._isMounted = true;
43904
43747
  this.setupGraphie(this.state);
@@ -44643,9 +44486,6 @@ class Plotter extends React__namespace.Component {
44643
44486
  getUserInput() {
44644
44487
  return this.state.values;
44645
44488
  }
44646
- simpleValidate(rubric) {
44647
- return plotterValidator(this.getUserInput(), rubric);
44648
- }
44649
44489
  render() {
44650
44490
  // TODO(kevinb) actually compute the size of the graphie correctly and
44651
44491
  // make it that size so we don't have to add extra padding. The value
@@ -44671,7 +44511,8 @@ var Plotter$1 = {
44671
44511
  displayName: "Plotter",
44672
44512
  hidden: true,
44673
44513
  widget: Plotter,
44674
- staticTransform: staticTransform
44514
+ staticTransform: staticTransform,
44515
+ validator: plotterValidator
44675
44516
  };
44676
44517
 
44677
44518
  /**
@@ -44766,9 +44607,6 @@ class Sorter extends React__namespace.Component {
44766
44607
  onChange: function () {},
44767
44608
  linterContext: PerseusLinter.linterContextDefault
44768
44609
  };
44769
- static validate(userInput, rubric) {
44770
- return sorterValidator(userInput, rubric);
44771
- }
44772
44610
  state = {
44773
44611
  changed: false
44774
44612
  };
@@ -44811,9 +44649,6 @@ class Sorter extends React__namespace.Component {
44811
44649
  // @ts-expect-error - TS2339 - Property 'moveOptionToIndex' does not exist on type 'ReactInstance'.
44812
44650
  this.refs.sortable.moveOptionToIndex(option, index);
44813
44651
  };
44814
- simpleValidate(rubric) {
44815
- return sorterValidator(this.getUserInput(), rubric);
44816
- }
44817
44652
  render() {
44818
44653
  const options = shuffle(this.props.correct, this.props.problemNum, /* ensurePermuted */true);
44819
44654
  const {
@@ -44839,7 +44674,8 @@ var Sorter$1 = {
44839
44674
  name: "sorter",
44840
44675
  displayName: "Sorter",
44841
44676
  widget: Sorter,
44842
- isLintable: true
44677
+ isLintable: true,
44678
+ validator: sorterValidator
44843
44679
  };
44844
44680
 
44845
44681
  function tableValidator(state, rubric, strings) {
@@ -44947,9 +44783,6 @@ class Table extends React__namespace.Component {
44947
44783
  linterContext: PerseusLinter.linterContextDefault
44948
44784
  };
44949
44785
  }();
44950
- static validate(state, rubric, strings) {
44951
- return tableValidator(state, rubric, strings);
44952
- }
44953
44786
  _getRows = () => {
44954
44787
  return this.props.answers.length;
44955
44788
  };
@@ -44978,9 +44811,6 @@ class Table extends React__namespace.Component {
44978
44811
  headers: headers
44979
44812
  });
44980
44813
  };
44981
- simpleValidate(rubric) {
44982
- return tableValidator(this.getUserInput(), rubric, this.context.strings);
44983
- }
44984
44814
  _handleFocus = inputPath => {
44985
44815
  this.props.onFocus(inputPath);
44986
44816
  };
@@ -45124,7 +44954,8 @@ var Table$1 = {
45124
44954
  widget: Table,
45125
44955
  transform: propTransform,
45126
44956
  hidden: true,
45127
- isLintable: true
44957
+ isLintable: true,
44958
+ validator: tableValidator
45128
44959
  };
45129
44960
 
45130
44961
  /**
@@ -45210,22 +45041,9 @@ const IS_VIMEO = /(vimeo\.com)/;
45210
45041
  */
45211
45042
  class Video extends React__namespace.Component {
45212
45043
  static contextType = PerseusI18nContext;
45213
- /**
45214
- * This is the widget's grading function.
45215
- * Points for videos are tallied by the embedded video itself, in the case
45216
- * of Khan Academy videos.
45217
- */ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
45218
- static validate() {
45219
- return noopValidator();
45220
- }
45221
- getUserInput = () => {
45222
- return null;
45223
- };
45224
-
45225
- // TODO (LEMS-2396): remove validation logic from widgets that don't validate
45226
- simpleValidate() {
45227
- return noopValidator();
45228
- }
45044
+ // this just helps with TS weak typing when a Widget
45045
+ // doesn't implement any Widget methods
45046
+ isWidget = true;
45229
45047
  change = (() => {
45230
45048
  var _this = this;
45231
45049
  return function () {
@@ -45294,7 +45112,9 @@ var Video$1 = {
45294
45112
  displayName: "Video",
45295
45113
  defaultAlignment: "block",
45296
45114
  supportedAlignments: ["block", "float-left", "float-right", "full-width"],
45297
- widget: Video
45115
+ widget: Video,
45116
+ // TODO: things that aren't interactive shouldn't need validators
45117
+ validator: () => noopValidator()
45298
45118
  };
45299
45119
 
45300
45120
  /* eslint-disable import/no-named-as-default */
@@ -45338,7 +45158,7 @@ const init = function (options) {
45338
45158
 
45339
45159
  // This file is processed by a Rollup plugin (replace) to inject the production
45340
45160
  const libName = "@khanacademy/perseus";
45341
- const libVersion = "36.1.2";
45161
+ const libVersion = "37.0.0";
45342
45162
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45343
45163
 
45344
45164
  const apiVersion = {
@@ -46064,7 +45884,7 @@ class ServerItemRenderer extends React__namespace.Component {
46064
45884
  });
46065
45885
 
46066
45886
  // Call the interactionCallback, if it exists, with the current user input data
46067
- this.props.apiOptions?.interactionCallback?.(this.questionRenderer.getUserInputForWidgets());
45887
+ this.props.apiOptions?.interactionCallback?.(this.questionRenderer.getUserInputMap());
46068
45888
  };
46069
45889
  focus() {
46070
45890
  return this.questionRenderer.focus();
@@ -48690,11 +48510,12 @@ function parsePerseusItem(json) {
48690
48510
  throw new Error("Something went wrong.");
48691
48511
  }
48692
48512
  function buildRandomString() {
48513
+ let capitalize = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
48693
48514
  let randomString = "";
48694
48515
  const randomLength = Math.floor(Math.random() * 8) + 3;
48695
48516
  for (let i = 0; i < randomLength; i++) {
48696
48517
  const randomLetter = String.fromCharCode(97 + Math.floor(Math.random() * 26));
48697
- randomString += randomLetter;
48518
+ randomString += capitalize && i === 0 ? randomLetter.toUpperCase() : randomLetter;
48698
48519
  }
48699
48520
  return randomString;
48700
48521
  }
@@ -48702,9 +48523,12 @@ function buildRandomPhrase() {
48702
48523
  const phrases = [];
48703
48524
  const randomLength = Math.floor(Math.random() * 10) + 5;
48704
48525
  for (let i = 0; i < randomLength; i++) {
48705
- phrases.push(buildRandomString());
48526
+ phrases.push(buildRandomString(i === 0));
48706
48527
  }
48707
- 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]}`;
48708
48532
  }
48709
48533
  function buildTestData(testObject) {
48710
48534
  return `{"data":{"assessmentItem":{"__typename":"AssessmentItemOrError","error":null,"item":{"__typename":"AssessmentItem","id":"x890b3c70f3e8f4a6","itemData":"${testObject}","problemType":"Type 1","sha":"c7284a3ad65214b4e62bccce236d92f7f5d35941"}}}}`;