@khanacademy/perseus 41.1.1 → 41.3.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 (154) hide show
  1. package/dist/all-widgets.d.ts +1 -1
  2. package/dist/article-renderer.d.ts +2 -2
  3. package/dist/basic-widgets.d.ts +1 -1
  4. package/dist/es/index.js +1663 -397
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/extra-widgets.d.ts +1 -1
  7. package/dist/index.d.ts +4 -4
  8. package/dist/index.js +1583 -316
  9. package/dist/index.js.map +1 -1
  10. package/dist/init.d.ts +1 -8
  11. package/dist/perseus-api.d.ts +3 -3
  12. package/dist/perseus-types.d.ts +7 -2
  13. package/dist/prompt-types.d.ts +33 -0
  14. package/dist/renderer.d.ts +8 -2
  15. package/dist/server-item-renderer.d.ts +6 -4
  16. package/dist/types.d.ts +4 -2
  17. package/dist/util/parse-perseus-json/error-tracking-parse-context.d.ts +9 -0
  18. package/dist/util/parse-perseus-json/general-purpose-parsers/any.d.ts +2 -0
  19. package/dist/util/parse-perseus-json/general-purpose-parsers/array.d.ts +2 -0
  20. package/dist/util/parse-perseus-json/general-purpose-parsers/array.typetest.d.ts +1 -0
  21. package/dist/util/parse-perseus-json/general-purpose-parsers/boolean.d.ts +2 -0
  22. package/dist/util/parse-perseus-json/general-purpose-parsers/compose-parsers.d.ts +2 -0
  23. package/dist/util/parse-perseus-json/general-purpose-parsers/constant.d.ts +4 -0
  24. package/dist/util/parse-perseus-json/general-purpose-parsers/defaulted.d.ts +2 -0
  25. package/dist/util/parse-perseus-json/general-purpose-parsers/enumeration.d.ts +2 -0
  26. package/dist/util/parse-perseus-json/general-purpose-parsers/enumeration.typetest.d.ts +1 -0
  27. package/dist/util/parse-perseus-json/general-purpose-parsers/index.d.ts +15 -0
  28. package/dist/util/parse-perseus-json/general-purpose-parsers/is-object.d.ts +1 -0
  29. package/dist/util/parse-perseus-json/general-purpose-parsers/nullable.d.ts +2 -0
  30. package/dist/util/parse-perseus-json/general-purpose-parsers/number.d.ts +2 -0
  31. package/dist/util/parse-perseus-json/general-purpose-parsers/object.d.ts +6 -0
  32. package/dist/util/parse-perseus-json/general-purpose-parsers/optional.d.ts +2 -0
  33. package/dist/util/parse-perseus-json/general-purpose-parsers/pair.d.ts +2 -0
  34. package/dist/util/parse-perseus-json/general-purpose-parsers/record.d.ts +2 -0
  35. package/dist/util/parse-perseus-json/general-purpose-parsers/string.d.ts +2 -0
  36. package/dist/util/parse-perseus-json/general-purpose-parsers/test-helpers.d.ts +8 -0
  37. package/dist/util/parse-perseus-json/general-purpose-parsers/trio.d.ts +2 -0
  38. package/dist/util/parse-perseus-json/general-purpose-parsers/union.d.ts +7 -0
  39. package/dist/util/parse-perseus-json/general-purpose-parsers/union.typetest.d.ts +1 -0
  40. package/dist/util/{parse-perseus-json.d.ts → parse-perseus-json/index.d.ts} +7 -1
  41. package/dist/util/parse-perseus-json/object-path.d.ts +2 -0
  42. package/dist/util/parse-perseus-json/parse-failure-detail.d.ts +2 -0
  43. package/dist/util/parse-perseus-json/parse.d.ts +3 -0
  44. package/dist/util/parse-perseus-json/parser-types.d.ts +30 -0
  45. package/dist/util/parse-perseus-json/perseus-parsers/categorizer-widget.d.ts +3 -0
  46. package/dist/util/parse-perseus-json/perseus-parsers/cs-program-widget.d.ts +3 -0
  47. package/dist/util/parse-perseus-json/perseus-parsers/definition-widget.d.ts +3 -0
  48. package/dist/util/parse-perseus-json/perseus-parsers/dropdown-widget.d.ts +3 -0
  49. package/dist/util/parse-perseus-json/perseus-parsers/explanation-widget.d.ts +3 -0
  50. package/dist/util/parse-perseus-json/perseus-parsers/expression-widget.d.ts +3 -0
  51. package/dist/util/parse-perseus-json/perseus-parsers/hint.d.ts +3 -0
  52. package/dist/util/parse-perseus-json/perseus-parsers/interactive-graph-widget.d.ts +3 -0
  53. package/dist/util/parse-perseus-json/perseus-parsers/perseus-image-background.d.ts +3 -0
  54. package/dist/util/parse-perseus-json/perseus-parsers/perseus-item.d.ts +3 -0
  55. package/dist/util/parse-perseus-json/perseus-parsers/perseus-renderer.d.ts +3 -0
  56. package/dist/util/parse-perseus-json/perseus-parsers/widget.d.ts +3 -0
  57. package/dist/util/parse-perseus-json/perseus-parsers/widgets-map.d.ts +3 -0
  58. package/dist/util/parse-perseus-json/result.d.ts +16 -0
  59. package/dist/validation.types.d.ts +0 -3
  60. package/dist/widget-ai-utils/categorizer/prompt-utils.d.ts +14 -0
  61. package/dist/widget-ai-utils/cs-program/prompt-utils.d.ts +2 -0
  62. package/dist/widget-ai-utils/definition/prompt-utils.d.ts +8 -0
  63. package/dist/widget-ai-utils/dropdown/prompt-utils.d.ts +13 -0
  64. package/dist/widget-ai-utils/explanation/prompt-utils.d.ts +8 -0
  65. package/dist/widget-ai-utils/expression/prompt-utils.d.ts +11 -0
  66. package/dist/widget-ai-utils/graded-group/prompt-utils.d.ts +7 -0
  67. package/dist/widget-ai-utils/graded-group-set/prompt-utils.d.ts +11 -0
  68. package/dist/widget-ai-utils/grapher/prompt-utils.d.ts +18 -0
  69. package/dist/widget-ai-utils/group/prompt-utils.d.ts +5 -0
  70. package/dist/widget-ai-utils/iframe/prompt-utils.d.ts +2 -0
  71. package/dist/widget-ai-utils/image/prompt-utils.d.ts +12 -0
  72. package/dist/widget-ai-utils/input-number/prompt-utils.d.ts +14 -0
  73. package/dist/widget-ai-utils/interaction/prompt-utils.d.ts +2 -0
  74. package/dist/widget-ai-utils/interactive-graph/prompt-utils.d.ts +94 -0
  75. package/dist/widget-ai-utils/label-image/prompt-utils.d.ts +24 -0
  76. package/dist/widget-ai-utils/matcher/prompt-utils.d.ts +17 -0
  77. package/dist/widget-ai-utils/matrix/prompt-utils.d.ts +14 -0
  78. package/dist/widget-ai-utils/measurer/prompt-utils.d.ts +2 -0
  79. package/dist/widget-ai-utils/number-line/prompt-utils.d.ts +16 -0
  80. package/dist/widget-ai-utils/numeric-input/prompt-utils.d.ts +11 -0
  81. package/dist/widget-ai-utils/orderer/prompt-utils.d.ts +13 -0
  82. package/dist/widget-ai-utils/passage/prompt-utils.d.ts +11 -0
  83. package/dist/widget-ai-utils/passage-ref/prompt-utils.d.ts +11 -0
  84. package/dist/widget-ai-utils/phet-simulation/prompt-utils.d.ts +2 -0
  85. package/dist/widget-ai-utils/plotter/prompt-utils.d.ts +2 -0
  86. package/dist/widget-ai-utils/python-program/prompt-utils.d.ts +2 -0
  87. package/dist/widget-ai-utils/radio/prompt-utils.d.ts +15 -0
  88. package/dist/widget-ai-utils/sorter/prompt-utils.d.ts +9 -0
  89. package/dist/widget-ai-utils/unsupported-widget.d.ts +11 -0
  90. package/dist/widget-ai-utils/video/prompt-utils.d.ts +2 -0
  91. package/dist/widgets/__shared__/{noop-validator.d.ts → score-noop.d.ts} +4 -4
  92. package/dist/widgets/categorizer/categorizer.d.ts +10 -19
  93. package/dist/widgets/categorizer/{categorizer-validator.d.ts → score-categorizer.d.ts} +2 -2
  94. package/dist/widgets/cs-program/cs-program.d.ts +8 -19
  95. package/dist/widgets/cs-program/{cs-program-validator.d.ts → score-cs-program.d.ts} +2 -1
  96. package/dist/widgets/definition/definition.d.ts +8 -19
  97. package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +4 -19
  98. package/dist/widgets/dropdown/dropdown.d.ts +10 -19
  99. package/dist/widgets/dropdown/score-dropdown.d.ts +4 -0
  100. package/dist/widgets/explanation/explanation.d.ts +9 -19
  101. package/dist/widgets/expression/expression.d.ts +26 -290
  102. package/dist/widgets/expression/{expression-validator.d.ts → score-expression.d.ts} +2 -2
  103. package/dist/widgets/graded-group/graded-group.d.ts +13 -21
  104. package/dist/widgets/graded-group-set/graded-group-set.d.ts +9 -20
  105. package/dist/widgets/grapher/grapher.d.ts +9 -19
  106. package/dist/widgets/grapher/score-grapher.d.ts +4 -0
  107. package/dist/widgets/group/group.d.ts +7 -19
  108. package/dist/widgets/iframe/iframe.d.ts +7 -19
  109. package/dist/widgets/iframe/{iframe-validator.d.ts → score-iframe.d.ts} +1 -1
  110. package/dist/widgets/image/image.d.ts +9 -19
  111. package/dist/widgets/input-number/input-number.d.ts +11 -19
  112. package/dist/widgets/input-number/{input-number-validator.d.ts → score-input-number.d.ts} +2 -2
  113. package/dist/widgets/interaction/interaction.d.ts +7 -19
  114. package/dist/widgets/interactive-graph.d.ts +8 -19
  115. package/dist/widgets/interactive-graphs/score-interactive-graph.d.ts +4 -0
  116. package/dist/widgets/interactive-graphs/utils.d.ts +6 -0
  117. package/dist/widgets/label-image/label-image.d.ts +10 -86
  118. package/dist/widgets/label-image/{label-image-validator.d.ts → score-label-image.d.ts} +2 -2
  119. package/dist/widgets/matcher/matcher.d.ts +7 -19
  120. package/dist/widgets/matcher/score-matcher.d.ts +4 -0
  121. package/dist/widgets/matrix/matrix.d.ts +10 -19
  122. package/dist/widgets/matrix/{matrix-validator.d.ts → score-matrix.d.ts} +2 -2
  123. package/dist/widgets/measurer/measurer.d.ts +13 -19
  124. package/dist/widgets/molecule/molecule.d.ts +4 -19
  125. package/dist/widgets/number-line/number-line.d.ts +8 -19
  126. package/dist/widgets/number-line/score-number-line.d.ts +4 -0
  127. package/dist/widgets/numeric-input/numeric-input.d.ts +22 -19
  128. package/dist/widgets/numeric-input/{numeric-input-validator.d.ts → score-numeric-input.d.ts} +2 -2
  129. package/dist/widgets/orderer/orderer.d.ts +8 -19
  130. package/dist/widgets/orderer/{orderer-validator.d.ts → score-orderer.d.ts} +1 -1
  131. package/dist/widgets/passage/passage.d.ts +8 -19
  132. package/dist/widgets/passage-ref/passage-ref.d.ts +12 -19
  133. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +11 -19
  134. package/dist/widgets/phet-simulation/phet-simulation.d.ts +5 -19
  135. package/dist/widgets/plotter/plotter.d.ts +8 -19
  136. package/dist/widgets/plotter/score-plotter.d.ts +4 -0
  137. package/dist/widgets/python-program/python-program.d.ts +4 -19
  138. package/dist/widgets/radio/radio-component.d.ts +3 -1
  139. package/dist/widgets/radio/radio.d.ts +18 -19
  140. package/dist/widgets/radio/{radio-validator.d.ts → score-radio.d.ts} +2 -2
  141. package/dist/widgets/sorter/score-sorter.d.ts +4 -0
  142. package/dist/widgets/sorter/sorter.d.ts +7 -19
  143. package/dist/widgets/table/{table-validator.d.ts → score-table.d.ts} +2 -2
  144. package/dist/widgets/table/table.d.ts +8 -19
  145. package/dist/widgets/video/video.d.ts +7 -19
  146. package/dist/widgets.d.ts +2 -2
  147. package/package.json +4 -4
  148. package/dist/widgets/dropdown/dropdown-validator.d.ts +0 -4
  149. package/dist/widgets/grapher/grapher-validator.d.ts +0 -4
  150. package/dist/widgets/interactive-graphs/interactive-graph-validator.d.ts +0 -4
  151. package/dist/widgets/matcher/matcher-validator.d.ts +0 -4
  152. package/dist/widgets/number-line/number-line-validator.d.ts +0 -4
  153. package/dist/widgets/plotter/plotter-validator.d.ts +0 -4
  154. package/dist/widgets/sorter/sorter-validator.d.ts +0 -4
package/dist/es/index.js CHANGED
@@ -19,10 +19,11 @@ import $ from 'jquery';
19
19
  import { v4 } from 'uuid';
20
20
  import { PerseusError, Errors, addLibraryVersionToPerseusDebug } from '@khanacademy/perseus-core';
21
21
  import PropTypes from 'prop-types';
22
- import { number, point, vector as vector$1, line } from '@khanacademy/kmath';
22
+ import { number as number$1, point, vector as vector$1, line } from '@khanacademy/kmath';
23
23
  import { entries, UnreachableCaseError, keys } from '@khanacademy/wonder-stuff-core';
24
24
  import { CircularSpinner } from '@khanacademy/wonder-blocks-progress-spinner';
25
- import SimpleMarkdown from '@khanacademy/simple-markdown';
25
+ import SimpleMarkdown$1 from '@khanacademy/simple-markdown';
26
+ import * as SimpleMarkdown from '@khanacademy/pure-markdown';
26
27
  import { pureMarkdownRules } from '@khanacademy/pure-markdown';
27
28
  import { TextField } from '@khanacademy/wonder-blocks-form';
28
29
  import Button from '@khanacademy/wonder-blocks-button';
@@ -1168,6 +1169,42 @@ const ClassNames = {
1168
1169
  MOBILE: "perseus-mobile"
1169
1170
  };
1170
1171
 
1172
+ const getPromptJSON$t = (renderProps, userInput) => {
1173
+ return {
1174
+ type: "expression",
1175
+ label: renderProps.visibleLabel,
1176
+ userInput: {
1177
+ value: userInput
1178
+ }
1179
+ };
1180
+ };
1181
+
1182
+ /**
1183
+ * Get the character used for separating decimals.
1184
+ */
1185
+ const getDecimalSeparator = locale => {
1186
+ var _match$;
1187
+ switch (locale) {
1188
+ // TODO(somewhatabstract): Remove this when Chrome supports the `ka`
1189
+ // locale properly.
1190
+ // https://github.com/formatjs/formatjs/issues/1526#issuecomment-559891201
1191
+ //
1192
+ // Supported locales in Chrome:
1193
+ // https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/scripts/chrome_ui_languages.list
1194
+ case "ka":
1195
+ return ",";
1196
+ default:
1197
+ const numberWithDecimalSeparator = 1.1;
1198
+ // TODO(FEI-3647): Update to use .formatToParts() once we no longer have to
1199
+ // support Safari 12.
1200
+ const match = new Intl.NumberFormat(locale).format(numberWithDecimalSeparator)
1201
+ // 0x661 is ARABIC-INDIC DIGIT ONE
1202
+ // 0x6F1 is EXTENDED ARABIC-INDIC DIGIT ONE
1203
+ .match(/[^\d\u0661\u06F1]/);
1204
+ return (_match$ = match == null ? void 0 : match[0]) != null ? _match$ : ".";
1205
+ }
1206
+ };
1207
+
1171
1208
  // TODO(LP-11481): Change the primary API we use for logging in Perseus to use
1172
1209
  // getDependencies() internally... simplify usage to just using a "singleton"
1173
1210
  // which get's the injected logger.
@@ -1288,7 +1325,7 @@ const KhanMath = {
1288
1325
  toFixedApprox: function (num, precision) {
1289
1326
  // TODO(aria): Make this locale-dependent
1290
1327
  const fixedStr = num.toFixed(precision);
1291
- if (number.equal(+fixedStr, num)) {
1328
+ if (number$1.equal(+fixedStr, num)) {
1292
1329
  return fixedStr;
1293
1330
  }
1294
1331
  return "\\approx " + fixedStr;
@@ -1300,7 +1337,7 @@ const KhanMath = {
1300
1337
  */
1301
1338
  roundToApprox: function (num, precision) {
1302
1339
  const fixed = KhanMath.roundTo(precision, num);
1303
- if (number.equal(fixed, num)) {
1340
+ if (number$1.equal(fixed, num)) {
1304
1341
  return String(fixed);
1305
1342
  }
1306
1343
  return KhanMath.toFixedApprox(num, precision);
@@ -1372,32 +1409,32 @@ const KhanMath = {
1372
1409
  return null;
1373
1410
  },
1374
1411
  // Returns a string of the number in a specified format
1375
- toNumericString: function (number$1, format) {
1376
- if (number$1 == null) {
1412
+ toNumericString: function (number, format) {
1413
+ if (number == null) {
1377
1414
  return "";
1378
1415
  }
1379
- if (number$1 === 0) {
1416
+ if (number === 0) {
1380
1417
  return "0"; // otherwise it might end up as 0% or 0pi
1381
1418
  }
1382
1419
 
1383
1420
  if (format === "percent") {
1384
- return number$1 * 100 + "%";
1421
+ return number * 100 + "%";
1385
1422
  }
1386
1423
  if (format === "pi") {
1387
- const fraction = number.toFraction(number$1 / Math.PI);
1424
+ const fraction = number$1.toFraction(number / Math.PI);
1388
1425
  const numerator = Math.abs(fraction[0]);
1389
1426
  const denominator = fraction[1];
1390
- if (number.isInteger(numerator)) {
1391
- const sign = number$1 < 0 ? "-" : "";
1427
+ if (number$1.isInteger(numerator)) {
1428
+ const sign = number < 0 ? "-" : "";
1392
1429
  const pi = "\u03C0";
1393
1430
  return sign + (numerator === 1 ? "" : numerator) + pi + (denominator === 1 ? "" : "/" + denominator);
1394
1431
  }
1395
1432
  }
1396
1433
  if (_(["proper", "improper", "mixed", "fraction"]).contains(format)) {
1397
- const fraction = number.toFraction(number$1);
1434
+ const fraction = number$1.toFraction(number);
1398
1435
  const numerator = Math.abs(fraction[0]);
1399
1436
  const denominator = fraction[1];
1400
- const sign = number$1 < 0 ? "-" : "";
1437
+ const sign = number < 0 ? "-" : "";
1401
1438
  if (denominator === 1) {
1402
1439
  return sign + numerator; // for integers, irrational, d > 1000
1403
1440
  }
@@ -1411,7 +1448,7 @@ const KhanMath = {
1411
1448
  }
1412
1449
 
1413
1450
  // otherwise (decimal, float, long long)
1414
- return String(number$1);
1451
+ return String(number);
1415
1452
  }
1416
1453
  };
1417
1454
  function sum$1(array) {
@@ -2099,32 +2136,6 @@ const KhanAnswerTypes = {
2099
2136
  }
2100
2137
  };
2101
2138
 
2102
- /**
2103
- * Get the character used for separating decimals.
2104
- */
2105
- const getDecimalSeparator = locale => {
2106
- var _match$;
2107
- switch (locale) {
2108
- // TODO(somewhatabstract): Remove this when Chrome supports the `ka`
2109
- // locale properly.
2110
- // https://github.com/formatjs/formatjs/issues/1526#issuecomment-559891201
2111
- //
2112
- // Supported locales in Chrome:
2113
- // https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/scripts/chrome_ui_languages.list
2114
- case "ka":
2115
- return ",";
2116
- default:
2117
- const numberWithDecimalSeparator = 1.1;
2118
- // TODO(FEI-3647): Update to use .formatToParts() once we no longer have to
2119
- // support Safari 12.
2120
- const match = new Intl.NumberFormat(locale).format(numberWithDecimalSeparator)
2121
- // 0x661 is ARABIC-INDIC DIGIT ONE
2122
- // 0x6F1 is EXTENDED ARABIC-INDIC DIGIT ONE
2123
- .match(/[^\d\u0661\u06F1]/);
2124
- return (_match$ = match == null ? void 0 : match[0]) != null ? _match$ : ".";
2125
- }
2126
- };
2127
-
2128
2139
  /* Content creators input a list of answers which are matched from top to
2129
2140
  * bottom. The intent is that they can include spcific solutions which should
2130
2141
  * be graded as correct or incorrect (or ungraded!) first, then get more
@@ -2143,7 +2154,7 @@ const getDecimalSeparator = locale => {
2143
2154
  * show the user an error. TODO(joel) - what error?
2144
2155
  * - Otherwise, pass through the resulting points and message.
2145
2156
  */
2146
- function expressionValidator(userInput, rubric, strings, locale) {
2157
+ function scoreExpression(userInput, rubric, strings, locale) {
2147
2158
  const options = _.clone(rubric);
2148
2159
  _.extend(options, {
2149
2160
  decimal_separator: getDecimalSeparator(locale)
@@ -2393,6 +2404,9 @@ class Expression extends React.Component {
2393
2404
  getUserInput() {
2394
2405
  return Expression.getUserInputFromProps(this.props);
2395
2406
  }
2407
+ getPromptJSON() {
2408
+ return getPromptJSON$t(this.props, this.getUserInput());
2409
+ }
2396
2410
  focusInputPath(inputPath) {
2397
2411
  // eslint-disable-next-line react/no-string-refs
2398
2412
  // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
@@ -2643,7 +2657,7 @@ var Expression$1 = {
2643
2657
  propUpgrades: propUpgrades$2,
2644
2658
  // For use by the editor
2645
2659
  isLintable: true,
2646
- validator: expressionValidator,
2660
+ scorer: scoreExpression,
2647
2661
  getOneCorrectAnswerFromRubric(rubric) {
2648
2662
  const correctAnswers = (rubric.answerForms || []).filter(answerForm => answerForm.considered === "correct");
2649
2663
  if (correctAnswers.length === 0) {
@@ -9330,7 +9344,7 @@ class Graphie$1 {
9330
9344
  // We round the coordinates to make testing easier, because trig
9331
9345
  // operations return unround numbers.
9332
9346
  // TODO(benchristel): move rounding to scalePoint?
9333
- const round = x => number.round(x, 6);
9347
+ const round = x => number$1.round(x, 6);
9334
9348
  const startPoint = this.scalePoint([round(center[0] + startVector[0]), round(center[1] + startVector[1])]);
9335
9349
  const endPoint = this.scalePoint([round(center[0] + endVector[0]), round(center[1] + endVector[1])]);
9336
9350
  const largeAngle = ((endAngle - startAngle) % 360 + 360) % 360 > 180;
@@ -10887,7 +10901,7 @@ function similar(coords1, coords2, tolerance) {
10887
10901
  return eq$3(factors[0], factor);
10888
10902
  });
10889
10903
  const congruentEnough = _.all(sidePairs, function (pair) {
10890
- return number.equal(pair[0], pair[1], tolerance);
10904
+ return number$1.equal(pair[0], pair[1], tolerance);
10891
10905
  });
10892
10906
  if (same && congruentEnough) {
10893
10907
  return true;
@@ -17914,7 +17928,7 @@ class InteractionTracker {
17914
17928
  */
17915
17929
  const arrayRules = {
17916
17930
  fence: {
17917
- match: SimpleMarkdown.defaultRules.fence.match,
17931
+ match: SimpleMarkdown$1.defaultRules.fence.match,
17918
17932
  order: 1,
17919
17933
  parse: (capture, state, parse) => ({
17920
17934
  type: "codeBlock",
@@ -17923,14 +17937,14 @@ const arrayRules = {
17923
17937
  })
17924
17938
  },
17925
17939
  paragraph: {
17926
- match: SimpleMarkdown.defaultRules.paragraph.match,
17940
+ match: SimpleMarkdown$1.defaultRules.paragraph.match,
17927
17941
  order: 2,
17928
17942
  parse: (capture, state, parse) => ({
17929
17943
  content: capture[1]
17930
17944
  })
17931
17945
  }
17932
17946
  };
17933
- const builtArrayParser = SimpleMarkdown.parserFor(arrayRules);
17947
+ const builtArrayParser = SimpleMarkdown$1.parserFor(arrayRules);
17934
17948
 
17935
17949
  // This should just return an array of strings! magick!
17936
17950
  const parseToArray = source => {
@@ -18355,7 +18369,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18355
18369
  // iOS 14.4, in order for VoiceOver to work.
18356
18370
  table: _extends({}, pureMarkdownRules.table, {
18357
18371
  react: function (node, output, state) {
18358
- const table = SimpleMarkdown.defaultRules.table.react(node, output, state);
18372
+ const table = SimpleMarkdown$1.defaultRules.table.react(node, output, state);
18359
18373
  if (!state.isMobile) {
18360
18374
  return table;
18361
18375
  }
@@ -18404,7 +18418,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18404
18418
  contents = rules$1.lint.react(node.table, output, state);
18405
18419
  }
18406
18420
  } else {
18407
- const tableOutput = SimpleMarkdown.defaultRules.table.react(node.table, output, state);
18421
+ const tableOutput = SimpleMarkdown$1.defaultRules.table.react(node.table, output, state);
18408
18422
  const caption = /*#__PURE__*/React.createElement("caption", {
18409
18423
  key: "caption",
18410
18424
  className: "perseus-table-title"
@@ -18474,7 +18488,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18474
18488
  // window and handle reverse tabnapping phishing attacks
18475
18489
  link: _extends({}, pureMarkdownRules.link, {
18476
18490
  react: function (node, output, state) {
18477
- const link = SimpleMarkdown.defaultRules.link.react(node, output, state);
18491
+ const link = SimpleMarkdown$1.defaultRules.link.react(node, output, state);
18478
18492
  const href = link.props.href;
18479
18493
  let rel = null;
18480
18494
  // The href prop should always be here, but just in case somehow the article's
@@ -18507,7 +18521,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18507
18521
  className: "perseus-markdown-alt perseus-sr-only"
18508
18522
  }, output(node.content, state));
18509
18523
  }
18510
- return SimpleMarkdown.defaultRules.codeBlock.react(node, output, state);
18524
+ return SimpleMarkdown$1.defaultRules.codeBlock.react(node, output, state);
18511
18525
  }
18512
18526
  }),
18513
18527
  // The lint rule never actually matches anything.
@@ -18549,7 +18563,7 @@ const inlineNodeTypes = {
18549
18563
  };
18550
18564
 
18551
18565
  // @ts-expect-error - TS2345 - Argument of type '{ readonly columns: { readonly react: (node: any, output: any, state: any) => Element; readonly order: -2; readonly match: any; readonly parse: (capture: any, parse: any, state: any) => any; }; ... 35 more ...; readonly text: TextInOutRule; }' is not assignable to parameter of type 'ParserRules'.
18552
- const builtParser$1 = SimpleMarkdown.parserFor(rules$1);
18566
+ const builtParser$1 = SimpleMarkdown$1.parserFor(rules$1);
18553
18567
  const parse$3 = (source, state) => {
18554
18568
  const paragraphedSource = source + "\n\n";
18555
18569
  return builtParser$1(paragraphedSource, _extends({}, state, {
@@ -18642,10 +18656,10 @@ var PerseusMarkdown = {
18642
18656
  traverseContent: traverseContent,
18643
18657
  parse: parse$3,
18644
18658
  parseInline: inlineParser,
18645
- reactFor: SimpleMarkdown.reactFor,
18646
- ruleOutput: SimpleMarkdown.ruleOutput(rules$1, "react"),
18647
- basicOutput: SimpleMarkdown.reactFor(SimpleMarkdown.ruleOutput(rules$1, "react")),
18648
- sanitizeUrl: SimpleMarkdown.sanitizeUrl
18659
+ reactFor: SimpleMarkdown$1.reactFor,
18660
+ ruleOutput: SimpleMarkdown$1.ruleOutput(rules$1, "react"),
18661
+ basicOutput: SimpleMarkdown$1.reactFor(SimpleMarkdown$1.ruleOutput(rules$1, "react")),
18662
+ sanitizeUrl: SimpleMarkdown$1.sanitizeUrl
18649
18663
  };
18650
18664
 
18651
18665
  class QuestionParagraph extends React.Component {
@@ -18766,9 +18780,9 @@ const getWidgetExport = name => {
18766
18780
  var _widgets$name3;
18767
18781
  return (_widgets$name3 = widgets[name]) != null ? _widgets$name3 : null;
18768
18782
  };
18769
- const getWidgetValidator = name => {
18770
- var _widgets$name$validat, _widgets$name4;
18771
- return (_widgets$name$validat = (_widgets$name4 = widgets[name]) == null ? void 0 : _widgets$name4.validator) != null ? _widgets$name$validat : null;
18783
+ const getWidgetScorer = name => {
18784
+ var _widgets$name$scorer, _widgets$name4;
18785
+ return (_widgets$name$scorer = (_widgets$name4 = widgets[name]) == null ? void 0 : _widgets$name4.scorer) != null ? _widgets$name$scorer : null;
18772
18786
  };
18773
18787
  const getEditor = name => {
18774
18788
  return _.has(editors, name) ? editors[name] : null;
@@ -19073,7 +19087,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19073
19087
  replaceDeprecatedEditors: replaceDeprecatedEditors,
19074
19088
  getWidget: getWidget,
19075
19089
  getWidgetExport: getWidgetExport,
19076
- getWidgetValidator: getWidgetValidator,
19090
+ getWidgetScorer: getWidgetScorer,
19077
19091
  getEditor: getEditor,
19078
19092
  getTransform: getTransform,
19079
19093
  getVersion: getVersion,
@@ -19105,12 +19119,12 @@ widgetIds, userInputMap, strings, locale) {
19105
19119
  }
19106
19120
  let score = null;
19107
19121
  const userInput = userInputMap[id];
19108
- const validator = getWidgetValidator(widget.type);
19122
+ const scorer = getWidgetScorer(widget.type);
19109
19123
  if (widget.type === "group") {
19110
19124
  const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19111
19125
  score = Util.flattenScores(scores);
19112
- } else if (validator) {
19113
- score = validator(userInput, widget.options, strings, locale);
19126
+ } else if (scorer) {
19127
+ score = scorer(userInput, widget.options, strings, locale);
19114
19128
  }
19115
19129
  if (score) {
19116
19130
  return Util.scoreIsEmpty(score);
@@ -19135,12 +19149,12 @@ widgetIds, userInputMap, strings, locale) {
19135
19149
  return;
19136
19150
  }
19137
19151
  const userInput = userInputMap[id];
19138
- const validator = getWidgetValidator(widget.type);
19152
+ const scorer = getWidgetScorer(widget.type);
19139
19153
  if (widget.type === "group") {
19140
19154
  const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19141
19155
  widgetScores[id] = Util.flattenScores(scores);
19142
- } else if (validator) {
19143
- widgetScores[id] = validator(userInput, widget.options, strings, locale);
19156
+ } else if (scorer) {
19157
+ widgetScores[id] = scorer(userInput, widget.options, strings, locale);
19144
19158
  }
19145
19159
  });
19146
19160
  return widgetScores;
@@ -19599,42 +19613,6 @@ class Renderer extends React.Component {
19599
19613
  }
19600
19614
  return null;
19601
19615
  };
19602
- this.getWidgetProps = id => {
19603
- const apiOptions = this.getApiOptions();
19604
- const widgetProps = this.state.widgetProps[id] || {};
19605
-
19606
- // The widget needs access to its "rubric" at all times when in review
19607
- // mode (which is really just part of its widget info).
19608
- const widgetInfo = this.state.widgetInfo[id];
19609
- const reviewModeRubric = this.props.reviewMode && widgetInfo ? widgetInfo.options : null;
19610
- if (!this._interactionTrackers) {
19611
- this._interactionTrackers = {};
19612
- }
19613
- let interactionTracker = this._interactionTrackers[id];
19614
- if (!interactionTracker) {
19615
- interactionTracker = this._interactionTrackers[id] = new InteractionTracker(apiOptions.trackInteraction, widgetInfo && widgetInfo.type, id, getTracking(widgetInfo && widgetInfo.type));
19616
- }
19617
- return _extends({}, widgetProps, {
19618
- widgetId: id,
19619
- alignment: widgetInfo && widgetInfo.alignment,
19620
- static: widgetInfo == null ? void 0 : widgetInfo.static,
19621
- problemNum: this.props.problemNum,
19622
- apiOptions: this.getApiOptions(),
19623
- keypadElement: this.props.keypadElement,
19624
- questionCompleted: this.props.questionCompleted,
19625
- showSolutions: this.props.showSolutions,
19626
- onFocus: _.partial(this._onWidgetFocus, id),
19627
- onBlur: _.partial(this._onWidgetBlur, id),
19628
- findWidgets: this.findWidgets,
19629
- reviewModeRubric: reviewModeRubric,
19630
- reviewMode: this.props.reviewMode,
19631
- onChange: (newProps, cb, silent = false) => {
19632
- this._setWidgetProps(id, newProps, cb, silent);
19633
- },
19634
- trackInteraction: interactionTracker.track,
19635
- isLastUsedWidget: id === this.state.lastUsedWidgetId
19636
- });
19637
- };
19638
19616
  /**
19639
19617
  * Serializes the questions state so it can be recovered.
19640
19618
  *
@@ -20583,9 +20561,9 @@ class Renderer extends React.Component {
20583
20561
  // WidgetContainers don't update their widgets' props when
20584
20562
  // they are re-rendered, so even if they've been
20585
20563
  // re-rendered we need to call these methods on them.
20586
- _.each(this.widgetIds, id => {
20564
+ this.widgetIds.forEach(id => {
20587
20565
  const container = this._widgetContainers.get(makeContainerId(id));
20588
- container && container.replaceWidgetProps(this.getWidgetProps(id));
20566
+ container == null || container.replaceWidgetProps(this.getWidgetProps(id));
20589
20567
  });
20590
20568
  if (this.props.serializedState && !_.isEqual(this.props.serializedState, this.getSerializedState())) {
20591
20569
  this.restoreSerializedState(this.props.serializedState);
@@ -20614,6 +20592,42 @@ class Renderer extends React.Component {
20614
20592
  }
20615
20593
  this._isMounted = false;
20616
20594
  }
20595
+ getWidgetProps(widgetId) {
20596
+ const apiOptions = this.getApiOptions();
20597
+ const widgetProps = this.state.widgetProps[widgetId] || {};
20598
+
20599
+ // The widget needs access to its "rubric" at all times when in review
20600
+ // mode (which is really just part of its widget info).
20601
+ const widgetInfo = this.state.widgetInfo[widgetId];
20602
+ const reviewModeRubric = this.props.reviewMode && widgetInfo ? widgetInfo.options : null;
20603
+ if (!this._interactionTrackers) {
20604
+ this._interactionTrackers = {};
20605
+ }
20606
+ let interactionTracker = this._interactionTrackers[widgetId];
20607
+ if (!interactionTracker) {
20608
+ interactionTracker = this._interactionTrackers[widgetId] = new InteractionTracker(apiOptions.trackInteraction, widgetInfo && widgetInfo.type, widgetId, getTracking(widgetInfo && widgetInfo.type));
20609
+ }
20610
+ return _extends({}, widgetProps, {
20611
+ widgetId: widgetId,
20612
+ alignment: widgetInfo && widgetInfo.alignment,
20613
+ static: widgetInfo == null ? void 0 : widgetInfo.static,
20614
+ problemNum: this.props.problemNum,
20615
+ apiOptions: this.getApiOptions(),
20616
+ keypadElement: this.props.keypadElement,
20617
+ questionCompleted: this.props.questionCompleted,
20618
+ showSolutions: this.props.showSolutions,
20619
+ onFocus: _.partial(this._onWidgetFocus, widgetId),
20620
+ onBlur: _.partial(this._onWidgetBlur, widgetId),
20621
+ findWidgets: this.findWidgets,
20622
+ reviewModeRubric: reviewModeRubric,
20623
+ reviewMode: this.props.reviewMode,
20624
+ onChange: (newProps, cb, silent = false) => {
20625
+ this._setWidgetProps(widgetId, newProps, cb, silent);
20626
+ },
20627
+ trackInteraction: interactionTracker.track,
20628
+ isLastUsedWidget: widgetId === this.state.lastUsedWidgetId
20629
+ });
20630
+ }
20617
20631
  emptyWidgets() {
20618
20632
  return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20619
20633
  }
@@ -20650,6 +20664,25 @@ class Renderer extends React.Component {
20650
20664
  });
20651
20665
  return userInputMap;
20652
20666
  }
20667
+ /**
20668
+ * Returns a JSON representation of the content and widgets
20669
+ * that can be passed to an LLM for prompt context.
20670
+ */
20671
+ getPromptJSON() {
20672
+ const {
20673
+ content
20674
+ } = this.props;
20675
+ const widgetJSON = {};
20676
+ this.widgetIds.forEach(id => {
20677
+ const widget = this.getWidgetInstance(id);
20678
+ widgetJSON[id] = (widget == null || widget.getPromptJSON == null ? void 0 : widget.getPromptJSON()) || {};
20679
+ });
20680
+ return {
20681
+ content,
20682
+ widgets: widgetJSON
20683
+ };
20684
+ }
20685
+
20653
20686
  /**
20654
20687
  * Returns an object mapping from widget ID to perseus-style score.
20655
20688
  * The keys of this object are the values of the array returned
@@ -21389,6 +21422,19 @@ SimpleKeypadInput.propTypes = {
21389
21422
  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
21390
21423
  };
21391
21424
 
21425
+ const getPromptJSON$s = (renderProps, userInput) => {
21426
+ return {
21427
+ type: "input-number",
21428
+ options: {
21429
+ simplify: renderProps.simplify,
21430
+ answerType: renderProps.answerType
21431
+ },
21432
+ userInput: {
21433
+ value: userInput.currentValue
21434
+ }
21435
+ };
21436
+ };
21437
+
21392
21438
  /*
21393
21439
  * In this file, an `expression` is some portion of valid TeX enclosed in
21394
21440
  * curly brackets.
@@ -21579,7 +21625,7 @@ const answerTypes = {
21579
21625
  forms: "pi"
21580
21626
  }
21581
21627
  };
21582
- function inputNumberValidator(userInput, rubric, strings) {
21628
+ function scoreInputNumber(userInput, rubric, strings) {
21583
21629
  if (rubric.answerType == null) {
21584
21630
  rubric.answerType = "number";
21585
21631
  }
@@ -21713,6 +21759,9 @@ class InputNumber extends React.Component {
21713
21759
  getUserInput() {
21714
21760
  return InputNumber.getUserInputFromProps(this.props);
21715
21761
  }
21762
+ getPromptJSON() {
21763
+ return getPromptJSON$s(this.props, this.getUserInput());
21764
+ }
21716
21765
  render() {
21717
21766
  if (this.props.apiOptions.customKeypad) {
21718
21767
  // TODO(charlie): Support "Review Mode".
@@ -21814,7 +21863,7 @@ var InputNumber$1 = {
21814
21863
  widget: InputNumber,
21815
21864
  transform: propTransform$3,
21816
21865
  isLintable: true,
21817
- validator: inputNumberValidator,
21866
+ scorer: scoreInputNumber,
21818
21867
  getOneCorrectAnswerFromRubric(rubric) {
21819
21868
  if (rubric.value == null) {
21820
21869
  return;
@@ -21827,6 +21876,16 @@ var InputNumber$1 = {
21827
21876
  }
21828
21877
  };
21829
21878
 
21879
+ const getPromptJSON$r = (renderProps, userInput) => {
21880
+ return {
21881
+ type: "numeric-input",
21882
+ label: renderProps.labelText,
21883
+ userInput: {
21884
+ value: userInput.currentValue
21885
+ }
21886
+ };
21887
+ };
21888
+
21830
21889
  const ParseTex = TexWrangler.parseTex;
21831
21890
  const answerFormButtons = [{
21832
21891
  title: "Integers",
@@ -21885,7 +21944,7 @@ function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
21885
21944
  // Otherwise, we return input valuåe (number) stripped of the "%".
21886
21945
  return value;
21887
21946
  }
21888
- function numericInputValidator(userInput, rubric, strings) {
21947
+ function scoreNumericInput(userInput, rubric, strings) {
21889
21948
  var _matchedAnswer$messag;
21890
21949
  const defaultAnswerForms = answerFormButtons.map(e => e["value"])
21891
21950
  // Don't default to validating the answer as a pi answer
@@ -22056,6 +22115,9 @@ class NumericInput extends React.Component {
22056
22115
  getUserInput() {
22057
22116
  return NumericInput.getUserInputFromProps(this.props);
22058
22117
  }
22118
+ getPromptJSON() {
22119
+ return getPromptJSON$r(this.props, this.getUserInput());
22120
+ }
22059
22121
  render() {
22060
22122
  let labelText = this.props.labelText;
22061
22123
  if (labelText == null || labelText === "") {
@@ -22180,7 +22242,7 @@ var NumericInput$1 = {
22180
22242
  widget: NumericInput,
22181
22243
  transform: propsTransform,
22182
22244
  isLintable: true,
22183
- validator: numericInputValidator,
22245
+ scorer: scoreNumericInput,
22184
22246
  getOneCorrectAnswerFromRubric(rubric) {
22185
22247
  const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
22186
22248
  const answerStrings = correctAnswers.map(answer => {
@@ -22211,15 +22273,43 @@ var NumericInput$1 = {
22211
22273
  }
22212
22274
  };
22213
22275
 
22276
+ const getPromptJSON$q = (renderProps, userInput) => {
22277
+ const choices = renderProps.choices || [];
22278
+ const options = choices.map(choice => {
22279
+ return {
22280
+ value: choice.content
22281
+ };
22282
+ });
22283
+ return {
22284
+ type: "radio",
22285
+ hasNoneOfTheAbove: !!renderProps.hasNoneOfTheAbove,
22286
+ options,
22287
+ userInput: {
22288
+ selectedOptions: userInput.choicesSelected.slice()
22289
+ }
22290
+ };
22291
+ };
22292
+
22293
+ const getPromptJSON$p = renderProps => {
22294
+ return {
22295
+ type: "passage-ref",
22296
+ options: {
22297
+ passageNumber: renderProps.passageNumber,
22298
+ referenceNumber: renderProps.referenceNumber,
22299
+ summaryText: renderProps.summaryText
22300
+ }
22301
+ };
22302
+ };
22303
+
22214
22304
  /**
22215
- * Several widgets don't have "right"/"wrong" validation logic,
22305
+ * Several widgets don't have "right"/"wrong" scoring logic,
22216
22306
  * so this just says to move on past those widgets
22217
22307
  *
22218
22308
  * TODO(LEMS-2543) widgets that use this probably shouldn't have any
22219
- * validation logic and the thing validating an exercise
22309
+ * scoring logic and the thing scoring an exercise
22220
22310
  * should just know to skip these
22221
22311
  */
22222
- function noopValidator(points = 0) {
22312
+ function scoreNoop(points = 0) {
22223
22313
  return {
22224
22314
  type: "points",
22225
22315
  earned: points,
@@ -23603,6 +23693,17 @@ const styles$x = StyleSheet.create({
23603
23693
  }
23604
23694
  });
23605
23695
 
23696
+ const getPromptJSON$o = renderProps => {
23697
+ return {
23698
+ type: "passage",
23699
+ options: {
23700
+ passageTitle: renderProps.passageTitle,
23701
+ passageText: renderProps.passageText,
23702
+ footnotes: renderProps.footnotes
23703
+ }
23704
+ };
23705
+ };
23706
+
23606
23707
  function getInitialParseState() {
23607
23708
  return {
23608
23709
  currentRef: [],
@@ -23652,12 +23753,12 @@ const AltText = ({
23652
23753
  }));
23653
23754
  };
23654
23755
  const rules = {
23655
- newline: SimpleMarkdown.defaultRules.newline,
23656
- paragraph: SimpleMarkdown.defaultRules.paragraph,
23657
- escape: SimpleMarkdown.defaultRules.escape,
23756
+ newline: SimpleMarkdown$1.defaultRules.newline,
23757
+ paragraph: SimpleMarkdown$1.defaultRules.paragraph,
23758
+ escape: SimpleMarkdown$1.defaultRules.escape,
23658
23759
  passageFootnote: {
23659
- order: SimpleMarkdown.defaultRules.escape.order + 0.1,
23660
- match: SimpleMarkdown.inlineRegex(/^\^/),
23760
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.1,
23761
+ match: SimpleMarkdown$1.inlineRegex(/^\^/),
23661
23762
  parse: (capture, parse, state) => {
23662
23763
  // if no footnotes have been seen, we're id 1. otherwise,
23663
23764
  // we're the next subsequent id
@@ -23686,7 +23787,7 @@ const rules = {
23686
23787
  }
23687
23788
  },
23688
23789
  refStart: {
23689
- order: SimpleMarkdown.defaultRules.escape.order + 0.2,
23790
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.2,
23690
23791
  match: function (source) {
23691
23792
  const capture = /^\{\{/.exec(source);
23692
23793
  if (capture) {
@@ -23772,8 +23873,8 @@ const rules = {
23772
23873
  }
23773
23874
  },
23774
23875
  refEnd: {
23775
- order: SimpleMarkdown.defaultRules.escape.order + 0.3,
23776
- match: SimpleMarkdown.inlineRegex(/^\}\}/),
23876
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.3,
23877
+ match: SimpleMarkdown$1.inlineRegex(/^\}\}/),
23777
23878
  parse: (capture, parse, state) => {
23778
23879
  if (!state.useRefs) {
23779
23880
  return {
@@ -23801,8 +23902,8 @@ const rules = {
23801
23902
  }
23802
23903
  },
23803
23904
  squareLabel: {
23804
- order: SimpleMarkdown.defaultRules.escape.order + 0.4,
23805
- match: SimpleMarkdown.inlineRegex(/^\[\[(\w+)\]\]( *)/),
23905
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.4,
23906
+ match: SimpleMarkdown$1.inlineRegex(/^\[\[(\w+)\]\]( *)/),
23806
23907
  parse: (capture, parse, state) => {
23807
23908
  if (!state.firstQuestionRef) {
23808
23909
  state.firstQuestionRef = capture[1];
@@ -23828,8 +23929,8 @@ const rules = {
23828
23929
  }
23829
23930
  },
23830
23931
  circleLabel: {
23831
- order: SimpleMarkdown.defaultRules.escape.order + 0.5,
23832
- match: SimpleMarkdown.inlineRegex(/^\(\((\w+)\)\)( *)/),
23932
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.5,
23933
+ match: SimpleMarkdown$1.inlineRegex(/^\(\((\w+)\)\)( *)/),
23833
23934
  parse: (capture, parse, state) => {
23834
23935
  return {
23835
23936
  content: capture[1],
@@ -23853,8 +23954,8 @@ const rules = {
23853
23954
  }
23854
23955
  },
23855
23956
  squareBracketRef: {
23856
- order: SimpleMarkdown.defaultRules.escape.order + 0.6,
23857
- match: SimpleMarkdown.inlineRegex(/^\[(\d+)\]( *)/),
23957
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.6,
23958
+ match: SimpleMarkdown$1.inlineRegex(/^\[(\d+)\]( *)/),
23858
23959
  parse: (capture, parse, state) => {
23859
23960
  if (!state.firstSentenceRef) {
23860
23961
  state.firstSentenceRef = capture[1];
@@ -23877,8 +23978,8 @@ const rules = {
23877
23978
  }
23878
23979
  },
23879
23980
  highlight: {
23880
- order: SimpleMarkdown.defaultRules.escape.order + 0.7,
23881
- match: SimpleMarkdown.inlineRegex(/^{highlighting.start}(.+?){highlighting.end}/),
23981
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.7,
23982
+ match: SimpleMarkdown$1.inlineRegex(/^{highlighting.start}(.+?){highlighting.end}/),
23882
23983
  parse: (capture, parse, state) => {
23883
23984
  return {
23884
23985
  content: capture[1]
@@ -23892,8 +23993,8 @@ const rules = {
23892
23993
  }
23893
23994
  },
23894
23995
  reviewHighlight: {
23895
- order: SimpleMarkdown.defaultRules.escape.order + 0.7,
23896
- match: SimpleMarkdown.inlineRegex(/^{review-highlighting.start}(.+?){review-highlighting.end}/),
23996
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.7,
23997
+ match: SimpleMarkdown$1.inlineRegex(/^{review-highlighting.start}(.+?){review-highlighting.end}/),
23897
23998
  parse: (capture, parse, state) => {
23898
23999
  return {
23899
24000
  content: capture[1]
@@ -23906,11 +24007,11 @@ const rules = {
23906
24007
  }, node.content)];
23907
24008
  }
23908
24009
  },
23909
- strong: SimpleMarkdown.defaultRules.strong,
23910
- u: SimpleMarkdown.defaultRules.u,
23911
- em: SimpleMarkdown.defaultRules.em,
23912
- del: SimpleMarkdown.defaultRules.del,
23913
- text: _extends({}, SimpleMarkdown.defaultRules.text, {
24010
+ strong: SimpleMarkdown$1.defaultRules.strong,
24011
+ u: SimpleMarkdown$1.defaultRules.u,
24012
+ em: SimpleMarkdown$1.defaultRules.em,
24013
+ del: SimpleMarkdown$1.defaultRules.del,
24014
+ text: _extends({}, SimpleMarkdown$1.defaultRules.text, {
23914
24015
  react(node, output, state) {
23915
24016
  return /*#__PURE__*/React.createElement("span", {
23916
24017
  key: state.key
@@ -23951,7 +24052,7 @@ const CIRCLE_LABEL_STYLE = {
23951
24052
  borderRadius: "50%",
23952
24053
  textAlign: "center"
23953
24054
  };
23954
- const builtParser = SimpleMarkdown.parserFor(rules);
24055
+ const builtParser = SimpleMarkdown$1.parserFor(rules);
23955
24056
  const parse$2 = (source, state) => {
23956
24057
  // @ts-expect-error - TS2322 - Type 'ParseState | {}' is not assignable to type 'ParseState | null | undefined'.
23957
24058
  state = state || {};
@@ -23960,7 +24061,7 @@ const parse$2 = (source, state) => {
23960
24061
  };
23961
24062
 
23962
24063
  // @ts-expect-error - TS2322 - Type 'ReactOutput' is not assignable to type '(arg1: SingleASTNode[]) => ReactElement<any, string | JSXElementConstructor<any>>'.
23963
- const output = SimpleMarkdown.reactFor(SimpleMarkdown.ruleOutput(rules, "react"));
24064
+ const output = SimpleMarkdown$1.reactFor(SimpleMarkdown$1.ruleOutput(rules, "react"));
23964
24065
  var PassageMarkdown = {
23965
24066
  parse: parse$2,
23966
24067
  output,
@@ -24233,6 +24334,9 @@ class Passage extends React.Component {
24233
24334
  content: refContent
24234
24335
  };
24235
24336
  }
24337
+ getPromptJSON() {
24338
+ return getPromptJSON$o(this.props);
24339
+ }
24236
24340
 
24237
24341
  /**
24238
24342
  * Rendering
@@ -24387,7 +24491,7 @@ var Passage$1 = {
24387
24491
  return _.pick(editorProps, "passageTitle", "passageText", "footnotes", "showLineNumbers");
24388
24492
  },
24389
24493
  isLintable: true,
24390
- validator: () => noopValidator()
24494
+ scorer: () => scoreNoop()
24391
24495
  };
24392
24496
 
24393
24497
  /**
@@ -24462,6 +24566,9 @@ class PassageRef extends React.Component {
24462
24566
  window.removeEventListener("resize", this._throttledUpdateRange);
24463
24567
  this._isMounted = false;
24464
24568
  }
24569
+ getPromptJSON() {
24570
+ return getPromptJSON$p(this.props);
24571
+ }
24465
24572
  render() {
24466
24573
  const {
24467
24574
  strings
@@ -24517,8 +24624,8 @@ var PassageRef$1 = {
24517
24624
  major: 0,
24518
24625
  minor: 1
24519
24626
  },
24520
- // TODO: things that aren't interactive shouldn't need validators
24521
- validator: () => noopValidator()
24627
+ // TODO: things that aren't interactive shouldn't need scoring functions
24628
+ scorer: () => scoreNoop()
24522
24629
  };
24523
24630
 
24524
24631
  /**
@@ -25876,7 +25983,7 @@ const styles$p = StyleSheet.create({
25876
25983
  }
25877
25984
  });
25878
25985
 
25879
- function radioValidator(userInput, rubric, strings) {
25986
+ function scoreRadio(userInput, rubric, strings) {
25880
25987
  const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
25881
25988
  return sum + (selected ? 1 : 0);
25882
25989
  }, 0);
@@ -25886,10 +25993,10 @@ function radioValidator(userInput, rubric, strings) {
25886
25993
  message: null
25887
25994
  };
25888
25995
  }
25889
-
25890
- // TODO(LEMS-2541): should numCorrect actually be on the rubric
25891
- // instead of the userInput?
25892
- if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
25996
+ const numCorrect = rubric.choices.reduce((sum, currentChoice) => {
25997
+ return currentChoice.correct ? sum + 1 : sum;
25998
+ }, 0);
25999
+ if (numCorrect > 1 && numSelected !== numCorrect) {
25893
26000
  return {
25894
26001
  type: "invalid",
25895
26002
  message: strings.chooseCorrectNum
@@ -25897,9 +26004,8 @@ function radioValidator(userInput, rubric, strings) {
25897
26004
  // If NOTA and some other answer are checked, ...
25898
26005
  }
25899
26006
 
25900
- // TODO(LEMS-2541): should noneOfTheAboveSelected be replaced with a
25901
- // combination of choicesSelected and noneOfTheAboveIndex?
25902
- if (userInput.noneOfTheAboveSelected && numSelected > 1) {
26007
+ const noneOfTheAboveSelected = rubric.choices.some((choice, index) => choice.isNoneOfTheAbove && userInput.choicesSelected[index]);
26008
+ if (noneOfTheAboveSelected && numSelected > 1) {
25903
26009
  return {
25904
26010
  type: "invalid",
25905
26011
  message: strings.notNoneOfTheAbove
@@ -25907,9 +26013,7 @@ function radioValidator(userInput, rubric, strings) {
25907
26013
  }
25908
26014
  const correct = userInput.choicesSelected.every((selected, i) => {
25909
26015
  let isCorrect;
25910
- // TODO(LEMS-2541): should noneOfTheAboveIndex actually be on the rubric
25911
- // instead of the userInput?
25912
- if (userInput.noneOfTheAboveIndex === i) {
26016
+ if (rubric.choices[i].isNoneOfTheAbove) {
25913
26017
  isCorrect = rubric.choices.every((choice, j) => {
25914
26018
  return i === j || !choice.correct;
25915
26019
  });
@@ -26031,7 +26135,7 @@ class Radio$1 extends React.Component {
26031
26135
  choiceStates
26032
26136
  } = this.props;
26033
26137
  if (choiceStates) {
26034
- const score = radioValidator(this.getUserInput(), rubric, this.context.strings);
26138
+ const score = scoreRadio(this.getUserInput(), rubric, this.context.strings);
26035
26139
  const widgetCorrect = score.type === "points" && score.total === score.earned;
26036
26140
  const newStates = choiceStates.map(state => _extends({}, state, {
26037
26141
  highlighted: state.selected,
@@ -26079,32 +26183,19 @@ class Radio$1 extends React.Component {
26079
26183
  }
26080
26184
  };
26081
26185
  }
26082
- static getUserInputFromProps(props) {
26186
+ static getUserInputFromProps(props, unshuffle = true) {
26083
26187
  // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
26084
26188
  // future timeline implementers: this used to be {value: i} before
26085
26189
  // multiple select was added)
26086
26190
  if (props.choiceStates) {
26087
- let noneOfTheAboveIndex = null;
26088
- let noneOfTheAboveSelected = false;
26089
26191
  const choiceStates = props.choiceStates;
26090
26192
  const choicesSelected = choiceStates.map(() => false);
26091
- const numCorrect = props.numCorrect;
26092
26193
  for (let i = 0; i < choicesSelected.length; i++) {
26093
- const index = props.choices[i].originalIndex;
26194
+ const index = unshuffle ? props.choices[i].originalIndex : i;
26094
26195
  choicesSelected[index] = choiceStates[i].selected;
26095
- if (props.choices[i].isNoneOfTheAbove) {
26096
- // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'null'.
26097
- noneOfTheAboveIndex = index;
26098
- if (choicesSelected[i]) {
26099
- noneOfTheAboveSelected = true;
26100
- }
26101
- }
26102
26196
  }
26103
26197
  return {
26104
- choicesSelected,
26105
- numCorrect,
26106
- noneOfTheAboveIndex,
26107
- noneOfTheAboveSelected
26198
+ choicesSelected
26108
26199
  };
26109
26200
  // Support legacy choiceState implementation
26110
26201
  }
@@ -26113,27 +26204,14 @@ class Radio$1 extends React.Component {
26113
26204
  values
26114
26205
  } = props;
26115
26206
  if (values) {
26116
- let noneOfTheAboveIndex = null;
26117
- let noneOfTheAboveSelected = false;
26118
26207
  const choicesSelected = [...values];
26119
- const numCorrect = props.numCorrect;
26120
26208
  const valuesLength = values.length;
26121
26209
  for (let i = 0; i < valuesLength; i++) {
26122
- const index = props.choices[i].originalIndex;
26210
+ const index = unshuffle ? props.choices[i].originalIndex : i;
26123
26211
  choicesSelected[index] = values[i];
26124
- if (props.choices[i].isNoneOfTheAbove) {
26125
- // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'null'.
26126
- noneOfTheAboveIndex = index;
26127
- if (choicesSelected[i]) {
26128
- noneOfTheAboveSelected = true;
26129
- }
26130
- }
26131
26212
  }
26132
26213
  return {
26133
- choicesSelected,
26134
- noneOfTheAboveIndex,
26135
- noneOfTheAboveSelected,
26136
- numCorrect
26214
+ choicesSelected
26137
26215
  };
26138
26216
  }
26139
26217
  // Nothing checked
@@ -26165,6 +26243,10 @@ class Radio$1 extends React.Component {
26165
26243
  getUserInput() {
26166
26244
  return Radio$1.getUserInputFromProps(this.props);
26167
26245
  }
26246
+ getPromptJSON() {
26247
+ const userInput = Radio$1.getUserInputFromProps(this.props, false);
26248
+ return getPromptJSON$q(this.props, userInput);
26249
+ }
26168
26250
  render() {
26169
26251
  const {
26170
26252
  choices
@@ -26402,13 +26484,26 @@ var Radio = {
26402
26484
  },
26403
26485
  propUpgrades: propUpgrades$1,
26404
26486
  isLintable: true,
26405
- validator: radioValidator
26487
+ scorer: scoreRadio
26406
26488
  };
26407
26489
 
26408
26490
  /* eslint-disable import/no-named-as-default */
26409
26491
  var basicWidgets = [Radio, InputNumber$1, NumericInput$1, Expression$1];
26410
26492
 
26411
- function categorizerValidator(userInput, rubric, strings) {
26493
+ const getPromptJSON$n = (renderProps, userInput) => {
26494
+ return {
26495
+ type: "categorizer",
26496
+ options: {
26497
+ items: renderProps.items,
26498
+ categories: renderProps.categories
26499
+ },
26500
+ userInput: {
26501
+ itemToCategoryMapping: userInput.values
26502
+ }
26503
+ };
26504
+ };
26505
+
26506
+ function scoreCategorizer(userInput, rubric, strings) {
26412
26507
  let completed = true;
26413
26508
  let allCorrect = true;
26414
26509
  rubric.values.forEach((value, i) => {
@@ -26453,6 +26548,9 @@ class Categorizer extends React.Component {
26453
26548
  getUserInput() {
26454
26549
  return Categorizer.getUserInputFromProps(this.props);
26455
26550
  }
26551
+ getPromptJSON() {
26552
+ return getPromptJSON$n(this.props, this.getUserInput());
26553
+ }
26456
26554
  onChange(itemNum, catNum) {
26457
26555
  const values = [...this.props.values];
26458
26556
  // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'never'.
@@ -26606,7 +26704,7 @@ var Categorizer$1 = {
26606
26704
  return _.pick(editorProps, "items", "categories", "values", "randomizeItems");
26607
26705
  },
26608
26706
  isLintable: true,
26609
- validator: categorizerValidator
26707
+ scorer: scoreCategorizer
26610
26708
  };
26611
26709
 
26612
26710
  /**
@@ -26656,7 +26754,19 @@ const parseUrl = url => new URL(url, location().origin);
26656
26754
  */
26657
26755
  const toAbsoluteUrl = url => parseUrl(url).href;
26658
26756
 
26659
- function csProgramValidator(state) {
26757
+ const getUnsupportedPromptJSON = (widgetType, message = "") => {
26758
+ return {
26759
+ type: widgetType,
26760
+ isSupported: false,
26761
+ message
26762
+ };
26763
+ };
26764
+
26765
+ const getPromptJSON$m = () => {
26766
+ return getUnsupportedPromptJSON("cs-program");
26767
+ };
26768
+
26769
+ function scoreCSProgram(state) {
26660
26770
  // The iframe can tell us whether it's correct or incorrect,
26661
26771
  // and pass an optional message
26662
26772
  if (state.status === "correct") {
@@ -26742,6 +26852,9 @@ class CSProgram extends React.Component {
26742
26852
  message: this.props.message
26743
26853
  };
26744
26854
  }
26855
+ getPromptJSON() {
26856
+ return getPromptJSON$m();
26857
+ }
26745
26858
  render() {
26746
26859
  if (!this.props.programID) {
26747
26860
  return /*#__PURE__*/React.createElement("div", null);
@@ -26824,7 +26937,15 @@ var CSProgram$1 = {
26824
26937
  supportedAlignments: ["block", "full-width"],
26825
26938
  widget: CSProgram,
26826
26939
  hidden: true,
26827
- validator: csProgramValidator
26940
+ scorer: scoreCSProgram
26941
+ };
26942
+
26943
+ const getPromptJSON$l = renderProps => {
26944
+ return {
26945
+ type: "definition",
26946
+ definition: renderProps.definition,
26947
+ togglePrompt: renderProps.togglePrompt
26948
+ };
26828
26949
  };
26829
26950
 
26830
26951
  class Definition extends React.Component {
@@ -26834,6 +26955,9 @@ class Definition extends React.Component {
26834
26955
  // doesn't implement any Widget methods
26835
26956
  this.isWidget = true;
26836
26957
  }
26958
+ getPromptJSON() {
26959
+ return getPromptJSON$l(this.props);
26960
+ }
26837
26961
  render() {
26838
26962
  return /*#__PURE__*/React.createElement(DefinitionConsumer, null, ({
26839
26963
  activeDefinitionId,
@@ -26889,8 +27013,8 @@ var Definition$1 = {
26889
27013
  defaultAlignment: "inline",
26890
27014
  widget: Definition,
26891
27015
  transform: x => x,
26892
- // TODO: things that aren't interactive shouldn't need validators
26893
- validator: () => noopValidator()
27016
+ // TODO: things that aren't interactive shouldn't need scoring functions
27017
+ scorer: () => scoreNoop()
26894
27018
  };
26895
27019
 
26896
27020
  // The props are type `any` on purpose so that this can receive props
@@ -26921,11 +27045,23 @@ var DeprecatedStandin$1 = {
26921
27045
  displayName: "Deprecated Standin",
26922
27046
  widget: DeprecatedStandin,
26923
27047
  hidden: true,
26924
- // TODO: things that aren't interactive shouldn't need validators
26925
- validator: () => noopValidator(1)
27048
+ // TODO: things that aren't interactive shouldn't need scoring functions
27049
+ scorer: () => scoreNoop(1)
27050
+ };
27051
+
27052
+ const getPromptJSON$k = (renderProps, userInput) => {
27053
+ return {
27054
+ type: "dropdown",
27055
+ options: {
27056
+ items: renderProps.choices
27057
+ },
27058
+ userInput: {
27059
+ selectedIndex: userInput.value - 1 // Offset to account for placeholder
27060
+ }
27061
+ };
26926
27062
  };
26927
27063
 
26928
- function dropdownValidator(userInput, rubric) {
27064
+ function scoreDropdown(userInput, rubric) {
26929
27065
  const selected = userInput.value;
26930
27066
  if (selected === 0) {
26931
27067
  return {
@@ -26968,6 +27104,9 @@ class Dropdown extends React.Component {
26968
27104
  value: this.props.selected
26969
27105
  };
26970
27106
  }
27107
+ getPromptJSON() {
27108
+ return getPromptJSON$k(this.props, this.getUserInput());
27109
+ }
26971
27110
  render() {
26972
27111
  const children = [/*#__PURE__*/React.createElement(OptionItem, {
26973
27112
  key: "placeholder",
@@ -27015,7 +27154,15 @@ var Dropdown$1 = {
27015
27154
  accessible: true,
27016
27155
  widget: Dropdown,
27017
27156
  transform: optionsTransform,
27018
- validator: dropdownValidator
27157
+ scorer: scoreDropdown
27158
+ };
27159
+
27160
+ const getPromptJSON$j = renderProps => {
27161
+ return {
27162
+ type: "explanation",
27163
+ showPrompt: renderProps.showPrompt,
27164
+ explanation: renderProps.explanation
27165
+ };
27019
27166
  };
27020
27167
 
27021
27168
  // transform = _.identity
@@ -27052,6 +27199,9 @@ class Explanation extends React.Component {
27052
27199
  componentWillUnmount() {
27053
27200
  this._mounted = false;
27054
27201
  }
27202
+ getPromptJSON() {
27203
+ return getPromptJSON$j(this.props);
27204
+ }
27055
27205
  render() {
27056
27206
  const promptText = this.state.expanded ? this.props.hidePrompt : this.props.showPrompt;
27057
27207
  const caretIcon = this.state.expanded ? caretUp : caretDown;
@@ -27166,8 +27316,25 @@ var Explanation$1 = {
27166
27316
  widget: Explanation,
27167
27317
  transform: _.identity,
27168
27318
  isLintable: true,
27169
- // TODO: things that aren't interactive shouldn't need validators
27170
- validator: () => noopValidator()
27319
+ // TODO: things that aren't interactive shouldn't need scoring functions
27320
+ scorer: () => scoreNoop()
27321
+ };
27322
+
27323
+ const getPromptJSON$i = (title, rendererJSON, hintRendererJSON) => {
27324
+ if (!rendererJSON) {
27325
+ return {
27326
+ type: "graded-group",
27327
+ title,
27328
+ content: "",
27329
+ widgets: {},
27330
+ hint: hintRendererJSON
27331
+ };
27332
+ }
27333
+ return _extends({}, rendererJSON, {
27334
+ title,
27335
+ type: "graded-group",
27336
+ hint: hintRendererJSON
27337
+ });
27171
27338
  };
27172
27339
 
27173
27340
  class GradedGroupAnswerBar extends React.Component {
@@ -27302,6 +27469,8 @@ class GradedGroup extends React.Component {
27302
27469
  message: "",
27303
27470
  answerBarState: "HIDDEN"
27304
27471
  };
27472
+ this.rendererRef = /*#__PURE__*/React.createRef();
27473
+ this.hintRendererRef = /*#__PURE__*/React.createRef();
27305
27474
  this.change = (...args) => {
27306
27475
  // @ts-expect-error - TS2345 - Argument of type 'readonly unknown[]' is not assignable to parameter of type 'any[]'.
27307
27476
  return change.apply(this, args);
@@ -27317,13 +27486,9 @@ class GradedGroup extends React.Component {
27317
27486
  status: GRADING_STATUSES.ungraded,
27318
27487
  message: ""
27319
27488
  });
27320
-
27321
- // eslint-disable-next-line react/no-string-refs
27322
- if (this.refs.renderer) {
27489
+ if (this.rendererRef.current) {
27323
27490
  this.change("widgets", this.props.widgets);
27324
- // eslint-disable-next-line react/no-string-refs
27325
- // @ts-expect-error - TS2339 - Property 'emptyWidgets' does not exist on type 'ReactInstance'.
27326
- const emptyWidgets = this.refs.renderer.emptyWidgets();
27491
+ const emptyWidgets = this.rendererRef.current.emptyWidgets();
27327
27492
  const answerable = emptyWidgets.length === 0;
27328
27493
  const answerBarState = this.state.answerBarState;
27329
27494
  this.setState({
@@ -27332,12 +27497,11 @@ class GradedGroup extends React.Component {
27332
27497
  }
27333
27498
  };
27334
27499
  this._checkAnswer = () => {
27335
- // eslint-disable-next-line react/no-string-refs
27336
- // @ts-expect-error - TS2339 - Property 'showRationalesForCurrentlySelectedChoices' does not exist on type 'ReactInstance'.
27337
- this.refs.renderer.showRationalesForCurrentlySelectedChoices();
27338
- // eslint-disable-next-line react/no-string-refs
27339
- // @ts-expect-error - TS2339 - Property 'score' does not exist on type 'ReactInstance'.
27340
- const score = this.refs.renderer.score();
27500
+ var _this$rendererRef$cur, _this$rendererRef$cur2;
27501
+ (_this$rendererRef$cur = this.rendererRef.current) == null || _this$rendererRef$cur.showRationalesForCurrentlySelectedChoices();
27502
+ const score = ((_this$rendererRef$cur2 = this.rendererRef.current) == null ? void 0 : _this$rendererRef$cur2.score()) || {
27503
+ type: "invalid"
27504
+ };
27341
27505
  const {
27342
27506
  INVALID_MESSAGE_PREFIX,
27343
27507
  DEFAULT_INVALID_MESSAGE_1,
@@ -27357,34 +27521,39 @@ class GradedGroup extends React.Component {
27357
27521
  };
27358
27522
  // Mobile API
27359
27523
  this.getInputPaths = () => {
27360
- // eslint-disable-next-line react/no-string-refs
27361
- // @ts-expect-error - TS2339 - Property 'getInputPaths' does not exist on type 'ReactInstance'.
27362
- return this.refs.renderer.getInputPaths();
27524
+ var _this$rendererRef$cur3;
27525
+ return ((_this$rendererRef$cur3 = this.rendererRef.current) == null ? void 0 : _this$rendererRef$cur3.getInputPaths()) || [];
27363
27526
  };
27364
27527
  this.setInputValue = (path, newValue, cb) => {
27365
- // eslint-disable-next-line react/no-string-refs
27366
- // @ts-expect-error - TS2339 - Property 'setInputValue' does not exist on type 'ReactInstance'.
27367
- return this.refs.renderer.setInputValue(path, newValue, cb);
27528
+ var _this$rendererRef$cur4;
27529
+ return (_this$rendererRef$cur4 = this.rendererRef.current) == null ? void 0 : _this$rendererRef$cur4.setInputValue(path, newValue, cb);
27368
27530
  };
27369
27531
  this.focus = () => {
27370
- // eslint-disable-next-line react/no-string-refs
27371
- // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
27372
- return this.refs.renderer.focus();
27532
+ var _this$rendererRef$cur5;
27533
+ return !!((_this$rendererRef$cur5 = this.rendererRef.current) != null && _this$rendererRef$cur5.focus());
27373
27534
  };
27374
27535
  this.focusInputPath = path => {
27375
- // eslint-disable-next-line react/no-string-refs
27376
- // @ts-expect-error - TS2339 - Property 'focusPath' does not exist on type 'ReactInstance'.
27377
- this.refs.renderer.focusPath(path);
27536
+ var _this$rendererRef$cur6;
27537
+ (_this$rendererRef$cur6 = this.rendererRef.current) == null || _this$rendererRef$cur6.focusPath(path);
27378
27538
  };
27379
27539
  this.blurInputPath = path => {
27380
- // eslint-disable-next-line react/no-string-refs
27381
- // @ts-expect-error - TS2339 - Property 'blurPath' does not exist on type 'ReactInstance'.
27382
- this.refs.renderer.blurPath(path);
27540
+ var _this$rendererRef$cur7;
27541
+ (_this$rendererRef$cur7 = this.rendererRef.current) == null || _this$rendererRef$cur7.blurPath(path);
27383
27542
  };
27384
27543
  }
27385
27544
  shouldComponentUpdate(nextProps, nextState) {
27386
27545
  return nextProps !== this.props || nextState !== this.state;
27387
27546
  }
27547
+ getPromptJSON() {
27548
+ var _this$hintRendererRef, _this$props$hint, _this$rendererRef$cur8;
27549
+ // If the hint isn't expanded, we can't get the prompt JSON from the rendered widgets.
27550
+ // We'll just pass in the hint content as a string instead.
27551
+ const hint = ((_this$hintRendererRef = this.hintRendererRef.current) == null ? void 0 : _this$hintRendererRef.getPromptJSON()) || {
27552
+ content: ((_this$props$hint = this.props.hint) == null ? void 0 : _this$props$hint.content) || "",
27553
+ widgets: {}
27554
+ };
27555
+ return getPromptJSON$i(this.props.title, (_this$rendererRef$cur8 = this.rendererRef.current) == null ? void 0 : _this$rendererRef$cur8.getPromptJSON(), hint);
27556
+ }
27388
27557
  render() {
27389
27558
  const apiOptions = _.extend({}, ApiOptions.defaults, this.props.apiOptions, {
27390
27559
  // Api Rewriting to support correct onFocus/onBlur
@@ -27441,8 +27610,7 @@ class GradedGroup extends React.Component {
27441
27610
  }, !!this.props.title && /*#__PURE__*/React.createElement("div", {
27442
27611
  className: css(styles$j.title)
27443
27612
  }, this.props.title), /*#__PURE__*/React.createElement(Renderer, _extends({}, this.props, {
27444
- // eslint-disable-next-line react/no-string-refs
27445
- ref: "renderer",
27613
+ ref: this.rendererRef,
27446
27614
  apiOptions: _extends({}, apiOptions, {
27447
27615
  readOnly
27448
27616
  }),
@@ -27484,8 +27652,7 @@ class GradedGroup extends React.Component {
27484
27652
  });
27485
27653
  }
27486
27654
  }, this.context.strings.hideExplanation), /*#__PURE__*/React.createElement(Renderer, _extends({}, this.props.hint, {
27487
- // eslint-disable-next-line react/no-string-refs
27488
- ref: "hints-renderer",
27655
+ ref: this.hintRendererRef,
27489
27656
  apiOptions: apiOptions,
27490
27657
  linterContext: this.props.linterContext,
27491
27658
  strings: this.context.strings
@@ -27587,6 +27754,16 @@ var GradedGroup$1 = {
27587
27754
  isLintable: true
27588
27755
  };
27589
27756
 
27757
+ const getPromptJSON$h = (renderProps, activeGroupJSON) => {
27758
+ return {
27759
+ type: "graded-group-set",
27760
+ options: {
27761
+ groupCount: renderProps.gradedGroups.length,
27762
+ currentGroup: activeGroupJSON
27763
+ }
27764
+ };
27765
+ };
27766
+
27590
27767
  class Indicators extends React.Component {
27591
27768
  constructor(...args) {
27592
27769
  super(...args);
@@ -27674,6 +27851,10 @@ class GradedGroupSet extends React.Component {
27674
27851
  nextProps.gradedGroups;
27675
27852
  return nextProps !== this.props || nextState !== this.state;
27676
27853
  }
27854
+ getPromptJSON() {
27855
+ const activeGroupPromptJSON = this._childGroup.getPromptJSON();
27856
+ return getPromptJSON$h(this.props, activeGroupPromptJSON);
27857
+ }
27677
27858
  render() {
27678
27859
  // When used in the context of TranslationEditor, render the
27679
27860
  // GradedGroup widget one below another instead of using an indicator
@@ -27911,6 +28092,33 @@ const styles$h = StyleSheet.create({
27911
28092
  }
27912
28093
  });
27913
28094
 
28095
+ const getPromptJSON$g = (renderProps, userInput) => {
28096
+ const {
28097
+ type,
28098
+ coords
28099
+ } = userInput;
28100
+ const input = {
28101
+ type,
28102
+ coords
28103
+ };
28104
+ if (userInput.type === "logarithm" || userInput.type === "exponential") {
28105
+ input["asymptote"] = userInput.asymptote;
28106
+ }
28107
+ return {
28108
+ type: "grapher",
28109
+ options: {
28110
+ availableTypes: renderProps.availableTypes,
28111
+ range: renderProps.graph.range,
28112
+ labels: renderProps.graph.labels,
28113
+ tickStep: renderProps.graph.step,
28114
+ gridStep: renderProps.graph.gridStep,
28115
+ snapStep: renderProps.graph.snapStep,
28116
+ backgroundImageUrl: renderProps.graph.backgroundImage.url
28117
+ },
28118
+ userInput: input
28119
+ };
28120
+ };
28121
+
27914
28122
  /* eslint-disable one-var */
27915
28123
  // @ts-expect-error - TS2339 - Property 'Plot' does not exist on type 'typeof Graphie'.
27916
28124
  const Plot$1 = Graphie.Plot;
@@ -28434,7 +28642,7 @@ function getCoefficientsByType(data) {
28434
28642
  throw new PerseusError("Invalid grapher type", Errors.InvalidInput);
28435
28643
  }
28436
28644
  }
28437
- function grapherValidator(userInput, rubric) {
28645
+ function scoreGrapher(userInput, rubric) {
28438
28646
  if (userInput.type !== rubric.correct.type) {
28439
28647
  return {
28440
28648
  type: "points",
@@ -28583,7 +28791,7 @@ class FunctionGrapher extends React.Component {
28583
28791
  constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
28584
28792
  // Always enforce that this is a function
28585
28793
  const isFunction = _.all(this._coords(), (otherCoord, j) => {
28586
- return i === j || !otherCoord || !number.equal(coord[0], otherCoord[0]);
28794
+ return i === j || !otherCoord || !number$1.equal(coord[0], otherCoord[0]);
28587
28795
  });
28588
28796
 
28589
28797
  // Evaluate this criteria before per-point
@@ -28783,6 +28991,9 @@ class Grapher extends React.Component {
28783
28991
  getUserInput() {
28784
28992
  return Grapher.getUserInputFromProps(this.props);
28785
28993
  }
28994
+ getPromptJSON() {
28995
+ return getPromptJSON$g(this.props, this.getUserInput());
28996
+ }
28786
28997
  render() {
28787
28998
  const type = this.props.plot.type;
28788
28999
  const coords = this.props.plot.coords;
@@ -28868,7 +29079,20 @@ var Grapher$1 = {
28868
29079
  widget: Grapher,
28869
29080
  transform: propTransform$2,
28870
29081
  staticTransform: staticTransform$4,
28871
- validator: grapherValidator
29082
+ scorer: scoreGrapher
29083
+ };
29084
+
29085
+ const getPromptJSON$f = rendererJSON => {
29086
+ if (!rendererJSON) {
29087
+ return {
29088
+ type: "group",
29089
+ content: "",
29090
+ widgets: {}
29091
+ };
29092
+ }
29093
+ return _extends({}, rendererJSON, {
29094
+ type: "group"
29095
+ });
28872
29096
  };
28873
29097
 
28874
29098
  // exports has no 'transform'
@@ -28926,17 +29150,21 @@ class Group extends React.Component {
28926
29150
  var _this$rendererRef8;
28927
29151
  return (_this$rendererRef8 = this.rendererRef) == null ? void 0 : _this$rendererRef8.getUserInput();
28928
29152
  }
29153
+ getPromptJSON() {
29154
+ var _this$rendererRef9;
29155
+ return getPromptJSON$f((_this$rendererRef9 = this.rendererRef) == null ? void 0 : _this$rendererRef9.getPromptJSON());
29156
+ }
28929
29157
  // Mobile API:
28930
29158
  getInputPaths() {
28931
- var _this$rendererRef$get, _this$rendererRef9;
28932
- return (_this$rendererRef$get = (_this$rendererRef9 = this.rendererRef) == null ? void 0 : _this$rendererRef9.getInputPaths()) != null ? _this$rendererRef$get : [];
29159
+ var _this$rendererRef$get, _this$rendererRef10;
29160
+ return (_this$rendererRef$get = (_this$rendererRef10 = this.rendererRef) == null ? void 0 : _this$rendererRef10.getInputPaths()) != null ? _this$rendererRef$get : [];
28933
29161
  }
28934
29162
  focus() {
28935
- var _this$rendererRef$foc, _this$rendererRef10;
28936
- return (_this$rendererRef$foc = (_this$rendererRef10 = this.rendererRef) == null ? void 0 : _this$rendererRef10.focus()) != null ? _this$rendererRef$foc : false;
29163
+ var _this$rendererRef$foc, _this$rendererRef11;
29164
+ return (_this$rendererRef$foc = (_this$rendererRef11 = this.rendererRef) == null ? void 0 : _this$rendererRef11.focus()) != null ? _this$rendererRef$foc : false;
28937
29165
  }
28938
29166
  render() {
28939
- var _this$rendererRef11;
29167
+ var _this$rendererRef12;
28940
29168
  const apiOptions = _extends({}, ApiOptions.defaults, this.props.apiOptions, {
28941
29169
  // Api Rewriting to support correct onFocus/onBlur
28942
29170
  // events for the mobile API
@@ -28972,7 +29200,7 @@ class Group extends React.Component {
28972
29200
  this.change("widgets", this.rendererRef.props.widgets);
28973
29201
  }
28974
29202
  };
28975
- const score = (_this$rendererRef11 = this.rendererRef) == null ? void 0 : _this$rendererRef11.score();
29203
+ const score = (_this$rendererRef12 = this.rendererRef) == null ? void 0 : _this$rendererRef12.score();
28976
29204
  const isValid = score && score.type !== "invalid";
28977
29205
  const isInvalid = score && score.type === "invalid";
28978
29206
 
@@ -29022,7 +29250,11 @@ var Group$1 = {
29022
29250
  isLintable: true
29023
29251
  };
29024
29252
 
29025
- function iframeValidator(userInput) {
29253
+ const getPromptJSON$e = () => {
29254
+ return getUnsupportedPromptJSON("iframe");
29255
+ };
29256
+
29257
+ function scoreIframe(userInput) {
29026
29258
  // The iframe can tell us whether it's correct or incorrect,
29027
29259
  // and pass an optional message
29028
29260
  if (userInput.status === "correct") {
@@ -29098,6 +29330,9 @@ class Iframe extends React.Component {
29098
29330
  message: this.props.message
29099
29331
  };
29100
29332
  }
29333
+ getPromptJSON() {
29334
+ return getPromptJSON$e();
29335
+ }
29101
29336
  render() {
29102
29337
  const style = {
29103
29338
  width: String(this.props.width),
@@ -29172,7 +29407,19 @@ var Iframe$1 = {
29172
29407
  widget: Iframe,
29173
29408
  // Let's not expose it to all content creators yet
29174
29409
  hidden: true,
29175
- validator: iframeValidator
29410
+ scorer: scoreIframe
29411
+ };
29412
+
29413
+ const getPromptJSON$d = renderProps => {
29414
+ return {
29415
+ type: "image",
29416
+ options: {
29417
+ altText: renderProps.alt,
29418
+ title: renderProps.title,
29419
+ caption: renderProps.caption,
29420
+ imageUrl: renderProps.backgroundImage.url
29421
+ }
29422
+ };
29176
29423
  };
29177
29424
 
29178
29425
  const defaultBoxSize = 400;
@@ -29197,6 +29444,9 @@ class ImageWidget extends React.Component {
29197
29444
  return change.apply(this, args);
29198
29445
  };
29199
29446
  }
29447
+ getPromptJSON() {
29448
+ return getPromptJSON$d(this.props);
29449
+ }
29200
29450
  render() {
29201
29451
  let image;
29202
29452
  const alt = this.props.caption === this.props.alt ? "" : this.props.alt;
@@ -29352,8 +29602,12 @@ var Image$1 = {
29352
29602
  displayName: "Image",
29353
29603
  widget: ImageWidget,
29354
29604
  isLintable: true,
29355
- // TODO: things that aren't interactive shouldn't need validators
29356
- validator: () => noopValidator()
29605
+ // TODO: things that aren't interactive shouldn't need scoring functions
29606
+ scorer: () => scoreNoop()
29607
+ };
29608
+
29609
+ const getPromptJSON$c = () => {
29610
+ return getUnsupportedPromptJSON("interaction");
29357
29611
  };
29358
29612
 
29359
29613
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -29526,6 +29780,9 @@ class Interaction extends React.Component {
29526
29780
  });
29527
29781
  }
29528
29782
  }
29783
+ getPromptJSON() {
29784
+ return getPromptJSON$c();
29785
+ }
29529
29786
  render() {
29530
29787
  const range = this.props.graph.range;
29531
29788
  let labels = this.props.graph.labels;
@@ -29945,8 +30202,8 @@ var Interactive = {
29945
30202
  widget: Interaction,
29946
30203
  transform: _.identity,
29947
30204
  hidden: true,
29948
- // TODO: things that aren't interactive shouldn't need validators
29949
- validator: () => noopValidator()
30205
+ // TODO: things that aren't interactive shouldn't need scoring functions
30206
+ scorer: () => scoreNoop()
29950
30207
  };
29951
30208
 
29952
30209
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -30249,6 +30506,139 @@ Graph.defaultProps = {
30249
30506
  isMobile: false
30250
30507
  };
30251
30508
 
30509
+ const getPromptJSON$b = (props, userInput) => {
30510
+ var _props$backgroundImag;
30511
+ return {
30512
+ type: "interactive-graph",
30513
+ options: {
30514
+ graph: getGraphOptionsForProps(props),
30515
+ backgroundImageUrl: (_props$backgroundImag = props.backgroundImage) == null ? void 0 : _props$backgroundImag.url,
30516
+ range: props.range,
30517
+ labels: props.labels
30518
+ },
30519
+ userInput: getUserInput(userInput)
30520
+ };
30521
+ };
30522
+ const getGraphOptionsForProps = props => {
30523
+ var _props$graph$startCoo, _props$graph$startCoo2;
30524
+ const type = props.graph.type;
30525
+ switch (type) {
30526
+ case "angle":
30527
+ return {
30528
+ type: props.graph.type,
30529
+ angleOffsetDegrees: props.graph.angleOffsetDeg,
30530
+ startCoords: props.graph.startCoords
30531
+ };
30532
+ case "circle":
30533
+ return {
30534
+ type: props.graph.type,
30535
+ startParams: {
30536
+ center: (_props$graph$startCoo = props.graph.startCoords) == null ? void 0 : _props$graph$startCoo.center,
30537
+ radius: (_props$graph$startCoo2 = props.graph.startCoords) == null ? void 0 : _props$graph$startCoo2.radius
30538
+ }
30539
+ };
30540
+ case "linear":
30541
+ return {
30542
+ type: props.graph.type,
30543
+ startCoords: props.graph.startCoords
30544
+ };
30545
+ case "linear-system":
30546
+ return {
30547
+ type: props.graph.type,
30548
+ startCoords: props.graph.startCoords
30549
+ };
30550
+ case "point":
30551
+ return {
30552
+ type: props.graph.type,
30553
+ numPoints: props.graph.numPoints,
30554
+ startCoords: props.graph.startCoords
30555
+ };
30556
+ case "polygon":
30557
+ return {
30558
+ type: props.graph.type,
30559
+ match: props.graph.match,
30560
+ numSides: props.graph.numSides,
30561
+ startCoords: props.graph.startCoords
30562
+ };
30563
+ case "quadratic":
30564
+ return {
30565
+ type: props.graph.type,
30566
+ startCoords: props.graph.startCoords
30567
+ };
30568
+ case "ray":
30569
+ return {
30570
+ type: props.graph.type,
30571
+ startCoords: props.graph.startCoords
30572
+ };
30573
+ case "segment":
30574
+ return {
30575
+ type: props.graph.type,
30576
+ numSegments: props.graph.numSegments,
30577
+ startCoords: props.graph.startCoords
30578
+ };
30579
+ case "sinusoid":
30580
+ return {
30581
+ type: props.graph.type,
30582
+ startCoords: props.graph.startCoords
30583
+ };
30584
+ case "none":
30585
+ return {};
30586
+ default:
30587
+ throw new UnreachableCaseError(type);
30588
+ }
30589
+ };
30590
+ const getUserInput = userInput => {
30591
+ const type = userInput.type;
30592
+ switch (type) {
30593
+ case "angle":
30594
+ return {
30595
+ coords: userInput.coords,
30596
+ angleOffsetDegrees: userInput.angleOffsetDeg
30597
+ };
30598
+ case "circle":
30599
+ return {
30600
+ center: userInput.center,
30601
+ radius: userInput.radius
30602
+ };
30603
+ case "linear":
30604
+ return {
30605
+ coords: userInput.coords
30606
+ };
30607
+ case "linear-system":
30608
+ return {
30609
+ coords: userInput.coords
30610
+ };
30611
+ case "point":
30612
+ return {
30613
+ coords: userInput.coords
30614
+ };
30615
+ case "polygon":
30616
+ return {
30617
+ coords: userInput.coords
30618
+ };
30619
+ case "quadratic":
30620
+ return {
30621
+ coords: userInput.coords
30622
+ };
30623
+ case "ray":
30624
+ return {
30625
+ coords: userInput.coords
30626
+ };
30627
+ case "segment":
30628
+ return {
30629
+ coords: userInput.coords
30630
+ };
30631
+ case "sinusoid":
30632
+ return {
30633
+ coords: userInput.coords
30634
+ };
30635
+ case "none":
30636
+ return {};
30637
+ default:
30638
+ throw new UnreachableCaseError(type);
30639
+ }
30640
+ };
30641
+
30252
30642
  function pathBuilder() {
30253
30643
  return new PathBuilder();
30254
30644
  }
@@ -31313,7 +31703,7 @@ class NumberInput extends React.Component {
31313
31703
  };
31314
31704
  }
31315
31705
  componentDidUpdate(prevProps) {
31316
- if (!number.equal(this.getValue(), this.props.value)) {
31706
+ if (!number$1.equal(this.getValue(), this.props.value)) {
31317
31707
  this._setValue(this.props.value, this.state.format);
31318
31708
  }
31319
31709
  }
@@ -31482,6 +31872,10 @@ const LegacyGrid = ({
31482
31872
  * To render more complex Perseus items, see the `Item` type in the multi item
31483
31873
  * area.
31484
31874
  */
31875
+ /**
31876
+ * A "PerseusArticle" is an item that is meant to be rendered as an article.
31877
+ * This item is never scored and is rendered by the `ArticleRenderer`.
31878
+ */
31485
31879
  /**
31486
31880
  * A "MultiItem" is an advanced Perseus item. It is rendered by the
31487
31881
  * `MultiRenderer` and you can control the layout of individual parts of the
@@ -31578,6 +31972,10 @@ const ItemExtras = [
31578
31972
  // A background image applied to various widgets.
31579
31973
 
31580
31974
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
31975
+
31976
+ // 2D range: xMin, xMax, yMin, yMax
31977
+
31978
+ const lockedFigureColorNames = ["blue", "green", "grayH", "purple", "pink", "orange", "red"];
31581
31979
  const lockedFigureColors = {
31582
31980
  blue: "#3D7586",
31583
31981
  green: "#447A53",
@@ -31598,6 +31996,104 @@ const lockedFigureFillStyles = {
31598
31996
 
31599
31997
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
31600
31998
 
31999
+ /**
32000
+ * 44 is touch best practice and AAA compliant for WCAG
32001
+ * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
32002
+ */
32003
+ const TARGET_SIZE = 44;
32004
+ const REMOVE_BUTTON_ID = "perseus_mafs_remove_button";
32005
+
32006
+ // same as pointsFromNormalized in interactive-graph.tsx
32007
+ const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
32008
+ // maps over [x, y]
32009
+ coords.map((coord, i) => {
32010
+ const axisRange = range[i];
32011
+ if (noSnap) {
32012
+ return axisRange[MIN] + size(axisRange) * coord;
32013
+ }
32014
+ const axisStep = step[i];
32015
+ const nSteps = Math.floor(size(axisRange) / axisStep);
32016
+ const tick = Math.round(coord * nSteps);
32017
+ return axisRange[MIN] + axisStep * tick;
32018
+ }));
32019
+
32020
+ // same as normalizeCoords in interactive-graph.tsx
32021
+ const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
32022
+ return (coord + ranges[i][1]) / size(ranges[i]);
32023
+ }));
32024
+
32025
+ // Returns the closest point to the given `point` that is within the graph
32026
+ // bounds given in `state`.
32027
+ function bound$1({
32028
+ snapStep,
32029
+ range,
32030
+ point
32031
+ }) {
32032
+ const boundingBox = inset(snapStep, range);
32033
+ return clampToBox(boundingBox, point);
32034
+ }
32035
+ function isUnlimitedGraphState(state) {
32036
+ return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
32037
+ }
32038
+
32039
+ /**
32040
+ * Replace all text outside of the $ TeX blocks with `\\text{...}`
32041
+ * This way, the entire resulting string can be rendered within <TeX>
32042
+ * and the text outside of the $ blocks will be non-TeX text.
32043
+ */
32044
+ function replaceOutsideTeX(mathString) {
32045
+ // All the information we need is in the first section,
32046
+ // whether it's typed as "blockmath" or "paragraph"
32047
+ const firstSection = SimpleMarkdown.parse(mathString)[0];
32048
+
32049
+ // If it's blockMath, the outer level has the full math content.
32050
+ if (firstSection.type === "blockMath") {
32051
+ return firstSection.content;
32052
+ }
32053
+
32054
+ // If it's a paragraph, we need to iterate through the sections
32055
+ // to look for individual math blocks.
32056
+ const condensedNodes = condenseTextNodes(firstSection.content);
32057
+ let result = "";
32058
+ for (const piece of condensedNodes) {
32059
+ piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
32060
+ }
32061
+ return result;
32062
+ }
32063
+ // Helper function for replaceOutsideTeX()
32064
+ // Condense adjacent text nodes into a single text node
32065
+ function condenseTextNodes(nodes) {
32066
+ const result = [];
32067
+ let currentText = "";
32068
+ for (const node of nodes) {
32069
+ if (node.type === "math") {
32070
+ if (currentText) {
32071
+ result.push({
32072
+ type: "text",
32073
+ content: currentText
32074
+ });
32075
+ currentText = "";
32076
+ }
32077
+ result.push(node);
32078
+ } else {
32079
+ currentText += node.content;
32080
+ }
32081
+ }
32082
+ if (currentText) {
32083
+ result.push({
32084
+ type: "text",
32085
+ content: currentText
32086
+ });
32087
+ }
32088
+ return result;
32089
+ }
32090
+
32091
+ // Helper function for replaceOutsideTeX()
32092
+ function escapeSpecialChars(str) {
32093
+ // Escape $, \, {, and } characters
32094
+ return str.replace(/([$\\{}])/g, "\\$1");
32095
+ }
32096
+
31601
32097
  function LockedLabel(props) {
31602
32098
  const {
31603
32099
  coord,
@@ -31610,7 +32106,7 @@ function LockedLabel(props) {
31610
32106
  TeX
31611
32107
  } = getDependencies();
31612
32108
 
31613
- // Move this all outside the SVG element
32109
+ // Note: The TeX component cannot be rendered within an SVG
31614
32110
  return /*#__PURE__*/React.createElement("span", {
31615
32111
  className: "locked-label",
31616
32112
  style: {
@@ -31622,7 +32118,7 @@ function LockedLabel(props) {
31622
32118
  backgroundColor: "rgba(255, 255, 255, 0.8)"
31623
32119
  },
31624
32120
  "aria-hidden": true
31625
- }, /*#__PURE__*/React.createElement(TeX, null, text));
32121
+ }, /*#__PURE__*/React.createElement(TeX, null, replaceOutsideTeX(text)));
31626
32122
  }
31627
32123
 
31628
32124
  function GraphLockedLabelsLayer(props) {
@@ -32542,46 +33038,6 @@ function calculateBisectorPoint(point1, point2, vertex, isReflex, allowReflex, a
32542
33038
  return scaledBisector;
32543
33039
  }
32544
33040
 
32545
- /**
32546
- * 44 is touch best practice and AAA compliant for WCAG
32547
- * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
32548
- */
32549
- const TARGET_SIZE = 44;
32550
- const REMOVE_BUTTON_ID = "perseus_mafs_remove_button";
32551
-
32552
- // same as pointsFromNormalized in interactive-graph.tsx
32553
- const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
32554
- // maps over [x, y]
32555
- coords.map((coord, i) => {
32556
- const axisRange = range[i];
32557
- if (noSnap) {
32558
- return axisRange[MIN] + size(axisRange) * coord;
32559
- }
32560
- const axisStep = step[i];
32561
- const nSteps = Math.floor(size(axisRange) / axisStep);
32562
- const tick = Math.round(coord * nSteps);
32563
- return axisRange[MIN] + axisStep * tick;
32564
- }));
32565
-
32566
- // same as normalizeCoords in interactive-graph.tsx
32567
- const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
32568
- return (coord + ranges[i][1]) / size(ranges[i]);
32569
- }));
32570
-
32571
- // Returns the closest point to the given `point` that is within the graph
32572
- // bounds given in `state`.
32573
- function bound$1({
32574
- snapStep,
32575
- range,
32576
- point
32577
- }) {
32578
- const boundingBox = inset(snapStep, range);
32579
- return clampToBox(boundingBox, point);
32580
- }
32581
- function isUnlimitedGraphState(state) {
32582
- return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
32583
- }
32584
-
32585
33041
  var isProduction = process.env.NODE_ENV === 'production';
32586
33042
  var prefix = 'Invariant failed';
32587
33043
  function invariant(condition, message) {
@@ -35898,7 +36354,7 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
35898
36354
 
35899
36355
  const eq$1 = Util.eq;
35900
36356
  const deepEq$2 = Util.deepEq;
35901
- function interactiveGraphValidator(userInput, rubric) {
36357
+ function scoreInteractiveGraph(userInput, rubric) {
35902
36358
  // None-type graphs are not graded
35903
36359
  if (userInput.type === "none" && rubric.correct.type === "none") {
35904
36360
  return {
@@ -36007,7 +36463,7 @@ function interactiveGraphValidator(userInput, rubric) {
36007
36463
  if (rubric.correct.match === "similar") {
36008
36464
  match = similar(guess, correct, Number.POSITIVE_INFINITY);
36009
36465
  } else if (rubric.correct.match === "congruent") {
36010
- match = similar(guess, correct, number.DEFAULT_TOLERANCE);
36466
+ match = similar(guess, correct, number$1.DEFAULT_TOLERANCE);
36011
36467
  } else if (rubric.correct.match === "approx") {
36012
36468
  match = similar(guess, correct, 0.1);
36013
36469
  } else {
@@ -36316,7 +36772,7 @@ class LegacyInteractiveGraph extends React.Component {
36316
36772
  };
36317
36773
  this.setTrashCanVisibility = opacity => {
36318
36774
  const graphie = this.graphie;
36319
- if (number.equal(opacity, 0)) {
36775
+ if (number$1.equal(opacity, 0)) {
36320
36776
  if (this.trashCan) {
36321
36777
  this.trashCan.remove();
36322
36778
  this.trashCan = null;
@@ -37410,6 +37866,9 @@ class InteractiveGraph extends React.Component {
37410
37866
  }
37411
37867
  throw new PerseusError("Cannot getUserInput from a graph that has never rendered", Errors.NotAllowed);
37412
37868
  }
37869
+ getPromptJSON() {
37870
+ return getPromptJSON$b(this.props, this.getUserInput());
37871
+ }
37413
37872
  render() {
37414
37873
  var _this$props$apiOption;
37415
37874
  // Mafs shim
@@ -37788,7 +38247,7 @@ var InteractiveGraph$1 = {
37788
38247
  displayName: "Interactive graph (Assessments only)",
37789
38248
  widget: InteractiveGraph,
37790
38249
  staticTransform: staticTransform$3,
37791
- validator: interactiveGraphValidator
38250
+ scorer: scoreInteractiveGraph
37792
38251
  };
37793
38252
 
37794
38253
  // TODO: can we use WB typography?
@@ -37800,6 +38259,32 @@ const bodyXsmallBold = {
37800
38259
  lineHeight: "22px"
37801
38260
  };
37802
38261
 
38262
+ const getPromptJSON$a = (renderProps, userInput) => {
38263
+ const propMarkers = renderProps.markers.map(marker => {
38264
+ return {
38265
+ label: marker.label
38266
+ };
38267
+ });
38268
+ const inputMarkers = userInput.markers.map(marker => {
38269
+ return {
38270
+ label: marker.label,
38271
+ selected: marker.selected
38272
+ };
38273
+ });
38274
+ return {
38275
+ type: "label-image",
38276
+ options: {
38277
+ choices: renderProps.choices,
38278
+ imageUrl: renderProps.imageUrl,
38279
+ imageAlt: renderProps.imageAlt,
38280
+ markers: propMarkers
38281
+ },
38282
+ userInput: {
38283
+ markers: inputMarkers
38284
+ }
38285
+ };
38286
+ };
38287
+
37803
38288
  const AnswerChoices = props => {
37804
38289
  const {
37805
38290
  strings
@@ -37882,59 +38367,6 @@ const styles$d = StyleSheet.create({
37882
38367
  }
37883
38368
  });
37884
38369
 
37885
- // Question state for marker as result of user selected answers.
37886
-
37887
- function scoreMarker(marker) {
37888
- const score = {
37889
- hasAnswers: false,
37890
- isCorrect: false
37891
- };
37892
- if (marker.selected && marker.selected.length > 0) {
37893
- score.hasAnswers = true;
37894
- }
37895
- if (marker.answers.length > 0) {
37896
- if (marker.selected && marker.selected.length === marker.answers.length) {
37897
- // All correct answers are selected by the user.
37898
- score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
37899
- }
37900
- } else if (!marker.selected || marker.selected.length === 0) {
37901
- // Correct as no answers should be selected by the user.
37902
- score.isCorrect = true;
37903
- }
37904
- return score;
37905
- }
37906
-
37907
- // TODO(LEMS-2440): May need to pull answers out of PerseusLabelImageWidgetOptions[markers] for the rubric
37908
- function labelImageValidator(userInput, rubric) {
37909
- let numAnswered = 0;
37910
- let numCorrect = 0;
37911
- for (const marker of userInput.markers) {
37912
- const score = scoreMarker(marker);
37913
- if (score.hasAnswers) {
37914
- numAnswered++;
37915
- }
37916
- if (score.isCorrect) {
37917
- numCorrect++;
37918
- }
37919
- }
37920
-
37921
- // We expect all question markers to be answered before grading.
37922
- if (numAnswered !== userInput.markers.length) {
37923
- return {
37924
- type: "invalid",
37925
- message: null
37926
- };
37927
- }
37928
- return {
37929
- type: "points",
37930
- // Markers with no expected answers are graded as correct if user
37931
- // makes no answer selection.
37932
- earned: numCorrect === userInput.markers.length ? 1 : 0,
37933
- total: 1,
37934
- message: null
37935
- };
37936
- }
37937
-
37938
38370
  const BringToFront = {
37939
38371
  boxShadow: `0 8px 8px ${color.offBlack64}`,
37940
38372
  zIndex: 1000
@@ -38188,6 +38620,59 @@ const styles$b = StyleSheet.create({
38188
38620
  }
38189
38621
  });
38190
38622
 
38623
+ // Question state for marker as result of user selected answers.
38624
+
38625
+ function scoreMarker(marker) {
38626
+ const score = {
38627
+ hasAnswers: false,
38628
+ isCorrect: false
38629
+ };
38630
+ if (marker.selected && marker.selected.length > 0) {
38631
+ score.hasAnswers = true;
38632
+ }
38633
+ if (marker.answers.length > 0) {
38634
+ if (marker.selected && marker.selected.length === marker.answers.length) {
38635
+ // All correct answers are selected by the user.
38636
+ score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
38637
+ }
38638
+ } else if (!marker.selected || marker.selected.length === 0) {
38639
+ // Correct as no answers should be selected by the user.
38640
+ score.isCorrect = true;
38641
+ }
38642
+ return score;
38643
+ }
38644
+
38645
+ // TODO(LEMS-2440): May need to pull answers out of PerseusLabelImageWidgetOptions[markers] for the rubric
38646
+ function scoreLabelImage(userInput, rubric) {
38647
+ let numAnswered = 0;
38648
+ let numCorrect = 0;
38649
+ for (const marker of userInput.markers) {
38650
+ const score = scoreMarker(marker);
38651
+ if (score.hasAnswers) {
38652
+ numAnswered++;
38653
+ }
38654
+ if (score.isCorrect) {
38655
+ numCorrect++;
38656
+ }
38657
+ }
38658
+
38659
+ // We expect all question markers to be answered before grading.
38660
+ if (numAnswered !== userInput.markers.length) {
38661
+ return {
38662
+ type: "invalid",
38663
+ message: null
38664
+ };
38665
+ }
38666
+ return {
38667
+ type: "points",
38668
+ // Markers with no expected answers are graded as correct if user
38669
+ // makes no answer selection.
38670
+ earned: numCorrect === userInput.markers.length ? 1 : 0,
38671
+ total: 1,
38672
+ message: null
38673
+ };
38674
+ }
38675
+
38191
38676
  /**
38192
38677
  * Represents a direction vector.
38193
38678
  * No diagonals allowed.
@@ -38396,6 +38881,9 @@ class LabelImage extends React.Component {
38396
38881
  markers
38397
38882
  };
38398
38883
  }
38884
+ getPromptJSON() {
38885
+ return getPromptJSON$a(this.props, this.getUserInput());
38886
+ }
38399
38887
 
38400
38888
  // TODO(LEMS-2544): Investigate impact on scoring; possibly pull out &/or remove rubric parameter.
38401
38889
  showRationalesForCurrentlySelectedChoices(rubric) {
@@ -38752,7 +39240,7 @@ var LabelImage$1 = {
38752
39240
  widget: LabelImageWithDependencies,
38753
39241
  accessible: true,
38754
39242
  isLintable: true,
38755
- validator: labelImageValidator
39243
+ scorer: scoreLabelImage
38756
39244
  };
38757
39245
 
38758
39246
  var ItemState = /*#__PURE__*/function (ItemState) {
@@ -39464,7 +39952,23 @@ const styles$9 = StyleSheet.create({
39464
39952
  }
39465
39953
  });
39466
39954
 
39467
- function matcherValidator(state, rubric) {
39955
+ const getPromptJSON$9 = (renderProps, userInput) => {
39956
+ return {
39957
+ type: "matcher",
39958
+ options: {
39959
+ labels: renderProps.labels,
39960
+ left: renderProps.left,
39961
+ right: renderProps.right,
39962
+ orderMatters: renderProps.orderMatters
39963
+ },
39964
+ userInput: {
39965
+ left: userInput.left,
39966
+ right: userInput.right
39967
+ }
39968
+ };
39969
+ };
39970
+
39971
+ function scoreMatcher(state, rubric) {
39468
39972
  const correct = _.isEqual(state.left, rubric.left) && _.isEqual(state.right, rubric.right);
39469
39973
  return {
39470
39974
  type: "points",
@@ -39537,6 +40041,9 @@ class Matcher extends React.Component {
39537
40041
  this.refs.right.moveOptionToIndex(option, index);
39538
40042
  };
39539
40043
  }
40044
+ getPromptJSON() {
40045
+ return getPromptJSON$9(this.props, this.getUserInput());
40046
+ }
39540
40047
  render() {
39541
40048
  // To minimize layout shift, we display a spinner until our math
39542
40049
  // renderer is ready to render the math inside the matcher. To
@@ -39675,10 +40182,23 @@ var Matcher$1 = {
39675
40182
  displayName: "Matcher (two column)",
39676
40183
  widget: Matcher,
39677
40184
  isLintable: true,
39678
- validator: matcherValidator
40185
+ scorer: scoreMatcher
40186
+ };
40187
+
40188
+ const getPromptJSON$8 = (renderProps, userInput) => {
40189
+ return {
40190
+ type: "matrix",
40191
+ options: {
40192
+ height: renderProps.matrixBoardSize[0],
40193
+ width: renderProps.matrixBoardSize[1]
40194
+ },
40195
+ userInput: {
40196
+ answerRows: userInput.answers
40197
+ }
40198
+ };
39679
40199
  };
39680
40200
 
39681
- function matrixValidator(userInput, rubric, strings) {
40201
+ function scoreMatrix(userInput, rubric, strings) {
39682
40202
  const solution = rubric.answers;
39683
40203
  const supplied = userInput.answers;
39684
40204
  const solutionSize = getMatrixSize(solution);
@@ -39948,6 +40468,9 @@ class Matrix extends React.Component {
39948
40468
  answers: this.props.answers
39949
40469
  };
39950
40470
  }
40471
+ getPromptJSON() {
40472
+ return getPromptJSON$8(this.props, this.getUserInput());
40473
+ }
39951
40474
  render() {
39952
40475
  // Set the input sizes through JS so we can control the size of the
39953
40476
  // brackets. (If we set them in CSS we won't know values until the
@@ -40141,7 +40664,11 @@ var Matrix$1 = {
40141
40664
  transform: propTransform$1,
40142
40665
  staticTransform: staticTransform$2,
40143
40666
  isLintable: true,
40144
- validator: matrixValidator
40667
+ scorer: scoreMatrix
40668
+ };
40669
+
40670
+ const getPromptJSON$7 = () => {
40671
+ return getUnsupportedPromptJSON("measurer");
40145
40672
  };
40146
40673
 
40147
40674
  const defaultImage = {
@@ -40208,6 +40735,9 @@ class Measurer extends React.Component {
40208
40735
  });
40209
40736
  }
40210
40737
  }
40738
+ getPromptJSON() {
40739
+ return getPromptJSON$7();
40740
+ }
40211
40741
  render() {
40212
40742
  const image = _.extend({}, defaultImage, this.props.image);
40213
40743
 
@@ -40267,8 +40797,8 @@ var Measurer$1 = {
40267
40797
  minor: 0
40268
40798
  },
40269
40799
  propUpgrades: propUpgrades,
40270
- // TODO: things that aren't interactive shouldn't need validators
40271
- validator: () => noopValidator(1)
40800
+ // TODO: things that aren't interactive shouldn't need scoring functions
40801
+ scorer: () => scoreNoop(1)
40272
40802
  };
40273
40803
 
40274
40804
  /**
@@ -41157,16 +41687,31 @@ var Molecule$1 = {
41157
41687
  displayName: "Molecule renderer",
41158
41688
  hidden: true,
41159
41689
  widget: MoleculeWidget,
41160
- validator: () => noopValidator()
41690
+ scorer: () => scoreNoop()
41161
41691
  };
41162
41692
 
41163
- function numberLineValidator(state, rubric) {
41693
+ const getPromptJSON$6 = (renderProps, userInput) => {
41694
+ return {
41695
+ type: "number-line",
41696
+ options: {
41697
+ range: renderProps.range,
41698
+ numDivisions: renderProps.numDivisions,
41699
+ snapDivisions: renderProps.snapDivisions
41700
+ },
41701
+ userInput: {
41702
+ numLinePosition: userInput.numLinePosition,
41703
+ numDivisions: userInput.numDivisions
41704
+ }
41705
+ };
41706
+ };
41707
+
41708
+ function scoreNumberLine(state, rubric) {
41164
41709
  const range = rubric.range;
41165
41710
  const divisionRange = state.divisionRange;
41166
41711
  const start = rubric.initialX != null ? rubric.initialX : range[0];
41167
41712
  const startRel = rubric.isInequality ? "ge" : "eq";
41168
41713
  const correctRel = rubric.correctRel || "eq";
41169
- const correctPos = number.equal(state.numLinePosition, rubric.correctX || 0);
41714
+ const correctPos = number$1.equal(state.numLinePosition, rubric.correctX || 0);
41170
41715
  const outsideAllowedRange = state.numDivisions > divisionRange[1] || state.numDivisions < divisionRange[0];
41171
41716
 
41172
41717
  // TODO: I don't think isTickCrtl is a thing anymore
@@ -41295,7 +41840,7 @@ const TickMarks = Graphie.createSimpleClass((graphie, props) => {
41295
41840
  const x = range[0] + i * tickStep;
41296
41841
  fractions.push(x);
41297
41842
  }
41298
- const getDenom = x => number.toFraction(x)[1];
41843
+ const getDenom = x => number$1.toFraction(x)[1];
41299
41844
  const denoms = _.map(fractions, getDenom);
41300
41845
  base = _.reduce(denoms, (x, y) => KhanMath.getLCM(x, y));
41301
41846
  } else {
@@ -41358,7 +41903,7 @@ class NumberLine extends React.Component {
41358
41903
  let initialX = this.props.numLinePosition;
41359
41904
  const divisionRange = this.props.divisionRange;
41360
41905
  initialX = initialX == null ? range[0] : initialX;
41361
- return range[0] < range[1] && number.sign(initialX - range[0]) >= 0 && number.sign(initialX - range[1]) <= 0 && divisionRange[0] < divisionRange[1] && 0 < this.props.numDivisions && 0 < this.props.snapDivisions;
41906
+ return range[0] < range[1] && number$1.sign(initialX - range[0]) >= 0 && number$1.sign(initialX - range[1]) <= 0 && divisionRange[0] < divisionRange[1] && 0 < this.props.numDivisions && 0 < this.props.snapDivisions;
41362
41907
  };
41363
41908
  this.onNumDivisionsChange = (numDivisions, cb) => {
41364
41909
  const divRange = this.props.divisionRange.slice();
@@ -41461,7 +42006,7 @@ class NumberLine extends React.Component {
41461
42006
  const right = props.range[1];
41462
42007
  const snapX = props.tickStep / props.snapDivisions;
41463
42008
  let x = bound(numLinePosition, left, right);
41464
- x = left + number.roundTo(x - left, snapX);
42009
+ x = left + number$1.roundTo(x - left, snapX);
41465
42010
  assert$1(_.isFinite(x));
41466
42011
  return x;
41467
42012
  };
@@ -41628,6 +42173,9 @@ class NumberLine extends React.Component {
41628
42173
  divisionRange: this.props.divisionRange
41629
42174
  };
41630
42175
  }
42176
+ getPromptJSON() {
42177
+ return getPromptJSON$6(this.props, this.getUserInput());
42178
+ }
41631
42179
  render() {
41632
42180
  const {
41633
42181
  strings
@@ -41747,10 +42295,22 @@ var NumberLine$1 = {
41747
42295
  widget: NumberLine,
41748
42296
  transform: numberLineTransform,
41749
42297
  staticTransform: staticTransform$1,
41750
- validator: numberLineValidator
42298
+ scorer: scoreNumberLine
42299
+ };
42300
+
42301
+ const getPromptJSON$5 = (renderProps, userInput) => {
42302
+ return {
42303
+ type: "orderer",
42304
+ options: {
42305
+ options: renderProps.options.map(option => option.content)
42306
+ },
42307
+ userInput: {
42308
+ values: userInput.current
42309
+ }
42310
+ };
41751
42311
  };
41752
42312
 
41753
- function ordererValidator(userInput, rubric) {
42313
+ function scoreOrderer(userInput, rubric) {
41754
42314
  if (userInput.current.length === 0) {
41755
42315
  return {
41756
42316
  type: "invalid",
@@ -42194,6 +42754,9 @@ class Orderer extends React.Component {
42194
42754
  })
42195
42755
  };
42196
42756
  }
42757
+ getPromptJSON() {
42758
+ return getPromptJSON$5(this.props, this.getUserInput());
42759
+ }
42197
42760
  render() {
42198
42761
  // This is the card we are currently dragging
42199
42762
  const dragging = this.state.dragging && /*#__PURE__*/React.createElement(Card
@@ -42312,7 +42875,7 @@ var Orderer$1 = {
42312
42875
  hidden: true,
42313
42876
  widget: Orderer,
42314
42877
  isLintable: true,
42315
- validator: ordererValidator
42878
+ scorer: scoreOrderer
42316
42879
  };
42317
42880
 
42318
42881
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -42361,8 +42924,12 @@ var PassageRefTarget$1 = {
42361
42924
  minor: 0
42362
42925
  },
42363
42926
  isLintable: true,
42364
- // TODO: things that aren't interactive shouldn't need validators
42365
- validator: () => noopValidator()
42927
+ // TODO: things that aren't interactive shouldn't need scoring functions
42928
+ scorer: () => scoreNoop()
42929
+ };
42930
+
42931
+ const getPromptJSON$4 = () => {
42932
+ return getUnsupportedPromptJSON("phet-simulation");
42366
42933
  };
42367
42934
 
42368
42935
  /**
@@ -42420,6 +42987,9 @@ class PhetSimulation extends React.Component {
42420
42987
  await this.updateSimState(this.props.url);
42421
42988
  }
42422
42989
  }
42990
+ getPromptJSON() {
42991
+ return getPromptJSON$4();
42992
+ }
42423
42993
  async updateSimState(urlString) {
42424
42994
  const url = makeSafeUrl(urlString, this.locale);
42425
42995
  if (url === null) {
@@ -42568,10 +43138,14 @@ var PhetSimulation$1 = {
42568
43138
  isLintable: true
42569
43139
  };
42570
43140
 
43141
+ const getPromptJSON$3 = () => {
43142
+ return getUnsupportedPromptJSON("plotter");
43143
+ };
43144
+
42571
43145
  const {
42572
43146
  deepEq: deepEq$1
42573
43147
  } = Util;
42574
- function plotterValidator(userInput, rubric) {
43148
+ function scorePlotter(userInput, rubric) {
42575
43149
  if (deepEq$1(userInput, rubric.starting)) {
42576
43150
  return {
42577
43151
  type: "invalid",
@@ -43344,6 +43918,9 @@ class Plotter extends React.Component {
43344
43918
  getUserInput() {
43345
43919
  return this.state.values;
43346
43920
  }
43921
+ getPromptJSON() {
43922
+ return getPromptJSON$3();
43923
+ }
43347
43924
  render() {
43348
43925
  // TODO(kevinb) actually compute the size of the graphie correctly and
43349
43926
  // make it that size so we don't have to add extra padding. The value
@@ -43384,7 +43961,11 @@ var Plotter$1 = {
43384
43961
  hidden: true,
43385
43962
  widget: Plotter,
43386
43963
  staticTransform: staticTransform,
43387
- validator: plotterValidator
43964
+ scorer: scorePlotter
43965
+ };
43966
+
43967
+ const getPromptJSON$2 = () => {
43968
+ return getUnsupportedPromptJSON("python-program");
43388
43969
  };
43389
43970
 
43390
43971
  /**
@@ -43406,6 +43987,9 @@ class PythonProgram extends React.Component {
43406
43987
  getDOMNodeForPath(path) {
43407
43988
  return null;
43408
43989
  }
43990
+ getPromptJSON() {
43991
+ return getPromptJSON$2();
43992
+ }
43409
43993
  render() {
43410
43994
  const url = getUrlFromProgramID(this.props.programID);
43411
43995
  const iframeStyle = {
@@ -43443,7 +44027,17 @@ var PythonProgram$1 = {
43443
44027
  widget: PythonProgram
43444
44028
  };
43445
44029
 
43446
- function sorterValidator(userInput, rubric) {
44030
+ const getPromptJSON$1 = userInput => {
44031
+ return {
44032
+ type: "sorter",
44033
+ userInput: {
44034
+ values: userInput.options,
44035
+ changed: userInput.changed
44036
+ }
44037
+ };
44038
+ };
44039
+
44040
+ function scoreSorter(userInput, rubric) {
43447
44041
  // If the sorter widget hasn't been changed yet, we treat it as "empty" which
43448
44042
  // prevents the "Check" button from becoming active. We want the user
43449
44043
  // to make a change before trying to move forward. This makes an
@@ -43515,6 +44109,9 @@ class Sorter extends React.Component {
43515
44109
  changed: this.state.changed
43516
44110
  };
43517
44111
  }
44112
+ getPromptJSON() {
44113
+ return getPromptJSON$1(this.getUserInput());
44114
+ }
43518
44115
  render() {
43519
44116
  const options = shuffle(this.props.correct, this.props.problemNum, /* ensurePermuted */true);
43520
44117
  const {
@@ -43549,10 +44146,10 @@ var Sorter$1 = {
43549
44146
  displayName: "Sorter",
43550
44147
  widget: Sorter,
43551
44148
  isLintable: true,
43552
- validator: sorterValidator
44149
+ scorer: scoreSorter
43553
44150
  };
43554
44151
 
43555
- function tableValidator(state, rubric, strings) {
44152
+ function scoreTable(state, rubric, strings) {
43556
44153
  const filterNonEmpty = function filterNonEmpty(table) {
43557
44154
  return _.filter(table, function (row) {
43558
44155
  // Check if row has a cell that is nonempty
@@ -43829,7 +44426,11 @@ var Table$1 = {
43829
44426
  transform: propTransform,
43830
44427
  hidden: true,
43831
44428
  isLintable: true,
43832
- validator: tableValidator
44429
+ scorer: scoreTable
44430
+ };
44431
+
44432
+ const getPromptJSON = () => {
44433
+ return getUnsupportedPromptJSON("video");
43833
44434
  };
43834
44435
 
43835
44436
  /**
@@ -43925,6 +44526,9 @@ class Video extends React.Component {
43925
44526
  return change.apply(this, args);
43926
44527
  };
43927
44528
  }
44529
+ getPromptJSON() {
44530
+ return getPromptJSON();
44531
+ }
43928
44532
  render() {
43929
44533
  const {
43930
44534
  InitialRequestUrl
@@ -43985,8 +44589,8 @@ var Video$1 = {
43985
44589
  defaultAlignment: "block",
43986
44590
  supportedAlignments: ["block", "float-left", "float-right", "full-width"],
43987
44591
  widget: Video,
43988
- // TODO: things that aren't interactive shouldn't need validators
43989
- validator: () => noopValidator()
44592
+ // TODO: things that aren't interactive shouldn't need scoring functions
44593
+ scorer: () => scoreNoop()
43990
44594
  };
43991
44595
 
43992
44596
  /* eslint-disable import/no-named-as-default */
@@ -43996,41 +44600,16 @@ var extraWidgets = [Categorizer$1, CSProgram$1, Dropdown$1, Explanation$1, Defin
43996
44600
  * This should be called by all clients, specifying whether extra widgets are
43997
44601
  * needed via `loadExtraWidgets`. It is idempotent, so it's not a problem to
43998
44602
  * call it multiple times.
43999
- *
44000
- * skipMathJax:
44001
- * If false/undefined, MathJax will be configured, and the
44002
- * promise will wait for MathJax to load (if it hasn't already).
44003
44603
  */
44004
- const init = function init(options) {
44604
+ const init = function init() {
44005
44605
  registerWidgets(basicWidgets);
44006
44606
  registerWidgets(extraWidgets);
44007
44607
  replaceDeprecatedWidgets();
44008
-
44009
- // Pass skipMathJax: true if MathJax is already loaded and configured.
44010
- const skipMathJax = options.skipMathJax;
44011
- if (skipMathJax) {
44012
- // @ts-expect-error - TS2322 - Type 'Promise<void>' is not assignable to type 'Promise<undefined>'.
44013
- return Promise.resolve();
44014
- }
44015
- return new Promise((resolve, reject) => {
44016
- MathJax.Hub.Config({
44017
- messageStyle: "none",
44018
- skipStartupTypeset: "none",
44019
- "HTML-CSS": {
44020
- availableFonts: ["TeX"],
44021
- imageFont: null,
44022
- scale: 100,
44023
- showMathMenu: false
44024
- }
44025
- });
44026
- MathJax.Hub.Configured();
44027
- MathJax.Hub.Queue(resolve);
44028
- });
44029
44608
  };
44030
44609
 
44031
44610
  // This file is processed by a Rollup plugin (replace) to inject the production
44032
44611
  const libName = "@khanacademy/perseus";
44033
- const libVersion = "41.1.1";
44612
+ const libVersion = "41.3.0";
44034
44613
  addLibraryVersionToPerseusDebug(libName, libVersion);
44035
44614
 
44036
44615
  const apiVersion = {
@@ -44767,6 +45346,9 @@ class ServerItemRenderer extends React.Component {
44767
45346
  getNumHints() {
44768
45347
  return this.props.item.hints.length;
44769
45348
  }
45349
+ getPromptJSON() {
45350
+ return this.questionRenderer.getPromptJSON();
45351
+ }
44770
45352
 
44771
45353
  /**
44772
45354
  * Grades the item.
@@ -45414,7 +45996,7 @@ function treeToItem(node) {
45414
45996
  };
45415
45997
  }
45416
45998
 
45417
- var isObject = function isObject(obj) {
45999
+ var isObject$1 = function isObject(obj) {
45418
46000
  return obj === Object(obj);
45419
46001
  };
45420
46002
  var merge = function merge() {
@@ -45430,7 +46012,7 @@ var merge = function merge() {
45430
46012
  return obj;
45431
46013
  };
45432
46014
  var clone = function clone(obj) {
45433
- if (!isObject(obj)) {
46015
+ if (!isObject$1(obj)) {
45434
46016
  return obj;
45435
46017
  }
45436
46018
  return Array.isArray(obj) ? obj.slice() : merge(obj);
@@ -45446,7 +46028,7 @@ var clone = function clone(obj) {
45446
46028
  var dispatch = function dispatch(x) {
45447
46029
  if (Array.isArray(x)) {
45448
46030
  return arr;
45449
- } else if (isObject(x)) {
46031
+ } else if (isObject$1(x)) {
45450
46032
  return obj;
45451
46033
  } else if (typeof x === "string") {
45452
46034
  return str;
@@ -47367,6 +47949,690 @@ function buildTestData(testObject) {
47367
47949
  return `{"data":{"assessmentItem":{"__typename":"AssessmentItemOrError","error":null,"item":{"__typename":"AssessmentItem","id":"x890b3c70f3e8f4a6","itemData":"${testObject}","problemType":"Type 1","sha":"c7284a3ad65214b4e62bccce236d92f7f5d35941"}}}}`;
47368
47950
  }
47369
47951
 
47952
+ function success(value) {
47953
+ return {
47954
+ type: "success",
47955
+ value
47956
+ };
47957
+ }
47958
+ function failure(detail) {
47959
+ return {
47960
+ type: "failure",
47961
+ detail
47962
+ };
47963
+ }
47964
+ function isFailure(result) {
47965
+ return result.type === "failure";
47966
+ }
47967
+ function isSuccess(result) {
47968
+ return result.type === "success";
47969
+ }
47970
+
47971
+ // Result's `all` function is similar to Promise.all: given an array of
47972
+ // results, it returns success if all succeeded, and failure if any failed.
47973
+ function all(results, combineFailureDetails = a => a) {
47974
+ const values = [];
47975
+ const failureDetails = [];
47976
+ for (const result of results) {
47977
+ if (result.type === "success") {
47978
+ values.push(result.value);
47979
+ } else {
47980
+ failureDetails.push(result.detail);
47981
+ }
47982
+ }
47983
+ if (failureDetails.length > 0) {
47984
+ return failure(failureDetails.reduce(combineFailureDetails));
47985
+ }
47986
+ return success(values);
47987
+ }
47988
+
47989
+ const any = (rawValue, ctx) => ctx.success(rawValue);
47990
+
47991
+ function array(elementParser) {
47992
+ return (rawValue, ctx) => {
47993
+ if (!Array.isArray(rawValue)) {
47994
+ return ctx.failure("array", rawValue);
47995
+ }
47996
+ const elementResults = rawValue.map((elem, i) => elementParser(elem, ctx.forSubtree(i)));
47997
+ return all(elementResults, concat);
47998
+ };
47999
+ }
48000
+ function concat(a, b) {
48001
+ return [...a, ...b];
48002
+ }
48003
+
48004
+ function boolean(rawValue, ctx) {
48005
+ if (typeof rawValue === "boolean") {
48006
+ return ctx.success(rawValue);
48007
+ }
48008
+ return ctx.failure("boolean", rawValue);
48009
+ }
48010
+
48011
+ function constant(acceptedValue) {
48012
+ return (rawValue, ctx) => {
48013
+ if (rawValue !== acceptedValue) {
48014
+ return ctx.failure(String(JSON.stringify(acceptedValue)), rawValue);
48015
+ }
48016
+ return ctx.success(acceptedValue);
48017
+ };
48018
+ }
48019
+
48020
+ function enumeration(...acceptedValues) {
48021
+ return (rawValue, ctx) => {
48022
+ if (typeof rawValue === "string") {
48023
+ const index = acceptedValues.indexOf(rawValue);
48024
+ if (index > -1) {
48025
+ return ctx.success(acceptedValues[index]);
48026
+ }
48027
+ }
48028
+ const expected = acceptedValues.map(v => JSON.stringify(v));
48029
+ return ctx.failure(expected, rawValue);
48030
+ };
48031
+ }
48032
+
48033
+ function isObject(x) {
48034
+ return x != null && Object.getPrototypeOf(x) === Object.prototype;
48035
+ }
48036
+
48037
+ function nullable(parseValue) {
48038
+ return (rawValue, ctx) => {
48039
+ if (rawValue === null) {
48040
+ return ctx.success(rawValue);
48041
+ }
48042
+ return parseValue(rawValue, ctx);
48043
+ };
48044
+ }
48045
+
48046
+ const number = (rawValue, ctx) => {
48047
+ if (typeof rawValue === "number") {
48048
+ return ctx.success(rawValue);
48049
+ }
48050
+ return ctx.failure("number", rawValue);
48051
+ };
48052
+
48053
+ function object(schema) {
48054
+ return (rawValue, ctx) => {
48055
+ if (!isObject(rawValue)) {
48056
+ return ctx.failure("object", rawValue);
48057
+ }
48058
+ const ret = _extends({}, rawValue);
48059
+ const mismatches = [];
48060
+ for (const [prop, propParser] of Object.entries(schema)) {
48061
+ const result = propParser(rawValue[prop], ctx.forSubtree(prop));
48062
+ if (isSuccess(result)) {
48063
+ ret[prop] = result.value;
48064
+ } else {
48065
+ mismatches.push(...result.detail);
48066
+ }
48067
+ }
48068
+ if (mismatches.length > 0) {
48069
+ return failure(mismatches);
48070
+ }
48071
+ return ctx.success(ret);
48072
+ };
48073
+ }
48074
+
48075
+ function optional(parseValue) {
48076
+ return (rawValue, ctx) => {
48077
+ if (rawValue === undefined) {
48078
+ return ctx.success(rawValue);
48079
+ }
48080
+ return parseValue(rawValue, ctx);
48081
+ };
48082
+ }
48083
+
48084
+ function pair(parseA, parseB) {
48085
+ return (rawValue, ctx) => {
48086
+ if (!Array.isArray(rawValue)) {
48087
+ return ctx.failure("array", rawValue);
48088
+ }
48089
+ if (rawValue.length !== 2) {
48090
+ return ctx.failure("array of length 2", rawValue);
48091
+ }
48092
+ const [rawA, rawB] = rawValue;
48093
+ const resultA = parseA(rawA, ctx.forSubtree(0));
48094
+ if (isFailure(resultA)) {
48095
+ return resultA;
48096
+ }
48097
+ const resultB = parseB(rawB, ctx.forSubtree(1));
48098
+ if (isFailure(resultB)) {
48099
+ return resultB;
48100
+ }
48101
+ return ctx.success([resultA.value, resultB.value]);
48102
+ };
48103
+ }
48104
+
48105
+ function record(parseKey, parseValue) {
48106
+ return (rawValue, ctx) => {
48107
+ if (!isObject(rawValue)) {
48108
+ return ctx.failure("object", rawValue);
48109
+ }
48110
+ const result = {};
48111
+ const mismatches = [];
48112
+ for (const [key, value] of Object.entries(rawValue)) {
48113
+ const entryCtx = ctx.forSubtree(key);
48114
+ const keyResult = parseKey(key, entryCtx);
48115
+ if (isFailure(keyResult)) {
48116
+ mismatches.push(...keyResult.detail);
48117
+ }
48118
+ const valueResult = parseValue(value, entryCtx);
48119
+ if (isFailure(valueResult)) {
48120
+ mismatches.push(...valueResult.detail);
48121
+ }
48122
+ if (isSuccess(keyResult) && isSuccess(valueResult)) {
48123
+ result[keyResult.value] = valueResult.value;
48124
+ }
48125
+ }
48126
+ if (mismatches.length > 0) {
48127
+ return failure(mismatches);
48128
+ }
48129
+ return ctx.success(result);
48130
+ };
48131
+ }
48132
+
48133
+ const string = (rawValue, ctx) => {
48134
+ if (typeof rawValue === "string") {
48135
+ return ctx.success(rawValue);
48136
+ }
48137
+ return ctx.failure("string", rawValue);
48138
+ };
48139
+
48140
+ function trio(parseA, parseB, parseC) {
48141
+ return (rawValue, ctx) => {
48142
+ if (!Array.isArray(rawValue)) {
48143
+ return ctx.failure("array", rawValue);
48144
+ }
48145
+ if (rawValue.length !== 3) {
48146
+ return ctx.failure("array of length 3", rawValue);
48147
+ }
48148
+ const resultA = parseA(rawValue[0], ctx.forSubtree(0));
48149
+ if (isFailure(resultA)) {
48150
+ return resultA;
48151
+ }
48152
+ const resultB = parseB(rawValue[1], ctx.forSubtree(1));
48153
+ if (isFailure(resultB)) {
48154
+ return resultB;
48155
+ }
48156
+ const resultC = parseC(rawValue[2], ctx.forSubtree(2));
48157
+ if (isFailure(resultC)) {
48158
+ return resultC;
48159
+ }
48160
+ return ctx.success([resultA.value, resultB.value, resultC.value]);
48161
+ };
48162
+ }
48163
+
48164
+ function union(parseBranch) {
48165
+ return new UnionBuilder(parseBranch);
48166
+ }
48167
+ class UnionBuilder {
48168
+ constructor(parser) {
48169
+ this.parser = parser;
48170
+ }
48171
+ or(newBranch) {
48172
+ return new UnionBuilder(either(this.parser, newBranch));
48173
+ }
48174
+ }
48175
+ function either(parseA, parseB) {
48176
+ return (rawValue, ctx) => {
48177
+ const resultA = parseA(rawValue, ctx);
48178
+ if (isSuccess(resultA)) {
48179
+ return resultA;
48180
+ }
48181
+ return parseB(rawValue, ctx);
48182
+ };
48183
+ }
48184
+
48185
+ function parseWidget(parseType, parseOptions) {
48186
+ return object({
48187
+ type: parseType,
48188
+ static: optional(boolean),
48189
+ graded: optional(boolean),
48190
+ alignment: optional(string),
48191
+ options: parseOptions,
48192
+ key: optional(number),
48193
+ version: object({
48194
+ major: number,
48195
+ minor: number
48196
+ })
48197
+ });
48198
+ }
48199
+
48200
+ const parseCategorizerWidget = parseWidget(constant("categorizer"), object({
48201
+ items: array(string),
48202
+ categories: array(string),
48203
+ randomizeItems: boolean,
48204
+ static: boolean,
48205
+ values: array(number),
48206
+ highlightLint: optional(boolean),
48207
+ linterContext: optional(object({
48208
+ contentType: string,
48209
+ paths: array(string),
48210
+ stack: array(string)
48211
+ }))
48212
+ }));
48213
+
48214
+ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
48215
+ programID: string,
48216
+ programType: any,
48217
+ settings: array(object({
48218
+ name: string,
48219
+ value: string
48220
+ })),
48221
+ showEditor: boolean,
48222
+ showButtons: boolean,
48223
+ width: number,
48224
+ height: number,
48225
+ static: boolean
48226
+ }));
48227
+
48228
+ const parseDefinitionWidget = parseWidget(constant("definition"), object({
48229
+ togglePrompt: string,
48230
+ definition: string,
48231
+ static: boolean
48232
+ }));
48233
+
48234
+ const parseDropdownWidget = parseWidget(constant("dropdown"), object({
48235
+ placeholder: string,
48236
+ static: boolean,
48237
+ choices: array(object({
48238
+ content: string,
48239
+ correct: boolean
48240
+ }))
48241
+ }));
48242
+
48243
+ const parseExplanationWidget = parseWidget(constant("explanation"), object({
48244
+ showPrompt: string,
48245
+ hidePrompt: string,
48246
+ explanation: string,
48247
+ // We wrap parseWidgetsMap in a function here to make sure it is not
48248
+ // referenced before it is defined. There is an import cycle between
48249
+ // this file and widgets-map.ts that could cause it to be undefined.
48250
+ widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
48251
+ static: boolean
48252
+ }));
48253
+
48254
+ const parseAnswerForm = object({
48255
+ value: string,
48256
+ form: boolean,
48257
+ simplify: boolean,
48258
+ considered: enumeration("correct", "wrong", "ungraded"),
48259
+ key: optional(string)
48260
+ });
48261
+ const parseExpressionWidget = parseWidget(constant("expression"), object({
48262
+ answerForms: array(parseAnswerForm),
48263
+ functions: array(string),
48264
+ times: boolean,
48265
+ visibleLabel: optional(string),
48266
+ ariaLabel: optional(string),
48267
+ buttonSets: array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")),
48268
+ buttonsVisible: optional(enumeration("always", "never", "focused"))
48269
+ }));
48270
+
48271
+ const parsePerseusImageBackground = object({
48272
+ url: optional(nullable(string)),
48273
+ width: optional(number),
48274
+ height: optional(number),
48275
+ top: optional(number),
48276
+ left: optional(number),
48277
+ bottom: optional(number),
48278
+ // TODO(benchristel): convert scale to a number
48279
+ scale: optional(union(number).or(string).parser)
48280
+ });
48281
+
48282
+ // Used to represent 2-D points and ranges
48283
+ const pairOfNumbers = pair(number, number);
48284
+ const parsePerseusGraphTypeAngle = object({
48285
+ type: constant("angle"),
48286
+ showAngles: optional(boolean),
48287
+ allowReflexAngles: optional(boolean),
48288
+ angleOffsetDeg: optional(number),
48289
+ snapDegrees: optional(number),
48290
+ match: optional(constant("congruent")),
48291
+ coords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
48292
+ startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))
48293
+ });
48294
+ const parsePerseusGraphTypeCircle = object({
48295
+ type: constant("circle"),
48296
+ center: optional(pairOfNumbers),
48297
+ radius: optional(number),
48298
+ startCoords: optional(object({
48299
+ center: pairOfNumbers,
48300
+ radius: number
48301
+ })),
48302
+ // TODO: remove coord? it's legacy.
48303
+ coord: optional(pairOfNumbers)
48304
+ });
48305
+ const parsePerseusGraphTypeLinear = object({
48306
+ type: constant("linear"),
48307
+ coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
48308
+ startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
48309
+ // TODO: remove coord? it's legacy.
48310
+ coord: optional(pairOfNumbers)
48311
+ });
48312
+ const parsePerseusGraphTypeLinearSystem = object({
48313
+ type: constant("linear-system"),
48314
+ // TODO(benchristel): default coords to empty array?
48315
+ coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
48316
+ startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
48317
+ // TODO: remove coord? it's legacy.
48318
+ coord: optional(pairOfNumbers)
48319
+ });
48320
+ const parsePerseusGraphTypeNone = object({
48321
+ type: constant("none")
48322
+ });
48323
+ const parsePerseusGraphTypePoint = object({
48324
+ type: constant("point"),
48325
+ numPoints: optional(union(number).or(constant("unlimited")).parser),
48326
+ coords: optional(nullable(array(pairOfNumbers))),
48327
+ startCoords: optional(array(pairOfNumbers)),
48328
+ // TODO: remove coord? it's legacy.
48329
+ coord: optional(pairOfNumbers)
48330
+ });
48331
+ const parsePerseusGraphTypePolygon = object({
48332
+ type: constant("polygon"),
48333
+ numSides: optional(union(number).or(constant("unlimited")).parser),
48334
+ showAngles: optional(boolean),
48335
+ showSides: optional(boolean),
48336
+ snapTo: optional(enumeration("grid", "angles", "sides")),
48337
+ match: optional(enumeration("similar", "congruent", "approx")),
48338
+ startCoords: optional(array(pairOfNumbers)),
48339
+ // TODO: remove coord? it's legacy.
48340
+ coord: optional(pairOfNumbers)
48341
+ });
48342
+ const parsePerseusGraphTypeQuadratic = object({
48343
+ type: constant("quadratic"),
48344
+ coords: optional(nullable(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))),
48345
+ startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
48346
+ // TODO: remove coord? it's legacy.
48347
+ coord: optional(pairOfNumbers)
48348
+ });
48349
+ const parsePerseusGraphTypeRay = object({
48350
+ type: constant("ray"),
48351
+ coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
48352
+ startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
48353
+ // TODO: remove coord? it's legacy.
48354
+ coord: optional(pairOfNumbers)
48355
+ });
48356
+ const parsePerseusGraphTypeSegment = object({
48357
+ type: constant("segment"),
48358
+ // TODO(benchristel): default numSegments?
48359
+ numSegments: optional(number),
48360
+ coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
48361
+ startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
48362
+ // TODO: remove coord? it's legacy.
48363
+ coord: optional(pairOfNumbers)
48364
+ });
48365
+ const parsePerseusGraphTypeSinusoid = object({
48366
+ type: constant("sinusoid"),
48367
+ coords: optional(nullable(array(pairOfNumbers))),
48368
+ startCoords: optional(array(pairOfNumbers)),
48369
+ // TODO: remove coord? it's legacy.
48370
+ coord: optional(pairOfNumbers)
48371
+ });
48372
+ const parsePerseusGraphType = union(parsePerseusGraphTypeAngle).or(parsePerseusGraphTypeAngle).or(parsePerseusGraphTypeCircle).or(parsePerseusGraphTypeLinear).or(parsePerseusGraphTypeLinearSystem).or(parsePerseusGraphTypeNone).or(parsePerseusGraphTypePoint).or(parsePerseusGraphTypePolygon).or(parsePerseusGraphTypeQuadratic).or(parsePerseusGraphTypeRay).or(parsePerseusGraphTypeSegment).or(parsePerseusGraphTypeSinusoid).parser;
48373
+ const parseLockedFigureColor = enumeration(...lockedFigureColorNames);
48374
+ const parseLockedFigureFillType = enumeration("none", "white", "translucent", "solid");
48375
+ const parseLockedLineStyle = enumeration("solid", "dashed");
48376
+ const parseLockedLabelType = object({
48377
+ type: constant("label"),
48378
+ coord: pairOfNumbers,
48379
+ text: string,
48380
+ color: parseLockedFigureColor,
48381
+ size: enumeration("small", "medium", "large")
48382
+ });
48383
+ const parseLockedPointType = object({
48384
+ type: constant("point"),
48385
+ coord: pairOfNumbers,
48386
+ color: parseLockedFigureColor,
48387
+ filled: boolean,
48388
+ // TODO(benchristel): default labels to empty array?
48389
+ labels: optional(array(parseLockedLabelType)),
48390
+ ariaLabel: optional(string)
48391
+ });
48392
+ const parseLockedLineType = object({
48393
+ type: constant("line"),
48394
+ kind: enumeration("line", "ray", "segment"),
48395
+ points: pair(parseLockedPointType, parseLockedPointType),
48396
+ color: parseLockedFigureColor,
48397
+ lineStyle: parseLockedLineStyle,
48398
+ showPoint1: boolean,
48399
+ showPoint2: boolean,
48400
+ // TODO(benchristel): default labels to empty array?
48401
+ labels: optional(array(parseLockedLabelType)),
48402
+ ariaLabel: optional(string)
48403
+ });
48404
+ const parseLockedVectorType = object({
48405
+ type: constant("vector"),
48406
+ points: pair(pairOfNumbers, pairOfNumbers),
48407
+ color: parseLockedFigureColor,
48408
+ // TODO(benchristel): default labels to empty array?
48409
+ labels: optional(array(parseLockedLabelType)),
48410
+ ariaLabel: optional(string)
48411
+ });
48412
+ const parseLockedEllipseType = object({
48413
+ type: constant("ellipse"),
48414
+ center: pairOfNumbers,
48415
+ radius: pairOfNumbers,
48416
+ angle: number,
48417
+ color: parseLockedFigureColor,
48418
+ fillStyle: parseLockedFigureFillType,
48419
+ strokeStyle: parseLockedLineStyle,
48420
+ // TODO(benchristel): default labels to empty array?
48421
+ labels: optional(array(parseLockedLabelType)),
48422
+ ariaLabel: optional(string)
48423
+ });
48424
+ const parseLockedPolygonType = object({
48425
+ type: constant("polygon"),
48426
+ points: array(pairOfNumbers),
48427
+ color: parseLockedFigureColor,
48428
+ showVertices: boolean,
48429
+ fillStyle: parseLockedFigureFillType,
48430
+ strokeStyle: parseLockedLineStyle,
48431
+ // TODO(benchristel): default labels to empty array?
48432
+ labels: optional(array(parseLockedLabelType)),
48433
+ ariaLabel: optional(string)
48434
+ });
48435
+ const parseLockedFunctionType = object({
48436
+ type: constant("function"),
48437
+ color: parseLockedFigureColor,
48438
+ strokeStyle: parseLockedLineStyle,
48439
+ equation: string,
48440
+ directionalAxis: enumeration("x", "y"),
48441
+ domain: optional(pairOfNumbers),
48442
+ // TODO(benchristel): default labels to empty array?
48443
+ labels: optional(array(parseLockedLabelType)),
48444
+ ariaLabel: optional(string)
48445
+ });
48446
+ const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
48447
+ const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
48448
+ step: pairOfNumbers,
48449
+ gridStep: pairOfNumbers,
48450
+ snapStep: pairOfNumbers,
48451
+ backgroundImage: optional(parsePerseusImageBackground),
48452
+ markings: enumeration("graph", "grid", "none"),
48453
+ labels: array(string),
48454
+ showProtractor: boolean,
48455
+ showRuler: optional(boolean),
48456
+ showTooltips: optional(boolean),
48457
+ rulerLabel: optional(string),
48458
+ rulerTicks: optional(number),
48459
+ range: pair(pairOfNumbers, pairOfNumbers),
48460
+ graph: parsePerseusGraphType,
48461
+ correct: parsePerseusGraphType,
48462
+ // TODO(benchristel): default lockedFigures to empty array
48463
+ lockedFigures: optional(array(parseLockedFigure)),
48464
+ fullGraphLabel: optional(string),
48465
+ fullGraphAriaDescription: optional(string)
48466
+ }));
48467
+
48468
+ const parseWidgetsMap = (rawValue, ctx) => {
48469
+ if (!isObject(rawValue)) {
48470
+ return ctx.failure("PerseusWidgetsMap", rawValue);
48471
+ }
48472
+ const widgetsMap = {};
48473
+ for (const key of Object.keys(rawValue)) {
48474
+ // parseWidgetsMapEntry modifies the widgetsMap. This is kind of gross,
48475
+ // but it's the only way I could find to make TypeScript check the key
48476
+ // against the widget type.
48477
+ const entryResult = parseWidgetsMapEntry([key, rawValue[key]], widgetsMap, ctx.forSubtree(key));
48478
+ if (isFailure(entryResult)) {
48479
+ return entryResult;
48480
+ }
48481
+ }
48482
+ return ctx.success(widgetsMap);
48483
+ };
48484
+ const parseWidgetsMapEntry = ([key, widget], widgetMap, ctx) => {
48485
+ const keyComponentsResult = parseWidgetMapKeyComponents(key.split(" "), ctx);
48486
+ if (isFailure(keyComponentsResult)) {
48487
+ return keyComponentsResult;
48488
+ }
48489
+ const [type, id] = keyComponentsResult.value;
48490
+ function parseAndAssign(key, parse) {
48491
+ const widgetResult = parse(widget, ctx);
48492
+ if (isFailure(widgetResult)) {
48493
+ return widgetResult;
48494
+ }
48495
+ widgetMap[key] = widgetResult.value;
48496
+ return ctx.success(undefined);
48497
+ }
48498
+ switch (type) {
48499
+ case "categorizer":
48500
+ return parseAndAssign(`categorizer ${id}`, parseCategorizerWidget);
48501
+ case "cs-program":
48502
+ return parseAndAssign(`cs-program ${id}`, parseCSProgramWidget);
48503
+ case "definition":
48504
+ return parseAndAssign(`definition ${id}`, parseDefinitionWidget);
48505
+ case "dropdown":
48506
+ return parseAndAssign(`dropdown ${id}`, parseDropdownWidget);
48507
+ case "explanation":
48508
+ return parseAndAssign(`explanation ${id}`, parseExplanationWidget);
48509
+ case "expression":
48510
+ return parseAndAssign(`expression ${id}`, parseExpressionWidget);
48511
+ case "grapher":
48512
+ // TODO(LEMS-2585): implement a real parser for this widget
48513
+ return parseAndAssign(`grapher ${id}`, any);
48514
+ case "group":
48515
+ // TODO(LEMS-2585): implement a real parser for this widget
48516
+ return parseAndAssign(`group ${id}`, any);
48517
+ case "graded-group":
48518
+ // TODO(LEMS-2585): implement a real parser for this widget
48519
+ return parseAndAssign(`graded-group ${id}`, any);
48520
+ case "graded-group-set":
48521
+ // TODO(LEMS-2585): implement a real parser for this widget
48522
+ return parseAndAssign(`graded-group-set ${id}`, any);
48523
+ case "iframe":
48524
+ // TODO(LEMS-2585): implement a real parser for this widget
48525
+ return parseAndAssign(`iframe ${id}`, any);
48526
+ case "image":
48527
+ // TODO(LEMS-2585): implement a real parser for this widget
48528
+ return parseAndAssign(`image ${id}`, any);
48529
+ case "input-number":
48530
+ // TODO(LEMS-2585): implement a real parser for this widget
48531
+ return parseAndAssign(`input-number ${id}`, any);
48532
+ case "interaction":
48533
+ // TODO(LEMS-2585): implement a real parser for this widget
48534
+ return parseAndAssign(`interaction ${id}`, any);
48535
+ case "interactive-graph":
48536
+ return parseAndAssign(`interactive-graph ${id}`, parseInteractiveGraphWidget);
48537
+ case "label-image":
48538
+ // TODO(LEMS-2585): implement a real parser for this widget
48539
+ return parseAndAssign(`label-image ${id}`, any);
48540
+ case "matcher":
48541
+ // TODO(LEMS-2585): implement a real parser for this widget
48542
+ return parseAndAssign(`matcher ${id}`, any);
48543
+ case "matrix":
48544
+ // TODO(LEMS-2585): implement a real parser for this widget
48545
+ return parseAndAssign(`matrix ${id}`, any);
48546
+ case "measurer":
48547
+ // TODO(LEMS-2585): implement a real parser for this widget
48548
+ return parseAndAssign(`measurer ${id}`, any);
48549
+ case "molecule-renderer":
48550
+ // TODO(LEMS-2585): implement a real parser for this widget
48551
+ return parseAndAssign(`molecule-renderer ${id}`, any);
48552
+ case "number-line":
48553
+ // TODO(LEMS-2585): implement a real parser for this widget
48554
+ return parseAndAssign(`number-line ${id}`, any);
48555
+ case "numeric-input":
48556
+ // TODO(LEMS-2585): implement a real parser for this widget
48557
+ return parseAndAssign(`numeric-input ${id}`, any);
48558
+ case "orderer":
48559
+ // TODO(LEMS-2585): implement a real parser for this widget
48560
+ return parseAndAssign(`orderer ${id}`, any);
48561
+ case "passage":
48562
+ // TODO(LEMS-2585): implement a real parser for this widget
48563
+ return parseAndAssign(`passage ${id}`, any);
48564
+ case "passage-ref":
48565
+ // TODO(LEMS-2585): implement a real parser for this widget
48566
+ return parseAndAssign(`passage-ref ${id}`, any);
48567
+ case "passage-ref-target":
48568
+ // TODO(LEMS-2585): implement a real parser for this widget
48569
+ return parseAndAssign(`passage-ref-target ${id}`, any);
48570
+ case "phet-simulation":
48571
+ // TODO(LEMS-2585): implement a real parser for this widget
48572
+ return parseAndAssign(`phet-simulation ${id}`, any);
48573
+ case "plotter":
48574
+ // TODO(LEMS-2585): implement a real parser for this widget
48575
+ return parseAndAssign(`plotter ${id}`, any);
48576
+ case "python-program":
48577
+ // TODO(LEMS-2585): implement a real parser for this widget
48578
+ return parseAndAssign(`python-program ${id}`, any);
48579
+ case "radio":
48580
+ // TODO(LEMS-2585): implement a real parser for this widget
48581
+ return parseAndAssign(`radio ${id}`, any);
48582
+ case "sorter":
48583
+ // TODO(LEMS-2585): implement a real parser for this widget
48584
+ return parseAndAssign(`sorter ${id}`, any);
48585
+ case "table":
48586
+ // TODO(LEMS-2585): implement a real parser for this widget
48587
+ return parseAndAssign(`table ${id}`, any);
48588
+ case "video":
48589
+ // TODO(LEMS-2585): implement a real parser for this widget
48590
+ return parseAndAssign(`video ${id}`, any);
48591
+ default:
48592
+ return ctx.failure("a valid widget type", type);
48593
+ }
48594
+ };
48595
+ const parseStringToPositiveInt = (rawValue, ctx) => {
48596
+ if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
48597
+ return ctx.failure("numeric string", rawValue);
48598
+ }
48599
+ return ctx.success(+rawValue);
48600
+ };
48601
+ const parseWidgetMapKeyComponents = pair(string, parseStringToPositiveInt);
48602
+
48603
+ const parseHint = object({
48604
+ replace: optional(boolean),
48605
+ content: string,
48606
+ widgets: parseWidgetsMap,
48607
+ metadata: optional(array(string)),
48608
+ images: record(string, object({
48609
+ width: number,
48610
+ height: number
48611
+ }))
48612
+ });
48613
+
48614
+ const parsePerseusRenderer = object({
48615
+ content: string,
48616
+ widgets: parseWidgetsMap,
48617
+ metadata: optional(array(string)),
48618
+ images: record(string, object({
48619
+ width: number,
48620
+ height: number
48621
+ }))
48622
+ });
48623
+
48624
+ object({
48625
+ question: parsePerseusRenderer,
48626
+ hints: array(parseHint),
48627
+ answerArea: record(enumeration(...ItemExtras), boolean),
48628
+ itemDataVersion: object({
48629
+ major: number,
48630
+ minor: number
48631
+ }),
48632
+ // Deprecated field
48633
+ answer: any
48634
+ });
48635
+
47370
48636
  /**
47371
48637
  * Helper to parse PerseusItem JSON
47372
48638
  * Why not just use JSON.parse? We want: