@khanacademy/perseus 41.2.0 → 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 (153) 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 +1609 -335
  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 -3
  8. package/dist/index.js +1528 -253
  9. package/dist/index.js.map +1 -1
  10. package/dist/perseus-api.d.ts +3 -3
  11. package/dist/perseus-types.d.ts +7 -2
  12. package/dist/prompt-types.d.ts +33 -0
  13. package/dist/renderer.d.ts +8 -2
  14. package/dist/server-item-renderer.d.ts +6 -4
  15. package/dist/types.d.ts +4 -2
  16. package/dist/util/parse-perseus-json/error-tracking-parse-context.d.ts +9 -0
  17. package/dist/util/parse-perseus-json/general-purpose-parsers/any.d.ts +2 -0
  18. package/dist/util/parse-perseus-json/general-purpose-parsers/array.d.ts +2 -0
  19. package/dist/util/parse-perseus-json/general-purpose-parsers/array.typetest.d.ts +1 -0
  20. package/dist/util/parse-perseus-json/general-purpose-parsers/boolean.d.ts +2 -0
  21. package/dist/util/parse-perseus-json/general-purpose-parsers/compose-parsers.d.ts +2 -0
  22. package/dist/util/parse-perseus-json/general-purpose-parsers/constant.d.ts +4 -0
  23. package/dist/util/parse-perseus-json/general-purpose-parsers/defaulted.d.ts +2 -0
  24. package/dist/util/parse-perseus-json/general-purpose-parsers/enumeration.d.ts +2 -0
  25. package/dist/util/parse-perseus-json/general-purpose-parsers/enumeration.typetest.d.ts +1 -0
  26. package/dist/util/parse-perseus-json/general-purpose-parsers/index.d.ts +15 -0
  27. package/dist/util/parse-perseus-json/general-purpose-parsers/is-object.d.ts +1 -0
  28. package/dist/util/parse-perseus-json/general-purpose-parsers/nullable.d.ts +2 -0
  29. package/dist/util/parse-perseus-json/general-purpose-parsers/number.d.ts +2 -0
  30. package/dist/util/parse-perseus-json/general-purpose-parsers/object.d.ts +6 -0
  31. package/dist/util/parse-perseus-json/general-purpose-parsers/optional.d.ts +2 -0
  32. package/dist/util/parse-perseus-json/general-purpose-parsers/pair.d.ts +2 -0
  33. package/dist/util/parse-perseus-json/general-purpose-parsers/record.d.ts +2 -0
  34. package/dist/util/parse-perseus-json/general-purpose-parsers/string.d.ts +2 -0
  35. package/dist/util/parse-perseus-json/general-purpose-parsers/test-helpers.d.ts +8 -0
  36. package/dist/util/parse-perseus-json/general-purpose-parsers/trio.d.ts +2 -0
  37. package/dist/util/parse-perseus-json/general-purpose-parsers/union.d.ts +7 -0
  38. package/dist/util/parse-perseus-json/general-purpose-parsers/union.typetest.d.ts +1 -0
  39. package/dist/util/{parse-perseus-json.d.ts → parse-perseus-json/index.d.ts} +7 -1
  40. package/dist/util/parse-perseus-json/object-path.d.ts +2 -0
  41. package/dist/util/parse-perseus-json/parse-failure-detail.d.ts +2 -0
  42. package/dist/util/parse-perseus-json/parse.d.ts +3 -0
  43. package/dist/util/parse-perseus-json/parser-types.d.ts +30 -0
  44. package/dist/util/parse-perseus-json/perseus-parsers/categorizer-widget.d.ts +3 -0
  45. package/dist/util/parse-perseus-json/perseus-parsers/cs-program-widget.d.ts +3 -0
  46. package/dist/util/parse-perseus-json/perseus-parsers/definition-widget.d.ts +3 -0
  47. package/dist/util/parse-perseus-json/perseus-parsers/dropdown-widget.d.ts +3 -0
  48. package/dist/util/parse-perseus-json/perseus-parsers/explanation-widget.d.ts +3 -0
  49. package/dist/util/parse-perseus-json/perseus-parsers/expression-widget.d.ts +3 -0
  50. package/dist/util/parse-perseus-json/perseus-parsers/hint.d.ts +3 -0
  51. package/dist/util/parse-perseus-json/perseus-parsers/interactive-graph-widget.d.ts +3 -0
  52. package/dist/util/parse-perseus-json/perseus-parsers/perseus-image-background.d.ts +3 -0
  53. package/dist/util/parse-perseus-json/perseus-parsers/perseus-item.d.ts +3 -0
  54. package/dist/util/parse-perseus-json/perseus-parsers/perseus-renderer.d.ts +3 -0
  55. package/dist/util/parse-perseus-json/perseus-parsers/widget.d.ts +3 -0
  56. package/dist/util/parse-perseus-json/perseus-parsers/widgets-map.d.ts +3 -0
  57. package/dist/util/parse-perseus-json/result.d.ts +16 -0
  58. package/dist/validation.types.d.ts +0 -3
  59. package/dist/widget-ai-utils/categorizer/prompt-utils.d.ts +14 -0
  60. package/dist/widget-ai-utils/cs-program/prompt-utils.d.ts +2 -0
  61. package/dist/widget-ai-utils/definition/prompt-utils.d.ts +8 -0
  62. package/dist/widget-ai-utils/dropdown/prompt-utils.d.ts +13 -0
  63. package/dist/widget-ai-utils/explanation/prompt-utils.d.ts +8 -0
  64. package/dist/widget-ai-utils/expression/prompt-utils.d.ts +11 -0
  65. package/dist/widget-ai-utils/graded-group/prompt-utils.d.ts +7 -0
  66. package/dist/widget-ai-utils/graded-group-set/prompt-utils.d.ts +11 -0
  67. package/dist/widget-ai-utils/grapher/prompt-utils.d.ts +18 -0
  68. package/dist/widget-ai-utils/group/prompt-utils.d.ts +5 -0
  69. package/dist/widget-ai-utils/iframe/prompt-utils.d.ts +2 -0
  70. package/dist/widget-ai-utils/image/prompt-utils.d.ts +12 -0
  71. package/dist/widget-ai-utils/input-number/prompt-utils.d.ts +14 -0
  72. package/dist/widget-ai-utils/interaction/prompt-utils.d.ts +2 -0
  73. package/dist/widget-ai-utils/interactive-graph/prompt-utils.d.ts +94 -0
  74. package/dist/widget-ai-utils/label-image/prompt-utils.d.ts +24 -0
  75. package/dist/widget-ai-utils/matcher/prompt-utils.d.ts +17 -0
  76. package/dist/widget-ai-utils/matrix/prompt-utils.d.ts +14 -0
  77. package/dist/widget-ai-utils/measurer/prompt-utils.d.ts +2 -0
  78. package/dist/widget-ai-utils/number-line/prompt-utils.d.ts +16 -0
  79. package/dist/widget-ai-utils/numeric-input/prompt-utils.d.ts +11 -0
  80. package/dist/widget-ai-utils/orderer/prompt-utils.d.ts +13 -0
  81. package/dist/widget-ai-utils/passage/prompt-utils.d.ts +11 -0
  82. package/dist/widget-ai-utils/passage-ref/prompt-utils.d.ts +11 -0
  83. package/dist/widget-ai-utils/phet-simulation/prompt-utils.d.ts +2 -0
  84. package/dist/widget-ai-utils/plotter/prompt-utils.d.ts +2 -0
  85. package/dist/widget-ai-utils/python-program/prompt-utils.d.ts +2 -0
  86. package/dist/widget-ai-utils/radio/prompt-utils.d.ts +15 -0
  87. package/dist/widget-ai-utils/sorter/prompt-utils.d.ts +9 -0
  88. package/dist/widget-ai-utils/unsupported-widget.d.ts +11 -0
  89. package/dist/widget-ai-utils/video/prompt-utils.d.ts +2 -0
  90. package/dist/widgets/__shared__/{noop-validator.d.ts → score-noop.d.ts} +4 -4
  91. package/dist/widgets/categorizer/categorizer.d.ts +10 -19
  92. package/dist/widgets/categorizer/{categorizer-validator.d.ts → score-categorizer.d.ts} +2 -2
  93. package/dist/widgets/cs-program/cs-program.d.ts +8 -19
  94. package/dist/widgets/cs-program/{cs-program-validator.d.ts → score-cs-program.d.ts} +2 -1
  95. package/dist/widgets/definition/definition.d.ts +8 -19
  96. package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +4 -19
  97. package/dist/widgets/dropdown/dropdown.d.ts +10 -19
  98. package/dist/widgets/dropdown/score-dropdown.d.ts +4 -0
  99. package/dist/widgets/explanation/explanation.d.ts +9 -19
  100. package/dist/widgets/expression/expression.d.ts +26 -290
  101. package/dist/widgets/expression/{expression-validator.d.ts → score-expression.d.ts} +2 -2
  102. package/dist/widgets/graded-group/graded-group.d.ts +13 -21
  103. package/dist/widgets/graded-group-set/graded-group-set.d.ts +9 -20
  104. package/dist/widgets/grapher/grapher.d.ts +9 -19
  105. package/dist/widgets/grapher/score-grapher.d.ts +4 -0
  106. package/dist/widgets/group/group.d.ts +7 -19
  107. package/dist/widgets/iframe/iframe.d.ts +7 -19
  108. package/dist/widgets/iframe/{iframe-validator.d.ts → score-iframe.d.ts} +1 -1
  109. package/dist/widgets/image/image.d.ts +9 -19
  110. package/dist/widgets/input-number/input-number.d.ts +11 -19
  111. package/dist/widgets/input-number/{input-number-validator.d.ts → score-input-number.d.ts} +2 -2
  112. package/dist/widgets/interaction/interaction.d.ts +7 -19
  113. package/dist/widgets/interactive-graph.d.ts +8 -19
  114. package/dist/widgets/interactive-graphs/score-interactive-graph.d.ts +4 -0
  115. package/dist/widgets/interactive-graphs/utils.d.ts +1 -1
  116. package/dist/widgets/label-image/label-image.d.ts +10 -86
  117. package/dist/widgets/label-image/{label-image-validator.d.ts → score-label-image.d.ts} +2 -2
  118. package/dist/widgets/matcher/matcher.d.ts +7 -19
  119. package/dist/widgets/matcher/score-matcher.d.ts +4 -0
  120. package/dist/widgets/matrix/matrix.d.ts +10 -19
  121. package/dist/widgets/matrix/{matrix-validator.d.ts → score-matrix.d.ts} +2 -2
  122. package/dist/widgets/measurer/measurer.d.ts +13 -19
  123. package/dist/widgets/molecule/molecule.d.ts +4 -19
  124. package/dist/widgets/number-line/number-line.d.ts +8 -19
  125. package/dist/widgets/number-line/score-number-line.d.ts +4 -0
  126. package/dist/widgets/numeric-input/numeric-input.d.ts +22 -19
  127. package/dist/widgets/numeric-input/{numeric-input-validator.d.ts → score-numeric-input.d.ts} +2 -2
  128. package/dist/widgets/orderer/orderer.d.ts +8 -19
  129. package/dist/widgets/orderer/{orderer-validator.d.ts → score-orderer.d.ts} +1 -1
  130. package/dist/widgets/passage/passage.d.ts +8 -19
  131. package/dist/widgets/passage-ref/passage-ref.d.ts +12 -19
  132. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +11 -19
  133. package/dist/widgets/phet-simulation/phet-simulation.d.ts +5 -19
  134. package/dist/widgets/plotter/plotter.d.ts +8 -19
  135. package/dist/widgets/plotter/score-plotter.d.ts +4 -0
  136. package/dist/widgets/python-program/python-program.d.ts +4 -19
  137. package/dist/widgets/radio/radio-component.d.ts +3 -1
  138. package/dist/widgets/radio/radio.d.ts +18 -19
  139. package/dist/widgets/radio/{radio-validator.d.ts → score-radio.d.ts} +2 -2
  140. package/dist/widgets/sorter/score-sorter.d.ts +4 -0
  141. package/dist/widgets/sorter/sorter.d.ts +7 -19
  142. package/dist/widgets/table/{table-validator.d.ts → score-table.d.ts} +2 -2
  143. package/dist/widgets/table/table.d.ts +8 -19
  144. package/dist/widgets/video/video.d.ts +7 -19
  145. package/dist/widgets.d.ts +2 -2
  146. package/package.json +4 -4
  147. package/dist/widgets/dropdown/dropdown-validator.d.ts +0 -4
  148. package/dist/widgets/grapher/grapher-validator.d.ts +0 -4
  149. package/dist/widgets/interactive-graphs/interactive-graph-validator.d.ts +0 -4
  150. package/dist/widgets/matcher/matcher-validator.d.ts +0 -4
  151. package/dist/widgets/number-line/number-line-validator.d.ts +0 -4
  152. package/dist/widgets/plotter/plotter-validator.d.ts +0 -4
  153. 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)
26926
27050
  };
26927
27051
 
26928
- function dropdownValidator(userInput, rubric) {
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
+ };
27062
+ };
27063
+
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",
@@ -31644,17 +32042,58 @@ function isUnlimitedGraphState(state) {
31644
32042
  * and the text outside of the $ blocks will be non-TeX text.
31645
32043
  */
31646
32044
  function replaceOutsideTeX(mathString) {
31647
- let currentlyTeX = mathString[0] === "$";
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);
31648
32057
  let result = "";
31649
- const splitString = mathString.split("$").filter(part => part !== "");
31650
- for (let i = 0; i < splitString.length; i++) {
31651
- const part = splitString[i];
31652
- result += currentlyTeX ? part : `\\text{${part}}`;
31653
- currentlyTeX = !currentlyTeX;
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
+ });
31654
32087
  }
31655
32088
  return result;
31656
32089
  }
31657
32090
 
32091
+ // Helper function for replaceOutsideTeX()
32092
+ function escapeSpecialChars(str) {
32093
+ // Escape $, \, {, and } characters
32094
+ return str.replace(/([$\\{}])/g, "\\$1");
32095
+ }
32096
+
31658
32097
  function LockedLabel(props) {
31659
32098
  const {
31660
32099
  coord,
@@ -35915,7 +36354,7 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
35915
36354
 
35916
36355
  const eq$1 = Util.eq;
35917
36356
  const deepEq$2 = Util.deepEq;
35918
- function interactiveGraphValidator(userInput, rubric) {
36357
+ function scoreInteractiveGraph(userInput, rubric) {
35919
36358
  // None-type graphs are not graded
35920
36359
  if (userInput.type === "none" && rubric.correct.type === "none") {
35921
36360
  return {
@@ -36024,7 +36463,7 @@ function interactiveGraphValidator(userInput, rubric) {
36024
36463
  if (rubric.correct.match === "similar") {
36025
36464
  match = similar(guess, correct, Number.POSITIVE_INFINITY);
36026
36465
  } else if (rubric.correct.match === "congruent") {
36027
- match = similar(guess, correct, number.DEFAULT_TOLERANCE);
36466
+ match = similar(guess, correct, number$1.DEFAULT_TOLERANCE);
36028
36467
  } else if (rubric.correct.match === "approx") {
36029
36468
  match = similar(guess, correct, 0.1);
36030
36469
  } else {
@@ -36333,7 +36772,7 @@ class LegacyInteractiveGraph extends React.Component {
36333
36772
  };
36334
36773
  this.setTrashCanVisibility = opacity => {
36335
36774
  const graphie = this.graphie;
36336
- if (number.equal(opacity, 0)) {
36775
+ if (number$1.equal(opacity, 0)) {
36337
36776
  if (this.trashCan) {
36338
36777
  this.trashCan.remove();
36339
36778
  this.trashCan = null;
@@ -37427,6 +37866,9 @@ class InteractiveGraph extends React.Component {
37427
37866
  }
37428
37867
  throw new PerseusError("Cannot getUserInput from a graph that has never rendered", Errors.NotAllowed);
37429
37868
  }
37869
+ getPromptJSON() {
37870
+ return getPromptJSON$b(this.props, this.getUserInput());
37871
+ }
37430
37872
  render() {
37431
37873
  var _this$props$apiOption;
37432
37874
  // Mafs shim
@@ -37805,7 +38247,7 @@ var InteractiveGraph$1 = {
37805
38247
  displayName: "Interactive graph (Assessments only)",
37806
38248
  widget: InteractiveGraph,
37807
38249
  staticTransform: staticTransform$3,
37808
- validator: interactiveGraphValidator
38250
+ scorer: scoreInteractiveGraph
37809
38251
  };
37810
38252
 
37811
38253
  // TODO: can we use WB typography?
@@ -37817,6 +38259,32 @@ const bodyXsmallBold = {
37817
38259
  lineHeight: "22px"
37818
38260
  };
37819
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
+
37820
38288
  const AnswerChoices = props => {
37821
38289
  const {
37822
38290
  strings
@@ -37899,59 +38367,6 @@ const styles$d = StyleSheet.create({
37899
38367
  }
37900
38368
  });
37901
38369
 
37902
- // Question state for marker as result of user selected answers.
37903
-
37904
- function scoreMarker(marker) {
37905
- const score = {
37906
- hasAnswers: false,
37907
- isCorrect: false
37908
- };
37909
- if (marker.selected && marker.selected.length > 0) {
37910
- score.hasAnswers = true;
37911
- }
37912
- if (marker.answers.length > 0) {
37913
- if (marker.selected && marker.selected.length === marker.answers.length) {
37914
- // All correct answers are selected by the user.
37915
- score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
37916
- }
37917
- } else if (!marker.selected || marker.selected.length === 0) {
37918
- // Correct as no answers should be selected by the user.
37919
- score.isCorrect = true;
37920
- }
37921
- return score;
37922
- }
37923
-
37924
- // TODO(LEMS-2440): May need to pull answers out of PerseusLabelImageWidgetOptions[markers] for the rubric
37925
- function labelImageValidator(userInput, rubric) {
37926
- let numAnswered = 0;
37927
- let numCorrect = 0;
37928
- for (const marker of userInput.markers) {
37929
- const score = scoreMarker(marker);
37930
- if (score.hasAnswers) {
37931
- numAnswered++;
37932
- }
37933
- if (score.isCorrect) {
37934
- numCorrect++;
37935
- }
37936
- }
37937
-
37938
- // We expect all question markers to be answered before grading.
37939
- if (numAnswered !== userInput.markers.length) {
37940
- return {
37941
- type: "invalid",
37942
- message: null
37943
- };
37944
- }
37945
- return {
37946
- type: "points",
37947
- // Markers with no expected answers are graded as correct if user
37948
- // makes no answer selection.
37949
- earned: numCorrect === userInput.markers.length ? 1 : 0,
37950
- total: 1,
37951
- message: null
37952
- };
37953
- }
37954
-
37955
38370
  const BringToFront = {
37956
38371
  boxShadow: `0 8px 8px ${color.offBlack64}`,
37957
38372
  zIndex: 1000
@@ -38205,6 +38620,59 @@ const styles$b = StyleSheet.create({
38205
38620
  }
38206
38621
  });
38207
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
+
38208
38676
  /**
38209
38677
  * Represents a direction vector.
38210
38678
  * No diagonals allowed.
@@ -38413,6 +38881,9 @@ class LabelImage extends React.Component {
38413
38881
  markers
38414
38882
  };
38415
38883
  }
38884
+ getPromptJSON() {
38885
+ return getPromptJSON$a(this.props, this.getUserInput());
38886
+ }
38416
38887
 
38417
38888
  // TODO(LEMS-2544): Investigate impact on scoring; possibly pull out &/or remove rubric parameter.
38418
38889
  showRationalesForCurrentlySelectedChoices(rubric) {
@@ -38769,7 +39240,7 @@ var LabelImage$1 = {
38769
39240
  widget: LabelImageWithDependencies,
38770
39241
  accessible: true,
38771
39242
  isLintable: true,
38772
- validator: labelImageValidator
39243
+ scorer: scoreLabelImage
38773
39244
  };
38774
39245
 
38775
39246
  var ItemState = /*#__PURE__*/function (ItemState) {
@@ -39481,7 +39952,23 @@ const styles$9 = StyleSheet.create({
39481
39952
  }
39482
39953
  });
39483
39954
 
39484
- 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) {
39485
39972
  const correct = _.isEqual(state.left, rubric.left) && _.isEqual(state.right, rubric.right);
39486
39973
  return {
39487
39974
  type: "points",
@@ -39554,6 +40041,9 @@ class Matcher extends React.Component {
39554
40041
  this.refs.right.moveOptionToIndex(option, index);
39555
40042
  };
39556
40043
  }
40044
+ getPromptJSON() {
40045
+ return getPromptJSON$9(this.props, this.getUserInput());
40046
+ }
39557
40047
  render() {
39558
40048
  // To minimize layout shift, we display a spinner until our math
39559
40049
  // renderer is ready to render the math inside the matcher. To
@@ -39692,10 +40182,23 @@ var Matcher$1 = {
39692
40182
  displayName: "Matcher (two column)",
39693
40183
  widget: Matcher,
39694
40184
  isLintable: true,
39695
- validator: matcherValidator
40185
+ scorer: scoreMatcher
39696
40186
  };
39697
40187
 
39698
- function matrixValidator(userInput, rubric, strings) {
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
+ };
40199
+ };
40200
+
40201
+ function scoreMatrix(userInput, rubric, strings) {
39699
40202
  const solution = rubric.answers;
39700
40203
  const supplied = userInput.answers;
39701
40204
  const solutionSize = getMatrixSize(solution);
@@ -39965,6 +40468,9 @@ class Matrix extends React.Component {
39965
40468
  answers: this.props.answers
39966
40469
  };
39967
40470
  }
40471
+ getPromptJSON() {
40472
+ return getPromptJSON$8(this.props, this.getUserInput());
40473
+ }
39968
40474
  render() {
39969
40475
  // Set the input sizes through JS so we can control the size of the
39970
40476
  // brackets. (If we set them in CSS we won't know values until the
@@ -40158,7 +40664,11 @@ var Matrix$1 = {
40158
40664
  transform: propTransform$1,
40159
40665
  staticTransform: staticTransform$2,
40160
40666
  isLintable: true,
40161
- validator: matrixValidator
40667
+ scorer: scoreMatrix
40668
+ };
40669
+
40670
+ const getPromptJSON$7 = () => {
40671
+ return getUnsupportedPromptJSON("measurer");
40162
40672
  };
40163
40673
 
40164
40674
  const defaultImage = {
@@ -40225,6 +40735,9 @@ class Measurer extends React.Component {
40225
40735
  });
40226
40736
  }
40227
40737
  }
40738
+ getPromptJSON() {
40739
+ return getPromptJSON$7();
40740
+ }
40228
40741
  render() {
40229
40742
  const image = _.extend({}, defaultImage, this.props.image);
40230
40743
 
@@ -40284,8 +40797,8 @@ var Measurer$1 = {
40284
40797
  minor: 0
40285
40798
  },
40286
40799
  propUpgrades: propUpgrades,
40287
- // TODO: things that aren't interactive shouldn't need validators
40288
- validator: () => noopValidator(1)
40800
+ // TODO: things that aren't interactive shouldn't need scoring functions
40801
+ scorer: () => scoreNoop(1)
40289
40802
  };
40290
40803
 
40291
40804
  /**
@@ -41174,16 +41687,31 @@ var Molecule$1 = {
41174
41687
  displayName: "Molecule renderer",
41175
41688
  hidden: true,
41176
41689
  widget: MoleculeWidget,
41177
- validator: () => noopValidator()
41690
+ scorer: () => scoreNoop()
41178
41691
  };
41179
41692
 
41180
- 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) {
41181
41709
  const range = rubric.range;
41182
41710
  const divisionRange = state.divisionRange;
41183
41711
  const start = rubric.initialX != null ? rubric.initialX : range[0];
41184
41712
  const startRel = rubric.isInequality ? "ge" : "eq";
41185
41713
  const correctRel = rubric.correctRel || "eq";
41186
- const correctPos = number.equal(state.numLinePosition, rubric.correctX || 0);
41714
+ const correctPos = number$1.equal(state.numLinePosition, rubric.correctX || 0);
41187
41715
  const outsideAllowedRange = state.numDivisions > divisionRange[1] || state.numDivisions < divisionRange[0];
41188
41716
 
41189
41717
  // TODO: I don't think isTickCrtl is a thing anymore
@@ -41312,7 +41840,7 @@ const TickMarks = Graphie.createSimpleClass((graphie, props) => {
41312
41840
  const x = range[0] + i * tickStep;
41313
41841
  fractions.push(x);
41314
41842
  }
41315
- const getDenom = x => number.toFraction(x)[1];
41843
+ const getDenom = x => number$1.toFraction(x)[1];
41316
41844
  const denoms = _.map(fractions, getDenom);
41317
41845
  base = _.reduce(denoms, (x, y) => KhanMath.getLCM(x, y));
41318
41846
  } else {
@@ -41375,7 +41903,7 @@ class NumberLine extends React.Component {
41375
41903
  let initialX = this.props.numLinePosition;
41376
41904
  const divisionRange = this.props.divisionRange;
41377
41905
  initialX = initialX == null ? range[0] : initialX;
41378
- 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;
41379
41907
  };
41380
41908
  this.onNumDivisionsChange = (numDivisions, cb) => {
41381
41909
  const divRange = this.props.divisionRange.slice();
@@ -41478,7 +42006,7 @@ class NumberLine extends React.Component {
41478
42006
  const right = props.range[1];
41479
42007
  const snapX = props.tickStep / props.snapDivisions;
41480
42008
  let x = bound(numLinePosition, left, right);
41481
- x = left + number.roundTo(x - left, snapX);
42009
+ x = left + number$1.roundTo(x - left, snapX);
41482
42010
  assert$1(_.isFinite(x));
41483
42011
  return x;
41484
42012
  };
@@ -41645,6 +42173,9 @@ class NumberLine extends React.Component {
41645
42173
  divisionRange: this.props.divisionRange
41646
42174
  };
41647
42175
  }
42176
+ getPromptJSON() {
42177
+ return getPromptJSON$6(this.props, this.getUserInput());
42178
+ }
41648
42179
  render() {
41649
42180
  const {
41650
42181
  strings
@@ -41764,10 +42295,22 @@ var NumberLine$1 = {
41764
42295
  widget: NumberLine,
41765
42296
  transform: numberLineTransform,
41766
42297
  staticTransform: staticTransform$1,
41767
- validator: numberLineValidator
42298
+ scorer: scoreNumberLine
41768
42299
  };
41769
42300
 
41770
- function ordererValidator(userInput, rubric) {
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
+ };
42311
+ };
42312
+
42313
+ function scoreOrderer(userInput, rubric) {
41771
42314
  if (userInput.current.length === 0) {
41772
42315
  return {
41773
42316
  type: "invalid",
@@ -42211,6 +42754,9 @@ class Orderer extends React.Component {
42211
42754
  })
42212
42755
  };
42213
42756
  }
42757
+ getPromptJSON() {
42758
+ return getPromptJSON$5(this.props, this.getUserInput());
42759
+ }
42214
42760
  render() {
42215
42761
  // This is the card we are currently dragging
42216
42762
  const dragging = this.state.dragging && /*#__PURE__*/React.createElement(Card
@@ -42329,7 +42875,7 @@ var Orderer$1 = {
42329
42875
  hidden: true,
42330
42876
  widget: Orderer,
42331
42877
  isLintable: true,
42332
- validator: ordererValidator
42878
+ scorer: scoreOrderer
42333
42879
  };
42334
42880
 
42335
42881
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -42378,8 +42924,12 @@ var PassageRefTarget$1 = {
42378
42924
  minor: 0
42379
42925
  },
42380
42926
  isLintable: true,
42381
- // TODO: things that aren't interactive shouldn't need validators
42382
- 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");
42383
42933
  };
42384
42934
 
42385
42935
  /**
@@ -42437,6 +42987,9 @@ class PhetSimulation extends React.Component {
42437
42987
  await this.updateSimState(this.props.url);
42438
42988
  }
42439
42989
  }
42990
+ getPromptJSON() {
42991
+ return getPromptJSON$4();
42992
+ }
42440
42993
  async updateSimState(urlString) {
42441
42994
  const url = makeSafeUrl(urlString, this.locale);
42442
42995
  if (url === null) {
@@ -42585,10 +43138,14 @@ var PhetSimulation$1 = {
42585
43138
  isLintable: true
42586
43139
  };
42587
43140
 
43141
+ const getPromptJSON$3 = () => {
43142
+ return getUnsupportedPromptJSON("plotter");
43143
+ };
43144
+
42588
43145
  const {
42589
43146
  deepEq: deepEq$1
42590
43147
  } = Util;
42591
- function plotterValidator(userInput, rubric) {
43148
+ function scorePlotter(userInput, rubric) {
42592
43149
  if (deepEq$1(userInput, rubric.starting)) {
42593
43150
  return {
42594
43151
  type: "invalid",
@@ -43361,6 +43918,9 @@ class Plotter extends React.Component {
43361
43918
  getUserInput() {
43362
43919
  return this.state.values;
43363
43920
  }
43921
+ getPromptJSON() {
43922
+ return getPromptJSON$3();
43923
+ }
43364
43924
  render() {
43365
43925
  // TODO(kevinb) actually compute the size of the graphie correctly and
43366
43926
  // make it that size so we don't have to add extra padding. The value
@@ -43401,7 +43961,11 @@ var Plotter$1 = {
43401
43961
  hidden: true,
43402
43962
  widget: Plotter,
43403
43963
  staticTransform: staticTransform,
43404
- validator: plotterValidator
43964
+ scorer: scorePlotter
43965
+ };
43966
+
43967
+ const getPromptJSON$2 = () => {
43968
+ return getUnsupportedPromptJSON("python-program");
43405
43969
  };
43406
43970
 
43407
43971
  /**
@@ -43423,6 +43987,9 @@ class PythonProgram extends React.Component {
43423
43987
  getDOMNodeForPath(path) {
43424
43988
  return null;
43425
43989
  }
43990
+ getPromptJSON() {
43991
+ return getPromptJSON$2();
43992
+ }
43426
43993
  render() {
43427
43994
  const url = getUrlFromProgramID(this.props.programID);
43428
43995
  const iframeStyle = {
@@ -43460,7 +44027,17 @@ var PythonProgram$1 = {
43460
44027
  widget: PythonProgram
43461
44028
  };
43462
44029
 
43463
- 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) {
43464
44041
  // If the sorter widget hasn't been changed yet, we treat it as "empty" which
43465
44042
  // prevents the "Check" button from becoming active. We want the user
43466
44043
  // to make a change before trying to move forward. This makes an
@@ -43532,6 +44109,9 @@ class Sorter extends React.Component {
43532
44109
  changed: this.state.changed
43533
44110
  };
43534
44111
  }
44112
+ getPromptJSON() {
44113
+ return getPromptJSON$1(this.getUserInput());
44114
+ }
43535
44115
  render() {
43536
44116
  const options = shuffle(this.props.correct, this.props.problemNum, /* ensurePermuted */true);
43537
44117
  const {
@@ -43566,10 +44146,10 @@ var Sorter$1 = {
43566
44146
  displayName: "Sorter",
43567
44147
  widget: Sorter,
43568
44148
  isLintable: true,
43569
- validator: sorterValidator
44149
+ scorer: scoreSorter
43570
44150
  };
43571
44151
 
43572
- function tableValidator(state, rubric, strings) {
44152
+ function scoreTable(state, rubric, strings) {
43573
44153
  const filterNonEmpty = function filterNonEmpty(table) {
43574
44154
  return _.filter(table, function (row) {
43575
44155
  // Check if row has a cell that is nonempty
@@ -43846,7 +44426,11 @@ var Table$1 = {
43846
44426
  transform: propTransform,
43847
44427
  hidden: true,
43848
44428
  isLintable: true,
43849
- validator: tableValidator
44429
+ scorer: scoreTable
44430
+ };
44431
+
44432
+ const getPromptJSON = () => {
44433
+ return getUnsupportedPromptJSON("video");
43850
44434
  };
43851
44435
 
43852
44436
  /**
@@ -43942,6 +44526,9 @@ class Video extends React.Component {
43942
44526
  return change.apply(this, args);
43943
44527
  };
43944
44528
  }
44529
+ getPromptJSON() {
44530
+ return getPromptJSON();
44531
+ }
43945
44532
  render() {
43946
44533
  const {
43947
44534
  InitialRequestUrl
@@ -44002,8 +44589,8 @@ var Video$1 = {
44002
44589
  defaultAlignment: "block",
44003
44590
  supportedAlignments: ["block", "float-left", "float-right", "full-width"],
44004
44591
  widget: Video,
44005
- // TODO: things that aren't interactive shouldn't need validators
44006
- validator: () => noopValidator()
44592
+ // TODO: things that aren't interactive shouldn't need scoring functions
44593
+ scorer: () => scoreNoop()
44007
44594
  };
44008
44595
 
44009
44596
  /* eslint-disable import/no-named-as-default */
@@ -44022,7 +44609,7 @@ const init = function init() {
44022
44609
 
44023
44610
  // This file is processed by a Rollup plugin (replace) to inject the production
44024
44611
  const libName = "@khanacademy/perseus";
44025
- const libVersion = "41.2.0";
44612
+ const libVersion = "41.3.0";
44026
44613
  addLibraryVersionToPerseusDebug(libName, libVersion);
44027
44614
 
44028
44615
  const apiVersion = {
@@ -44759,6 +45346,9 @@ class ServerItemRenderer extends React.Component {
44759
45346
  getNumHints() {
44760
45347
  return this.props.item.hints.length;
44761
45348
  }
45349
+ getPromptJSON() {
45350
+ return this.questionRenderer.getPromptJSON();
45351
+ }
44762
45352
 
44763
45353
  /**
44764
45354
  * Grades the item.
@@ -45406,7 +45996,7 @@ function treeToItem(node) {
45406
45996
  };
45407
45997
  }
45408
45998
 
45409
- var isObject = function isObject(obj) {
45999
+ var isObject$1 = function isObject(obj) {
45410
46000
  return obj === Object(obj);
45411
46001
  };
45412
46002
  var merge = function merge() {
@@ -45422,7 +46012,7 @@ var merge = function merge() {
45422
46012
  return obj;
45423
46013
  };
45424
46014
  var clone = function clone(obj) {
45425
- if (!isObject(obj)) {
46015
+ if (!isObject$1(obj)) {
45426
46016
  return obj;
45427
46017
  }
45428
46018
  return Array.isArray(obj) ? obj.slice() : merge(obj);
@@ -45438,7 +46028,7 @@ var clone = function clone(obj) {
45438
46028
  var dispatch = function dispatch(x) {
45439
46029
  if (Array.isArray(x)) {
45440
46030
  return arr;
45441
- } else if (isObject(x)) {
46031
+ } else if (isObject$1(x)) {
45442
46032
  return obj;
45443
46033
  } else if (typeof x === "string") {
45444
46034
  return str;
@@ -47359,6 +47949,690 @@ function buildTestData(testObject) {
47359
47949
  return `{"data":{"assessmentItem":{"__typename":"AssessmentItemOrError","error":null,"item":{"__typename":"AssessmentItem","id":"x890b3c70f3e8f4a6","itemData":"${testObject}","problemType":"Type 1","sha":"c7284a3ad65214b4e62bccce236d92f7f5d35941"}}}}`;
47360
47950
  }
47361
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
+
47362
48636
  /**
47363
48637
  * Helper to parse PerseusItem JSON
47364
48638
  * Why not just use JSON.parse? We want: