@khanacademy/perseus 41.2.0 → 41.4.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 (156) 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 +1643 -354
  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 +1562 -272
  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/answer-types.d.ts +45 -2
  17. package/dist/util/parse-perseus-json/error-tracking-parse-context.d.ts +9 -0
  18. package/dist/util/parse-perseus-json/general-purpose-parsers/any.d.ts +2 -0
  19. package/dist/util/parse-perseus-json/general-purpose-parsers/array.d.ts +2 -0
  20. package/dist/util/parse-perseus-json/general-purpose-parsers/array.typetest.d.ts +1 -0
  21. package/dist/util/parse-perseus-json/general-purpose-parsers/boolean.d.ts +2 -0
  22. package/dist/util/parse-perseus-json/general-purpose-parsers/compose-parsers.d.ts +2 -0
  23. package/dist/util/parse-perseus-json/general-purpose-parsers/constant.d.ts +4 -0
  24. package/dist/util/parse-perseus-json/general-purpose-parsers/defaulted.d.ts +2 -0
  25. package/dist/util/parse-perseus-json/general-purpose-parsers/enumeration.d.ts +2 -0
  26. package/dist/util/parse-perseus-json/general-purpose-parsers/enumeration.typetest.d.ts +1 -0
  27. package/dist/util/parse-perseus-json/general-purpose-parsers/index.d.ts +15 -0
  28. package/dist/util/parse-perseus-json/general-purpose-parsers/is-object.d.ts +1 -0
  29. package/dist/util/parse-perseus-json/general-purpose-parsers/nullable.d.ts +2 -0
  30. package/dist/util/parse-perseus-json/general-purpose-parsers/number.d.ts +2 -0
  31. package/dist/util/parse-perseus-json/general-purpose-parsers/object.d.ts +6 -0
  32. package/dist/util/parse-perseus-json/general-purpose-parsers/optional.d.ts +2 -0
  33. package/dist/util/parse-perseus-json/general-purpose-parsers/pair.d.ts +2 -0
  34. package/dist/util/parse-perseus-json/general-purpose-parsers/record.d.ts +2 -0
  35. package/dist/util/parse-perseus-json/general-purpose-parsers/string.d.ts +2 -0
  36. package/dist/util/parse-perseus-json/general-purpose-parsers/test-helpers.d.ts +8 -0
  37. package/dist/util/parse-perseus-json/general-purpose-parsers/trio.d.ts +2 -0
  38. package/dist/util/parse-perseus-json/general-purpose-parsers/union.d.ts +7 -0
  39. package/dist/util/parse-perseus-json/general-purpose-parsers/union.typetest.d.ts +1 -0
  40. package/dist/util/{parse-perseus-json.d.ts → parse-perseus-json/index.d.ts} +7 -1
  41. package/dist/util/parse-perseus-json/object-path.d.ts +2 -0
  42. package/dist/util/parse-perseus-json/parse-failure-detail.d.ts +2 -0
  43. package/dist/util/parse-perseus-json/parse.d.ts +3 -0
  44. package/dist/util/parse-perseus-json/parser-types.d.ts +30 -0
  45. package/dist/util/parse-perseus-json/perseus-parsers/categorizer-widget.d.ts +3 -0
  46. package/dist/util/parse-perseus-json/perseus-parsers/cs-program-widget.d.ts +3 -0
  47. package/dist/util/parse-perseus-json/perseus-parsers/definition-widget.d.ts +3 -0
  48. package/dist/util/parse-perseus-json/perseus-parsers/dropdown-widget.d.ts +3 -0
  49. package/dist/util/parse-perseus-json/perseus-parsers/explanation-widget.d.ts +3 -0
  50. package/dist/util/parse-perseus-json/perseus-parsers/expression-widget.d.ts +3 -0
  51. package/dist/util/parse-perseus-json/perseus-parsers/hint.d.ts +3 -0
  52. package/dist/util/parse-perseus-json/perseus-parsers/interactive-graph-widget.d.ts +3 -0
  53. package/dist/util/parse-perseus-json/perseus-parsers/perseus-image-background.d.ts +3 -0
  54. package/dist/util/parse-perseus-json/perseus-parsers/perseus-item.d.ts +3 -0
  55. package/dist/util/parse-perseus-json/perseus-parsers/perseus-renderer.d.ts +3 -0
  56. package/dist/util/parse-perseus-json/perseus-parsers/widget.d.ts +3 -0
  57. package/dist/util/parse-perseus-json/perseus-parsers/widgets-map.d.ts +3 -0
  58. package/dist/util/parse-perseus-json/result.d.ts +16 -0
  59. package/dist/validation.types.d.ts +4 -5
  60. package/dist/widget-ai-utils/categorizer/prompt-utils.d.ts +14 -0
  61. package/dist/widget-ai-utils/cs-program/prompt-utils.d.ts +2 -0
  62. package/dist/widget-ai-utils/definition/prompt-utils.d.ts +8 -0
  63. package/dist/widget-ai-utils/dropdown/prompt-utils.d.ts +13 -0
  64. package/dist/widget-ai-utils/explanation/prompt-utils.d.ts +8 -0
  65. package/dist/widget-ai-utils/expression/prompt-utils.d.ts +11 -0
  66. package/dist/widget-ai-utils/graded-group/prompt-utils.d.ts +7 -0
  67. package/dist/widget-ai-utils/graded-group-set/prompt-utils.d.ts +11 -0
  68. package/dist/widget-ai-utils/grapher/prompt-utils.d.ts +18 -0
  69. package/dist/widget-ai-utils/group/prompt-utils.d.ts +5 -0
  70. package/dist/widget-ai-utils/iframe/prompt-utils.d.ts +2 -0
  71. package/dist/widget-ai-utils/image/prompt-utils.d.ts +12 -0
  72. package/dist/widget-ai-utils/input-number/prompt-utils.d.ts +14 -0
  73. package/dist/widget-ai-utils/interaction/prompt-utils.d.ts +2 -0
  74. package/dist/widget-ai-utils/interactive-graph/prompt-utils.d.ts +94 -0
  75. package/dist/widget-ai-utils/label-image/prompt-utils.d.ts +24 -0
  76. package/dist/widget-ai-utils/matcher/prompt-utils.d.ts +17 -0
  77. package/dist/widget-ai-utils/matrix/prompt-utils.d.ts +14 -0
  78. package/dist/widget-ai-utils/measurer/prompt-utils.d.ts +2 -0
  79. package/dist/widget-ai-utils/number-line/prompt-utils.d.ts +16 -0
  80. package/dist/widget-ai-utils/numeric-input/prompt-utils.d.ts +11 -0
  81. package/dist/widget-ai-utils/orderer/prompt-utils.d.ts +13 -0
  82. package/dist/widget-ai-utils/passage/prompt-utils.d.ts +11 -0
  83. package/dist/widget-ai-utils/passage-ref/prompt-utils.d.ts +11 -0
  84. package/dist/widget-ai-utils/phet-simulation/prompt-utils.d.ts +2 -0
  85. package/dist/widget-ai-utils/plotter/prompt-utils.d.ts +2 -0
  86. package/dist/widget-ai-utils/python-program/prompt-utils.d.ts +2 -0
  87. package/dist/widget-ai-utils/radio/prompt-utils.d.ts +15 -0
  88. package/dist/widget-ai-utils/sorter/prompt-utils.d.ts +9 -0
  89. package/dist/widget-ai-utils/unsupported-widget.d.ts +11 -0
  90. package/dist/widget-ai-utils/video/prompt-utils.d.ts +2 -0
  91. package/dist/widgets/__shared__/{noop-validator.d.ts → score-noop.d.ts} +4 -4
  92. package/dist/widgets/categorizer/categorizer.d.ts +10 -19
  93. package/dist/widgets/categorizer/{categorizer-validator.d.ts → score-categorizer.d.ts} +2 -2
  94. package/dist/widgets/cs-program/cs-program.d.ts +8 -19
  95. package/dist/widgets/cs-program/{cs-program-validator.d.ts → score-cs-program.d.ts} +2 -1
  96. package/dist/widgets/definition/definition.d.ts +8 -19
  97. package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +4 -19
  98. package/dist/widgets/dropdown/dropdown.d.ts +10 -19
  99. package/dist/widgets/dropdown/score-dropdown.d.ts +4 -0
  100. package/dist/widgets/explanation/explanation.d.ts +9 -19
  101. package/dist/widgets/expression/expression.d.ts +26 -290
  102. package/dist/widgets/expression/{expression-validator.d.ts → score-expression.d.ts} +2 -2
  103. package/dist/widgets/graded-group/graded-group.d.ts +13 -21
  104. package/dist/widgets/graded-group-set/graded-group-set.d.ts +9 -20
  105. package/dist/widgets/grapher/grapher.d.ts +9 -19
  106. package/dist/widgets/grapher/score-grapher.d.ts +4 -0
  107. package/dist/widgets/group/group.d.ts +7 -19
  108. package/dist/widgets/iframe/iframe.d.ts +7 -19
  109. package/dist/widgets/iframe/{iframe-validator.d.ts → score-iframe.d.ts} +1 -1
  110. package/dist/widgets/image/image.d.ts +9 -19
  111. package/dist/widgets/input-number/input-number.d.ts +11 -19
  112. package/dist/widgets/input-number/{input-number-validator.d.ts → score-input-number.d.ts} +2 -2
  113. package/dist/widgets/interaction/interaction.d.ts +7 -19
  114. package/dist/widgets/interactive-graph.d.ts +8 -19
  115. package/dist/widgets/interactive-graphs/score-interactive-graph.d.ts +4 -0
  116. package/dist/widgets/interactive-graphs/utils.d.ts +1 -1
  117. package/dist/widgets/label-image/label-image.d.ts +10 -86
  118. package/dist/widgets/label-image/{label-image-validator.d.ts → score-label-image.d.ts} +2 -2
  119. package/dist/widgets/matcher/matcher.d.ts +7 -19
  120. package/dist/widgets/matcher/score-matcher.d.ts +4 -0
  121. package/dist/widgets/matrix/matrix.d.ts +10 -19
  122. package/dist/widgets/matrix/{matrix-validator.d.ts → score-matrix.d.ts} +2 -2
  123. package/dist/widgets/measurer/measurer.d.ts +13 -19
  124. package/dist/widgets/molecule/molecule.d.ts +4 -19
  125. package/dist/widgets/number-line/number-line.d.ts +8 -19
  126. package/dist/widgets/number-line/score-number-line.d.ts +4 -0
  127. package/dist/widgets/numeric-input/numeric-input.d.ts +22 -19
  128. package/dist/widgets/numeric-input/{numeric-input-validator.d.ts → score-numeric-input.d.ts} +2 -2
  129. package/dist/widgets/orderer/orderer.d.ts +8 -19
  130. package/dist/widgets/orderer/{orderer-validator.d.ts → score-orderer.d.ts} +1 -1
  131. package/dist/widgets/passage/passage.d.ts +8 -19
  132. package/dist/widgets/passage-ref/passage-ref.d.ts +12 -19
  133. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +11 -19
  134. package/dist/widgets/phet-simulation/phet-simulation.d.ts +5 -19
  135. package/dist/widgets/plotter/plotter.d.ts +8 -19
  136. package/dist/widgets/plotter/score-plotter.d.ts +4 -0
  137. package/dist/widgets/python-program/python-program.d.ts +4 -19
  138. package/dist/widgets/radio/radio-component.d.ts +3 -1
  139. package/dist/widgets/radio/radio.d.ts +18 -19
  140. package/dist/widgets/radio/{radio-validator.d.ts → score-radio.d.ts} +2 -2
  141. package/dist/widgets/sorter/score-sorter.d.ts +4 -0
  142. package/dist/widgets/sorter/sorter.d.ts +7 -19
  143. package/dist/widgets/table/{table-validator.d.ts → score-table.d.ts} +2 -2
  144. package/dist/widgets/table/table.d.ts +8 -19
  145. package/dist/widgets/table/utils.d.ts +7 -0
  146. package/dist/widgets/table/validate-table.d.ts +6 -0
  147. package/dist/widgets/video/video.d.ts +7 -19
  148. package/dist/widgets.d.ts +2 -2
  149. package/package.json +5 -5
  150. package/dist/widgets/dropdown/dropdown-validator.d.ts +0 -4
  151. package/dist/widgets/grapher/grapher-validator.d.ts +0 -4
  152. package/dist/widgets/interactive-graphs/interactive-graph-validator.d.ts +0 -4
  153. package/dist/widgets/matcher/matcher-validator.d.ts +0 -4
  154. package/dist/widgets/number-line/number-line-validator.d.ts +0 -4
  155. package/dist/widgets/plotter/plotter-validator.d.ts +0 -4
  156. 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) {
@@ -1427,7 +1464,7 @@ const MAXERROR_EPSILON = Math.pow(2, -42);
1427
1464
  // TOOD(kevinb): Figure out how this relates to KEScore in
1428
1465
  // perseus-all-package/types.js and see if there's a way to
1429
1466
  // unify these types.
1430
- /*
1467
+ /**
1431
1468
  * Answer types
1432
1469
  *
1433
1470
  * Utility for creating answerable questions displayed in exercises
@@ -1470,7 +1507,6 @@ const MAXERROR_EPSILON = Math.pow(2, -42);
1470
1507
  * TODO(alpert): Think of a less-absurd name for createValidatorFunctional.
1471
1508
  *
1472
1509
  */
1473
-
1474
1510
  const KhanAnswerTypes = {
1475
1511
  /*
1476
1512
  * predicate answer type
@@ -1941,9 +1977,7 @@ const KhanAnswerTypes = {
1941
1977
  })];
1942
1978
  },
1943
1979
  createValidatorFunctional: function (correctAnswer, options, strings) {
1944
- return KhanAnswerTypes.predicate.createValidatorFunctional(
1945
- // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
1946
- ...KhanAnswerTypes.number.convertToPredicate(correctAnswer, options), strings);
1980
+ return KhanAnswerTypes.predicate.createValidatorFunctional(...KhanAnswerTypes.number.convertToPredicate(correctAnswer, options), strings);
1947
1981
  }
1948
1982
  },
1949
1983
  /*
@@ -2099,32 +2133,6 @@ const KhanAnswerTypes = {
2099
2133
  }
2100
2134
  };
2101
2135
 
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
2136
  /* Content creators input a list of answers which are matched from top to
2129
2137
  * bottom. The intent is that they can include spcific solutions which should
2130
2138
  * be graded as correct or incorrect (or ungraded!) first, then get more
@@ -2143,7 +2151,7 @@ const getDecimalSeparator = locale => {
2143
2151
  * show the user an error. TODO(joel) - what error?
2144
2152
  * - Otherwise, pass through the resulting points and message.
2145
2153
  */
2146
- function expressionValidator(userInput, rubric, strings, locale) {
2154
+ function scoreExpression(userInput, rubric, strings, locale) {
2147
2155
  const options = _.clone(rubric);
2148
2156
  _.extend(options, {
2149
2157
  decimal_separator: getDecimalSeparator(locale)
@@ -2393,6 +2401,9 @@ class Expression extends React.Component {
2393
2401
  getUserInput() {
2394
2402
  return Expression.getUserInputFromProps(this.props);
2395
2403
  }
2404
+ getPromptJSON() {
2405
+ return getPromptJSON$t(this.props, this.getUserInput());
2406
+ }
2396
2407
  focusInputPath(inputPath) {
2397
2408
  // eslint-disable-next-line react/no-string-refs
2398
2409
  // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
@@ -2643,7 +2654,7 @@ var Expression$1 = {
2643
2654
  propUpgrades: propUpgrades$2,
2644
2655
  // For use by the editor
2645
2656
  isLintable: true,
2646
- validator: expressionValidator,
2657
+ scorer: scoreExpression,
2647
2658
  getOneCorrectAnswerFromRubric(rubric) {
2648
2659
  const correctAnswers = (rubric.answerForms || []).filter(answerForm => answerForm.considered === "correct");
2649
2660
  if (correctAnswers.length === 0) {
@@ -9330,7 +9341,7 @@ class Graphie$1 {
9330
9341
  // We round the coordinates to make testing easier, because trig
9331
9342
  // operations return unround numbers.
9332
9343
  // TODO(benchristel): move rounding to scalePoint?
9333
- const round = x => number.round(x, 6);
9344
+ const round = x => number$1.round(x, 6);
9334
9345
  const startPoint = this.scalePoint([round(center[0] + startVector[0]), round(center[1] + startVector[1])]);
9335
9346
  const endPoint = this.scalePoint([round(center[0] + endVector[0]), round(center[1] + endVector[1])]);
9336
9347
  const largeAngle = ((endAngle - startAngle) % 360 + 360) % 360 > 180;
@@ -10887,7 +10898,7 @@ function similar(coords1, coords2, tolerance) {
10887
10898
  return eq$3(factors[0], factor);
10888
10899
  });
10889
10900
  const congruentEnough = _.all(sidePairs, function (pair) {
10890
- return number.equal(pair[0], pair[1], tolerance);
10901
+ return number$1.equal(pair[0], pair[1], tolerance);
10891
10902
  });
10892
10903
  if (same && congruentEnough) {
10893
10904
  return true;
@@ -17914,7 +17925,7 @@ class InteractionTracker {
17914
17925
  */
17915
17926
  const arrayRules = {
17916
17927
  fence: {
17917
- match: SimpleMarkdown.defaultRules.fence.match,
17928
+ match: SimpleMarkdown$1.defaultRules.fence.match,
17918
17929
  order: 1,
17919
17930
  parse: (capture, state, parse) => ({
17920
17931
  type: "codeBlock",
@@ -17923,14 +17934,14 @@ const arrayRules = {
17923
17934
  })
17924
17935
  },
17925
17936
  paragraph: {
17926
- match: SimpleMarkdown.defaultRules.paragraph.match,
17937
+ match: SimpleMarkdown$1.defaultRules.paragraph.match,
17927
17938
  order: 2,
17928
17939
  parse: (capture, state, parse) => ({
17929
17940
  content: capture[1]
17930
17941
  })
17931
17942
  }
17932
17943
  };
17933
- const builtArrayParser = SimpleMarkdown.parserFor(arrayRules);
17944
+ const builtArrayParser = SimpleMarkdown$1.parserFor(arrayRules);
17934
17945
 
17935
17946
  // This should just return an array of strings! magick!
17936
17947
  const parseToArray = source => {
@@ -18355,7 +18366,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18355
18366
  // iOS 14.4, in order for VoiceOver to work.
18356
18367
  table: _extends({}, pureMarkdownRules.table, {
18357
18368
  react: function (node, output, state) {
18358
- const table = SimpleMarkdown.defaultRules.table.react(node, output, state);
18369
+ const table = SimpleMarkdown$1.defaultRules.table.react(node, output, state);
18359
18370
  if (!state.isMobile) {
18360
18371
  return table;
18361
18372
  }
@@ -18404,7 +18415,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18404
18415
  contents = rules$1.lint.react(node.table, output, state);
18405
18416
  }
18406
18417
  } else {
18407
- const tableOutput = SimpleMarkdown.defaultRules.table.react(node.table, output, state);
18418
+ const tableOutput = SimpleMarkdown$1.defaultRules.table.react(node.table, output, state);
18408
18419
  const caption = /*#__PURE__*/React.createElement("caption", {
18409
18420
  key: "caption",
18410
18421
  className: "perseus-table-title"
@@ -18474,7 +18485,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18474
18485
  // window and handle reverse tabnapping phishing attacks
18475
18486
  link: _extends({}, pureMarkdownRules.link, {
18476
18487
  react: function (node, output, state) {
18477
- const link = SimpleMarkdown.defaultRules.link.react(node, output, state);
18488
+ const link = SimpleMarkdown$1.defaultRules.link.react(node, output, state);
18478
18489
  const href = link.props.href;
18479
18490
  let rel = null;
18480
18491
  // The href prop should always be here, but just in case somehow the article's
@@ -18507,7 +18518,7 @@ const rules$1 = _extends({}, pureMarkdownRules, {
18507
18518
  className: "perseus-markdown-alt perseus-sr-only"
18508
18519
  }, output(node.content, state));
18509
18520
  }
18510
- return SimpleMarkdown.defaultRules.codeBlock.react(node, output, state);
18521
+ return SimpleMarkdown$1.defaultRules.codeBlock.react(node, output, state);
18511
18522
  }
18512
18523
  }),
18513
18524
  // The lint rule never actually matches anything.
@@ -18549,7 +18560,7 @@ const inlineNodeTypes = {
18549
18560
  };
18550
18561
 
18551
18562
  // @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);
18563
+ const builtParser$1 = SimpleMarkdown$1.parserFor(rules$1);
18553
18564
  const parse$3 = (source, state) => {
18554
18565
  const paragraphedSource = source + "\n\n";
18555
18566
  return builtParser$1(paragraphedSource, _extends({}, state, {
@@ -18642,10 +18653,10 @@ var PerseusMarkdown = {
18642
18653
  traverseContent: traverseContent,
18643
18654
  parse: parse$3,
18644
18655
  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
18656
+ reactFor: SimpleMarkdown$1.reactFor,
18657
+ ruleOutput: SimpleMarkdown$1.ruleOutput(rules$1, "react"),
18658
+ basicOutput: SimpleMarkdown$1.reactFor(SimpleMarkdown$1.ruleOutput(rules$1, "react")),
18659
+ sanitizeUrl: SimpleMarkdown$1.sanitizeUrl
18649
18660
  };
18650
18661
 
18651
18662
  class QuestionParagraph extends React.Component {
@@ -18766,9 +18777,9 @@ const getWidgetExport = name => {
18766
18777
  var _widgets$name3;
18767
18778
  return (_widgets$name3 = widgets[name]) != null ? _widgets$name3 : null;
18768
18779
  };
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;
18780
+ const getWidgetScorer = name => {
18781
+ var _widgets$name$scorer, _widgets$name4;
18782
+ return (_widgets$name$scorer = (_widgets$name4 = widgets[name]) == null ? void 0 : _widgets$name4.scorer) != null ? _widgets$name$scorer : null;
18772
18783
  };
18773
18784
  const getEditor = name => {
18774
18785
  return _.has(editors, name) ? editors[name] : null;
@@ -19073,7 +19084,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19073
19084
  replaceDeprecatedEditors: replaceDeprecatedEditors,
19074
19085
  getWidget: getWidget,
19075
19086
  getWidgetExport: getWidgetExport,
19076
- getWidgetValidator: getWidgetValidator,
19087
+ getWidgetScorer: getWidgetScorer,
19077
19088
  getEditor: getEditor,
19078
19089
  getTransform: getTransform,
19079
19090
  getVersion: getVersion,
@@ -19105,12 +19116,12 @@ widgetIds, userInputMap, strings, locale) {
19105
19116
  }
19106
19117
  let score = null;
19107
19118
  const userInput = userInputMap[id];
19108
- const validator = getWidgetValidator(widget.type);
19119
+ const scorer = getWidgetScorer(widget.type);
19109
19120
  if (widget.type === "group") {
19110
19121
  const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19111
19122
  score = Util.flattenScores(scores);
19112
- } else if (validator) {
19113
- score = validator(userInput, widget.options, strings, locale);
19123
+ } else if (scorer) {
19124
+ score = scorer(userInput, widget.options, strings, locale);
19114
19125
  }
19115
19126
  if (score) {
19116
19127
  return Util.scoreIsEmpty(score);
@@ -19135,12 +19146,12 @@ widgetIds, userInputMap, strings, locale) {
19135
19146
  return;
19136
19147
  }
19137
19148
  const userInput = userInputMap[id];
19138
- const validator = getWidgetValidator(widget.type);
19149
+ const scorer = getWidgetScorer(widget.type);
19139
19150
  if (widget.type === "group") {
19140
19151
  const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19141
19152
  widgetScores[id] = Util.flattenScores(scores);
19142
- } else if (validator) {
19143
- widgetScores[id] = validator(userInput, widget.options, strings, locale);
19153
+ } else if (scorer) {
19154
+ widgetScores[id] = scorer(userInput, widget.options, strings, locale);
19144
19155
  }
19145
19156
  });
19146
19157
  return widgetScores;
@@ -19599,42 +19610,6 @@ class Renderer extends React.Component {
19599
19610
  }
19600
19611
  return null;
19601
19612
  };
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
19613
  /**
19639
19614
  * Serializes the questions state so it can be recovered.
19640
19615
  *
@@ -20583,9 +20558,9 @@ class Renderer extends React.Component {
20583
20558
  // WidgetContainers don't update their widgets' props when
20584
20559
  // they are re-rendered, so even if they've been
20585
20560
  // re-rendered we need to call these methods on them.
20586
- _.each(this.widgetIds, id => {
20561
+ this.widgetIds.forEach(id => {
20587
20562
  const container = this._widgetContainers.get(makeContainerId(id));
20588
- container && container.replaceWidgetProps(this.getWidgetProps(id));
20563
+ container == null || container.replaceWidgetProps(this.getWidgetProps(id));
20589
20564
  });
20590
20565
  if (this.props.serializedState && !_.isEqual(this.props.serializedState, this.getSerializedState())) {
20591
20566
  this.restoreSerializedState(this.props.serializedState);
@@ -20614,6 +20589,42 @@ class Renderer extends React.Component {
20614
20589
  }
20615
20590
  this._isMounted = false;
20616
20591
  }
20592
+ getWidgetProps(widgetId) {
20593
+ const apiOptions = this.getApiOptions();
20594
+ const widgetProps = this.state.widgetProps[widgetId] || {};
20595
+
20596
+ // The widget needs access to its "rubric" at all times when in review
20597
+ // mode (which is really just part of its widget info).
20598
+ const widgetInfo = this.state.widgetInfo[widgetId];
20599
+ const reviewModeRubric = this.props.reviewMode && widgetInfo ? widgetInfo.options : null;
20600
+ if (!this._interactionTrackers) {
20601
+ this._interactionTrackers = {};
20602
+ }
20603
+ let interactionTracker = this._interactionTrackers[widgetId];
20604
+ if (!interactionTracker) {
20605
+ interactionTracker = this._interactionTrackers[widgetId] = new InteractionTracker(apiOptions.trackInteraction, widgetInfo && widgetInfo.type, widgetId, getTracking(widgetInfo && widgetInfo.type));
20606
+ }
20607
+ return _extends({}, widgetProps, {
20608
+ widgetId: widgetId,
20609
+ alignment: widgetInfo && widgetInfo.alignment,
20610
+ static: widgetInfo == null ? void 0 : widgetInfo.static,
20611
+ problemNum: this.props.problemNum,
20612
+ apiOptions: this.getApiOptions(),
20613
+ keypadElement: this.props.keypadElement,
20614
+ questionCompleted: this.props.questionCompleted,
20615
+ showSolutions: this.props.showSolutions,
20616
+ onFocus: _.partial(this._onWidgetFocus, widgetId),
20617
+ onBlur: _.partial(this._onWidgetBlur, widgetId),
20618
+ findWidgets: this.findWidgets,
20619
+ reviewModeRubric: reviewModeRubric,
20620
+ reviewMode: this.props.reviewMode,
20621
+ onChange: (newProps, cb, silent = false) => {
20622
+ this._setWidgetProps(widgetId, newProps, cb, silent);
20623
+ },
20624
+ trackInteraction: interactionTracker.track,
20625
+ isLastUsedWidget: widgetId === this.state.lastUsedWidgetId
20626
+ });
20627
+ }
20617
20628
  emptyWidgets() {
20618
20629
  return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20619
20630
  }
@@ -20650,6 +20661,25 @@ class Renderer extends React.Component {
20650
20661
  });
20651
20662
  return userInputMap;
20652
20663
  }
20664
+ /**
20665
+ * Returns a JSON representation of the content and widgets
20666
+ * that can be passed to an LLM for prompt context.
20667
+ */
20668
+ getPromptJSON() {
20669
+ const {
20670
+ content
20671
+ } = this.props;
20672
+ const widgetJSON = {};
20673
+ this.widgetIds.forEach(id => {
20674
+ const widget = this.getWidgetInstance(id);
20675
+ widgetJSON[id] = (widget == null || widget.getPromptJSON == null ? void 0 : widget.getPromptJSON()) || {};
20676
+ });
20677
+ return {
20678
+ content,
20679
+ widgets: widgetJSON
20680
+ };
20681
+ }
20682
+
20653
20683
  /**
20654
20684
  * Returns an object mapping from widget ID to perseus-style score.
20655
20685
  * The keys of this object are the values of the array returned
@@ -21389,6 +21419,19 @@ SimpleKeypadInput.propTypes = {
21389
21419
  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
21390
21420
  };
21391
21421
 
21422
+ const getPromptJSON$s = (renderProps, userInput) => {
21423
+ return {
21424
+ type: "input-number",
21425
+ options: {
21426
+ simplify: renderProps.simplify,
21427
+ answerType: renderProps.answerType
21428
+ },
21429
+ userInput: {
21430
+ value: userInput.currentValue
21431
+ }
21432
+ };
21433
+ };
21434
+
21392
21435
  /*
21393
21436
  * In this file, an `expression` is some portion of valid TeX enclosed in
21394
21437
  * curly brackets.
@@ -21579,7 +21622,7 @@ const answerTypes = {
21579
21622
  forms: "pi"
21580
21623
  }
21581
21624
  };
21582
- function inputNumberValidator(userInput, rubric, strings) {
21625
+ function scoreInputNumber(userInput, rubric, strings) {
21583
21626
  if (rubric.answerType == null) {
21584
21627
  rubric.answerType = "number";
21585
21628
  }
@@ -21713,6 +21756,9 @@ class InputNumber extends React.Component {
21713
21756
  getUserInput() {
21714
21757
  return InputNumber.getUserInputFromProps(this.props);
21715
21758
  }
21759
+ getPromptJSON() {
21760
+ return getPromptJSON$s(this.props, this.getUserInput());
21761
+ }
21716
21762
  render() {
21717
21763
  if (this.props.apiOptions.customKeypad) {
21718
21764
  // TODO(charlie): Support "Review Mode".
@@ -21814,7 +21860,7 @@ var InputNumber$1 = {
21814
21860
  widget: InputNumber,
21815
21861
  transform: propTransform$3,
21816
21862
  isLintable: true,
21817
- validator: inputNumberValidator,
21863
+ scorer: scoreInputNumber,
21818
21864
  getOneCorrectAnswerFromRubric(rubric) {
21819
21865
  if (rubric.value == null) {
21820
21866
  return;
@@ -21827,6 +21873,16 @@ var InputNumber$1 = {
21827
21873
  }
21828
21874
  };
21829
21875
 
21876
+ const getPromptJSON$r = (renderProps, userInput) => {
21877
+ return {
21878
+ type: "numeric-input",
21879
+ label: renderProps.labelText,
21880
+ userInput: {
21881
+ value: userInput.currentValue
21882
+ }
21883
+ };
21884
+ };
21885
+
21830
21886
  const ParseTex = TexWrangler.parseTex;
21831
21887
  const answerFormButtons = [{
21832
21888
  title: "Integers",
@@ -21885,7 +21941,7 @@ function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
21885
21941
  // Otherwise, we return input valuåe (number) stripped of the "%".
21886
21942
  return value;
21887
21943
  }
21888
- function numericInputValidator(userInput, rubric, strings) {
21944
+ function scoreNumericInput(userInput, rubric, strings) {
21889
21945
  var _matchedAnswer$messag;
21890
21946
  const defaultAnswerForms = answerFormButtons.map(e => e["value"])
21891
21947
  // Don't default to validating the answer as a pi answer
@@ -22056,6 +22112,9 @@ class NumericInput extends React.Component {
22056
22112
  getUserInput() {
22057
22113
  return NumericInput.getUserInputFromProps(this.props);
22058
22114
  }
22115
+ getPromptJSON() {
22116
+ return getPromptJSON$r(this.props, this.getUserInput());
22117
+ }
22059
22118
  render() {
22060
22119
  let labelText = this.props.labelText;
22061
22120
  if (labelText == null || labelText === "") {
@@ -22180,7 +22239,7 @@ var NumericInput$1 = {
22180
22239
  widget: NumericInput,
22181
22240
  transform: propsTransform,
22182
22241
  isLintable: true,
22183
- validator: numericInputValidator,
22242
+ scorer: scoreNumericInput,
22184
22243
  getOneCorrectAnswerFromRubric(rubric) {
22185
22244
  const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
22186
22245
  const answerStrings = correctAnswers.map(answer => {
@@ -22211,15 +22270,43 @@ var NumericInput$1 = {
22211
22270
  }
22212
22271
  };
22213
22272
 
22273
+ const getPromptJSON$q = (renderProps, userInput) => {
22274
+ const choices = renderProps.choices || [];
22275
+ const options = choices.map(choice => {
22276
+ return {
22277
+ value: choice.content
22278
+ };
22279
+ });
22280
+ return {
22281
+ type: "radio",
22282
+ hasNoneOfTheAbove: !!renderProps.hasNoneOfTheAbove,
22283
+ options,
22284
+ userInput: {
22285
+ selectedOptions: userInput.choicesSelected.slice()
22286
+ }
22287
+ };
22288
+ };
22289
+
22290
+ const getPromptJSON$p = renderProps => {
22291
+ return {
22292
+ type: "passage-ref",
22293
+ options: {
22294
+ passageNumber: renderProps.passageNumber,
22295
+ referenceNumber: renderProps.referenceNumber,
22296
+ summaryText: renderProps.summaryText
22297
+ }
22298
+ };
22299
+ };
22300
+
22214
22301
  /**
22215
- * Several widgets don't have "right"/"wrong" validation logic,
22302
+ * Several widgets don't have "right"/"wrong" scoring logic,
22216
22303
  * so this just says to move on past those widgets
22217
22304
  *
22218
22305
  * TODO(LEMS-2543) widgets that use this probably shouldn't have any
22219
- * validation logic and the thing validating an exercise
22306
+ * scoring logic and the thing scoring an exercise
22220
22307
  * should just know to skip these
22221
22308
  */
22222
- function noopValidator(points = 0) {
22309
+ function scoreNoop(points = 0) {
22223
22310
  return {
22224
22311
  type: "points",
22225
22312
  earned: points,
@@ -23603,6 +23690,17 @@ const styles$x = StyleSheet.create({
23603
23690
  }
23604
23691
  });
23605
23692
 
23693
+ const getPromptJSON$o = renderProps => {
23694
+ return {
23695
+ type: "passage",
23696
+ options: {
23697
+ passageTitle: renderProps.passageTitle,
23698
+ passageText: renderProps.passageText,
23699
+ footnotes: renderProps.footnotes
23700
+ }
23701
+ };
23702
+ };
23703
+
23606
23704
  function getInitialParseState() {
23607
23705
  return {
23608
23706
  currentRef: [],
@@ -23652,12 +23750,12 @@ const AltText = ({
23652
23750
  }));
23653
23751
  };
23654
23752
  const rules = {
23655
- newline: SimpleMarkdown.defaultRules.newline,
23656
- paragraph: SimpleMarkdown.defaultRules.paragraph,
23657
- escape: SimpleMarkdown.defaultRules.escape,
23753
+ newline: SimpleMarkdown$1.defaultRules.newline,
23754
+ paragraph: SimpleMarkdown$1.defaultRules.paragraph,
23755
+ escape: SimpleMarkdown$1.defaultRules.escape,
23658
23756
  passageFootnote: {
23659
- order: SimpleMarkdown.defaultRules.escape.order + 0.1,
23660
- match: SimpleMarkdown.inlineRegex(/^\^/),
23757
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.1,
23758
+ match: SimpleMarkdown$1.inlineRegex(/^\^/),
23661
23759
  parse: (capture, parse, state) => {
23662
23760
  // if no footnotes have been seen, we're id 1. otherwise,
23663
23761
  // we're the next subsequent id
@@ -23686,7 +23784,7 @@ const rules = {
23686
23784
  }
23687
23785
  },
23688
23786
  refStart: {
23689
- order: SimpleMarkdown.defaultRules.escape.order + 0.2,
23787
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.2,
23690
23788
  match: function (source) {
23691
23789
  const capture = /^\{\{/.exec(source);
23692
23790
  if (capture) {
@@ -23772,8 +23870,8 @@ const rules = {
23772
23870
  }
23773
23871
  },
23774
23872
  refEnd: {
23775
- order: SimpleMarkdown.defaultRules.escape.order + 0.3,
23776
- match: SimpleMarkdown.inlineRegex(/^\}\}/),
23873
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.3,
23874
+ match: SimpleMarkdown$1.inlineRegex(/^\}\}/),
23777
23875
  parse: (capture, parse, state) => {
23778
23876
  if (!state.useRefs) {
23779
23877
  return {
@@ -23801,8 +23899,8 @@ const rules = {
23801
23899
  }
23802
23900
  },
23803
23901
  squareLabel: {
23804
- order: SimpleMarkdown.defaultRules.escape.order + 0.4,
23805
- match: SimpleMarkdown.inlineRegex(/^\[\[(\w+)\]\]( *)/),
23902
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.4,
23903
+ match: SimpleMarkdown$1.inlineRegex(/^\[\[(\w+)\]\]( *)/),
23806
23904
  parse: (capture, parse, state) => {
23807
23905
  if (!state.firstQuestionRef) {
23808
23906
  state.firstQuestionRef = capture[1];
@@ -23828,8 +23926,8 @@ const rules = {
23828
23926
  }
23829
23927
  },
23830
23928
  circleLabel: {
23831
- order: SimpleMarkdown.defaultRules.escape.order + 0.5,
23832
- match: SimpleMarkdown.inlineRegex(/^\(\((\w+)\)\)( *)/),
23929
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.5,
23930
+ match: SimpleMarkdown$1.inlineRegex(/^\(\((\w+)\)\)( *)/),
23833
23931
  parse: (capture, parse, state) => {
23834
23932
  return {
23835
23933
  content: capture[1],
@@ -23853,8 +23951,8 @@ const rules = {
23853
23951
  }
23854
23952
  },
23855
23953
  squareBracketRef: {
23856
- order: SimpleMarkdown.defaultRules.escape.order + 0.6,
23857
- match: SimpleMarkdown.inlineRegex(/^\[(\d+)\]( *)/),
23954
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.6,
23955
+ match: SimpleMarkdown$1.inlineRegex(/^\[(\d+)\]( *)/),
23858
23956
  parse: (capture, parse, state) => {
23859
23957
  if (!state.firstSentenceRef) {
23860
23958
  state.firstSentenceRef = capture[1];
@@ -23877,8 +23975,8 @@ const rules = {
23877
23975
  }
23878
23976
  },
23879
23977
  highlight: {
23880
- order: SimpleMarkdown.defaultRules.escape.order + 0.7,
23881
- match: SimpleMarkdown.inlineRegex(/^{highlighting.start}(.+?){highlighting.end}/),
23978
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.7,
23979
+ match: SimpleMarkdown$1.inlineRegex(/^{highlighting.start}(.+?){highlighting.end}/),
23882
23980
  parse: (capture, parse, state) => {
23883
23981
  return {
23884
23982
  content: capture[1]
@@ -23892,8 +23990,8 @@ const rules = {
23892
23990
  }
23893
23991
  },
23894
23992
  reviewHighlight: {
23895
- order: SimpleMarkdown.defaultRules.escape.order + 0.7,
23896
- match: SimpleMarkdown.inlineRegex(/^{review-highlighting.start}(.+?){review-highlighting.end}/),
23993
+ order: SimpleMarkdown$1.defaultRules.escape.order + 0.7,
23994
+ match: SimpleMarkdown$1.inlineRegex(/^{review-highlighting.start}(.+?){review-highlighting.end}/),
23897
23995
  parse: (capture, parse, state) => {
23898
23996
  return {
23899
23997
  content: capture[1]
@@ -23906,11 +24004,11 @@ const rules = {
23906
24004
  }, node.content)];
23907
24005
  }
23908
24006
  },
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, {
24007
+ strong: SimpleMarkdown$1.defaultRules.strong,
24008
+ u: SimpleMarkdown$1.defaultRules.u,
24009
+ em: SimpleMarkdown$1.defaultRules.em,
24010
+ del: SimpleMarkdown$1.defaultRules.del,
24011
+ text: _extends({}, SimpleMarkdown$1.defaultRules.text, {
23914
24012
  react(node, output, state) {
23915
24013
  return /*#__PURE__*/React.createElement("span", {
23916
24014
  key: state.key
@@ -23951,7 +24049,7 @@ const CIRCLE_LABEL_STYLE = {
23951
24049
  borderRadius: "50%",
23952
24050
  textAlign: "center"
23953
24051
  };
23954
- const builtParser = SimpleMarkdown.parserFor(rules);
24052
+ const builtParser = SimpleMarkdown$1.parserFor(rules);
23955
24053
  const parse$2 = (source, state) => {
23956
24054
  // @ts-expect-error - TS2322 - Type 'ParseState | {}' is not assignable to type 'ParseState | null | undefined'.
23957
24055
  state = state || {};
@@ -23960,7 +24058,7 @@ const parse$2 = (source, state) => {
23960
24058
  };
23961
24059
 
23962
24060
  // @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"));
24061
+ const output = SimpleMarkdown$1.reactFor(SimpleMarkdown$1.ruleOutput(rules, "react"));
23964
24062
  var PassageMarkdown = {
23965
24063
  parse: parse$2,
23966
24064
  output,
@@ -24233,6 +24331,9 @@ class Passage extends React.Component {
24233
24331
  content: refContent
24234
24332
  };
24235
24333
  }
24334
+ getPromptJSON() {
24335
+ return getPromptJSON$o(this.props);
24336
+ }
24236
24337
 
24237
24338
  /**
24238
24339
  * Rendering
@@ -24387,7 +24488,7 @@ var Passage$1 = {
24387
24488
  return _.pick(editorProps, "passageTitle", "passageText", "footnotes", "showLineNumbers");
24388
24489
  },
24389
24490
  isLintable: true,
24390
- validator: () => noopValidator()
24491
+ scorer: () => scoreNoop()
24391
24492
  };
24392
24493
 
24393
24494
  /**
@@ -24462,6 +24563,9 @@ class PassageRef extends React.Component {
24462
24563
  window.removeEventListener("resize", this._throttledUpdateRange);
24463
24564
  this._isMounted = false;
24464
24565
  }
24566
+ getPromptJSON() {
24567
+ return getPromptJSON$p(this.props);
24568
+ }
24465
24569
  render() {
24466
24570
  const {
24467
24571
  strings
@@ -24517,8 +24621,8 @@ var PassageRef$1 = {
24517
24621
  major: 0,
24518
24622
  minor: 1
24519
24623
  },
24520
- // TODO: things that aren't interactive shouldn't need validators
24521
- validator: () => noopValidator()
24624
+ // TODO: things that aren't interactive shouldn't need scoring functions
24625
+ scorer: () => scoreNoop()
24522
24626
  };
24523
24627
 
24524
24628
  /**
@@ -25876,7 +25980,7 @@ const styles$p = StyleSheet.create({
25876
25980
  }
25877
25981
  });
25878
25982
 
25879
- function radioValidator(userInput, rubric, strings) {
25983
+ function scoreRadio(userInput, rubric, strings) {
25880
25984
  const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
25881
25985
  return sum + (selected ? 1 : 0);
25882
25986
  }, 0);
@@ -25886,10 +25990,10 @@ function radioValidator(userInput, rubric, strings) {
25886
25990
  message: null
25887
25991
  };
25888
25992
  }
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) {
25993
+ const numCorrect = rubric.choices.reduce((sum, currentChoice) => {
25994
+ return currentChoice.correct ? sum + 1 : sum;
25995
+ }, 0);
25996
+ if (numCorrect > 1 && numSelected !== numCorrect) {
25893
25997
  return {
25894
25998
  type: "invalid",
25895
25999
  message: strings.chooseCorrectNum
@@ -25897,9 +26001,8 @@ function radioValidator(userInput, rubric, strings) {
25897
26001
  // If NOTA and some other answer are checked, ...
25898
26002
  }
25899
26003
 
25900
- // TODO(LEMS-2541): should noneOfTheAboveSelected be replaced with a
25901
- // combination of choicesSelected and noneOfTheAboveIndex?
25902
- if (userInput.noneOfTheAboveSelected && numSelected > 1) {
26004
+ const noneOfTheAboveSelected = rubric.choices.some((choice, index) => choice.isNoneOfTheAbove && userInput.choicesSelected[index]);
26005
+ if (noneOfTheAboveSelected && numSelected > 1) {
25903
26006
  return {
25904
26007
  type: "invalid",
25905
26008
  message: strings.notNoneOfTheAbove
@@ -25907,9 +26010,7 @@ function radioValidator(userInput, rubric, strings) {
25907
26010
  }
25908
26011
  const correct = userInput.choicesSelected.every((selected, i) => {
25909
26012
  let isCorrect;
25910
- // TODO(LEMS-2541): should noneOfTheAboveIndex actually be on the rubric
25911
- // instead of the userInput?
25912
- if (userInput.noneOfTheAboveIndex === i) {
26013
+ if (rubric.choices[i].isNoneOfTheAbove) {
25913
26014
  isCorrect = rubric.choices.every((choice, j) => {
25914
26015
  return i === j || !choice.correct;
25915
26016
  });
@@ -26031,7 +26132,7 @@ class Radio$1 extends React.Component {
26031
26132
  choiceStates
26032
26133
  } = this.props;
26033
26134
  if (choiceStates) {
26034
- const score = radioValidator(this.getUserInput(), rubric, this.context.strings);
26135
+ const score = scoreRadio(this.getUserInput(), rubric, this.context.strings);
26035
26136
  const widgetCorrect = score.type === "points" && score.total === score.earned;
26036
26137
  const newStates = choiceStates.map(state => _extends({}, state, {
26037
26138
  highlighted: state.selected,
@@ -26079,32 +26180,19 @@ class Radio$1 extends React.Component {
26079
26180
  }
26080
26181
  };
26081
26182
  }
26082
- static getUserInputFromProps(props) {
26183
+ static getUserInputFromProps(props, unshuffle = true) {
26083
26184
  // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
26084
26185
  // future timeline implementers: this used to be {value: i} before
26085
26186
  // multiple select was added)
26086
26187
  if (props.choiceStates) {
26087
- let noneOfTheAboveIndex = null;
26088
- let noneOfTheAboveSelected = false;
26089
26188
  const choiceStates = props.choiceStates;
26090
26189
  const choicesSelected = choiceStates.map(() => false);
26091
- const numCorrect = props.numCorrect;
26092
26190
  for (let i = 0; i < choicesSelected.length; i++) {
26093
- const index = props.choices[i].originalIndex;
26191
+ const index = unshuffle ? props.choices[i].originalIndex : i;
26094
26192
  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
26193
  }
26103
26194
  return {
26104
- choicesSelected,
26105
- numCorrect,
26106
- noneOfTheAboveIndex,
26107
- noneOfTheAboveSelected
26195
+ choicesSelected
26108
26196
  };
26109
26197
  // Support legacy choiceState implementation
26110
26198
  }
@@ -26113,27 +26201,14 @@ class Radio$1 extends React.Component {
26113
26201
  values
26114
26202
  } = props;
26115
26203
  if (values) {
26116
- let noneOfTheAboveIndex = null;
26117
- let noneOfTheAboveSelected = false;
26118
26204
  const choicesSelected = [...values];
26119
- const numCorrect = props.numCorrect;
26120
26205
  const valuesLength = values.length;
26121
26206
  for (let i = 0; i < valuesLength; i++) {
26122
- const index = props.choices[i].originalIndex;
26207
+ const index = unshuffle ? props.choices[i].originalIndex : i;
26123
26208
  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
26209
  }
26132
26210
  return {
26133
- choicesSelected,
26134
- noneOfTheAboveIndex,
26135
- noneOfTheAboveSelected,
26136
- numCorrect
26211
+ choicesSelected
26137
26212
  };
26138
26213
  }
26139
26214
  // Nothing checked
@@ -26165,6 +26240,10 @@ class Radio$1 extends React.Component {
26165
26240
  getUserInput() {
26166
26241
  return Radio$1.getUserInputFromProps(this.props);
26167
26242
  }
26243
+ getPromptJSON() {
26244
+ const userInput = Radio$1.getUserInputFromProps(this.props, false);
26245
+ return getPromptJSON$q(this.props, userInput);
26246
+ }
26168
26247
  render() {
26169
26248
  const {
26170
26249
  choices
@@ -26402,13 +26481,26 @@ var Radio = {
26402
26481
  },
26403
26482
  propUpgrades: propUpgrades$1,
26404
26483
  isLintable: true,
26405
- validator: radioValidator
26484
+ scorer: scoreRadio
26406
26485
  };
26407
26486
 
26408
26487
  /* eslint-disable import/no-named-as-default */
26409
26488
  var basicWidgets = [Radio, InputNumber$1, NumericInput$1, Expression$1];
26410
26489
 
26411
- function categorizerValidator(userInput, rubric, strings) {
26490
+ const getPromptJSON$n = (renderProps, userInput) => {
26491
+ return {
26492
+ type: "categorizer",
26493
+ options: {
26494
+ items: renderProps.items,
26495
+ categories: renderProps.categories
26496
+ },
26497
+ userInput: {
26498
+ itemToCategoryMapping: userInput.values
26499
+ }
26500
+ };
26501
+ };
26502
+
26503
+ function scoreCategorizer(userInput, rubric, strings) {
26412
26504
  let completed = true;
26413
26505
  let allCorrect = true;
26414
26506
  rubric.values.forEach((value, i) => {
@@ -26453,6 +26545,9 @@ class Categorizer extends React.Component {
26453
26545
  getUserInput() {
26454
26546
  return Categorizer.getUserInputFromProps(this.props);
26455
26547
  }
26548
+ getPromptJSON() {
26549
+ return getPromptJSON$n(this.props, this.getUserInput());
26550
+ }
26456
26551
  onChange(itemNum, catNum) {
26457
26552
  const values = [...this.props.values];
26458
26553
  // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'never'.
@@ -26606,7 +26701,7 @@ var Categorizer$1 = {
26606
26701
  return _.pick(editorProps, "items", "categories", "values", "randomizeItems");
26607
26702
  },
26608
26703
  isLintable: true,
26609
- validator: categorizerValidator
26704
+ scorer: scoreCategorizer
26610
26705
  };
26611
26706
 
26612
26707
  /**
@@ -26656,7 +26751,19 @@ const parseUrl = url => new URL(url, location().origin);
26656
26751
  */
26657
26752
  const toAbsoluteUrl = url => parseUrl(url).href;
26658
26753
 
26659
- function csProgramValidator(state) {
26754
+ const getUnsupportedPromptJSON = (widgetType, message = "") => {
26755
+ return {
26756
+ type: widgetType,
26757
+ isSupported: false,
26758
+ message
26759
+ };
26760
+ };
26761
+
26762
+ const getPromptJSON$m = () => {
26763
+ return getUnsupportedPromptJSON("cs-program");
26764
+ };
26765
+
26766
+ function scoreCSProgram(state) {
26660
26767
  // The iframe can tell us whether it's correct or incorrect,
26661
26768
  // and pass an optional message
26662
26769
  if (state.status === "correct") {
@@ -26742,6 +26849,9 @@ class CSProgram extends React.Component {
26742
26849
  message: this.props.message
26743
26850
  };
26744
26851
  }
26852
+ getPromptJSON() {
26853
+ return getPromptJSON$m();
26854
+ }
26745
26855
  render() {
26746
26856
  if (!this.props.programID) {
26747
26857
  return /*#__PURE__*/React.createElement("div", null);
@@ -26824,7 +26934,15 @@ var CSProgram$1 = {
26824
26934
  supportedAlignments: ["block", "full-width"],
26825
26935
  widget: CSProgram,
26826
26936
  hidden: true,
26827
- validator: csProgramValidator
26937
+ scorer: scoreCSProgram
26938
+ };
26939
+
26940
+ const getPromptJSON$l = renderProps => {
26941
+ return {
26942
+ type: "definition",
26943
+ definition: renderProps.definition,
26944
+ togglePrompt: renderProps.togglePrompt
26945
+ };
26828
26946
  };
26829
26947
 
26830
26948
  class Definition extends React.Component {
@@ -26834,6 +26952,9 @@ class Definition extends React.Component {
26834
26952
  // doesn't implement any Widget methods
26835
26953
  this.isWidget = true;
26836
26954
  }
26955
+ getPromptJSON() {
26956
+ return getPromptJSON$l(this.props);
26957
+ }
26837
26958
  render() {
26838
26959
  return /*#__PURE__*/React.createElement(DefinitionConsumer, null, ({
26839
26960
  activeDefinitionId,
@@ -26889,8 +27010,8 @@ var Definition$1 = {
26889
27010
  defaultAlignment: "inline",
26890
27011
  widget: Definition,
26891
27012
  transform: x => x,
26892
- // TODO: things that aren't interactive shouldn't need validators
26893
- validator: () => noopValidator()
27013
+ // TODO: things that aren't interactive shouldn't need scoring functions
27014
+ scorer: () => scoreNoop()
26894
27015
  };
26895
27016
 
26896
27017
  // The props are type `any` on purpose so that this can receive props
@@ -26921,11 +27042,23 @@ var DeprecatedStandin$1 = {
26921
27042
  displayName: "Deprecated Standin",
26922
27043
  widget: DeprecatedStandin,
26923
27044
  hidden: true,
26924
- // TODO: things that aren't interactive shouldn't need validators
26925
- validator: () => noopValidator(1)
27045
+ // TODO: things that aren't interactive shouldn't need scoring functions
27046
+ scorer: () => scoreNoop(1)
27047
+ };
27048
+
27049
+ const getPromptJSON$k = (renderProps, userInput) => {
27050
+ return {
27051
+ type: "dropdown",
27052
+ options: {
27053
+ items: renderProps.choices
27054
+ },
27055
+ userInput: {
27056
+ selectedIndex: userInput.value - 1 // Offset to account for placeholder
27057
+ }
27058
+ };
26926
27059
  };
26927
27060
 
26928
- function dropdownValidator(userInput, rubric) {
27061
+ function scoreDropdown(userInput, rubric) {
26929
27062
  const selected = userInput.value;
26930
27063
  if (selected === 0) {
26931
27064
  return {
@@ -26968,6 +27101,9 @@ class Dropdown extends React.Component {
26968
27101
  value: this.props.selected
26969
27102
  };
26970
27103
  }
27104
+ getPromptJSON() {
27105
+ return getPromptJSON$k(this.props, this.getUserInput());
27106
+ }
26971
27107
  render() {
26972
27108
  const children = [/*#__PURE__*/React.createElement(OptionItem, {
26973
27109
  key: "placeholder",
@@ -27015,7 +27151,15 @@ var Dropdown$1 = {
27015
27151
  accessible: true,
27016
27152
  widget: Dropdown,
27017
27153
  transform: optionsTransform,
27018
- validator: dropdownValidator
27154
+ scorer: scoreDropdown
27155
+ };
27156
+
27157
+ const getPromptJSON$j = renderProps => {
27158
+ return {
27159
+ type: "explanation",
27160
+ showPrompt: renderProps.showPrompt,
27161
+ explanation: renderProps.explanation
27162
+ };
27019
27163
  };
27020
27164
 
27021
27165
  // transform = _.identity
@@ -27052,6 +27196,9 @@ class Explanation extends React.Component {
27052
27196
  componentWillUnmount() {
27053
27197
  this._mounted = false;
27054
27198
  }
27199
+ getPromptJSON() {
27200
+ return getPromptJSON$j(this.props);
27201
+ }
27055
27202
  render() {
27056
27203
  const promptText = this.state.expanded ? this.props.hidePrompt : this.props.showPrompt;
27057
27204
  const caretIcon = this.state.expanded ? caretUp : caretDown;
@@ -27166,8 +27313,25 @@ var Explanation$1 = {
27166
27313
  widget: Explanation,
27167
27314
  transform: _.identity,
27168
27315
  isLintable: true,
27169
- // TODO: things that aren't interactive shouldn't need validators
27170
- validator: () => noopValidator()
27316
+ // TODO: things that aren't interactive shouldn't need scoring functions
27317
+ scorer: () => scoreNoop()
27318
+ };
27319
+
27320
+ const getPromptJSON$i = (title, rendererJSON, hintRendererJSON) => {
27321
+ if (!rendererJSON) {
27322
+ return {
27323
+ type: "graded-group",
27324
+ title,
27325
+ content: "",
27326
+ widgets: {},
27327
+ hint: hintRendererJSON
27328
+ };
27329
+ }
27330
+ return _extends({}, rendererJSON, {
27331
+ title,
27332
+ type: "graded-group",
27333
+ hint: hintRendererJSON
27334
+ });
27171
27335
  };
27172
27336
 
27173
27337
  class GradedGroupAnswerBar extends React.Component {
@@ -27302,6 +27466,8 @@ class GradedGroup extends React.Component {
27302
27466
  message: "",
27303
27467
  answerBarState: "HIDDEN"
27304
27468
  };
27469
+ this.rendererRef = /*#__PURE__*/React.createRef();
27470
+ this.hintRendererRef = /*#__PURE__*/React.createRef();
27305
27471
  this.change = (...args) => {
27306
27472
  // @ts-expect-error - TS2345 - Argument of type 'readonly unknown[]' is not assignable to parameter of type 'any[]'.
27307
27473
  return change.apply(this, args);
@@ -27317,13 +27483,9 @@ class GradedGroup extends React.Component {
27317
27483
  status: GRADING_STATUSES.ungraded,
27318
27484
  message: ""
27319
27485
  });
27320
-
27321
- // eslint-disable-next-line react/no-string-refs
27322
- if (this.refs.renderer) {
27486
+ if (this.rendererRef.current) {
27323
27487
  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();
27488
+ const emptyWidgets = this.rendererRef.current.emptyWidgets();
27327
27489
  const answerable = emptyWidgets.length === 0;
27328
27490
  const answerBarState = this.state.answerBarState;
27329
27491
  this.setState({
@@ -27332,12 +27494,11 @@ class GradedGroup extends React.Component {
27332
27494
  }
27333
27495
  };
27334
27496
  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();
27497
+ var _this$rendererRef$cur, _this$rendererRef$cur2;
27498
+ (_this$rendererRef$cur = this.rendererRef.current) == null || _this$rendererRef$cur.showRationalesForCurrentlySelectedChoices();
27499
+ const score = ((_this$rendererRef$cur2 = this.rendererRef.current) == null ? void 0 : _this$rendererRef$cur2.score()) || {
27500
+ type: "invalid"
27501
+ };
27341
27502
  const {
27342
27503
  INVALID_MESSAGE_PREFIX,
27343
27504
  DEFAULT_INVALID_MESSAGE_1,
@@ -27357,34 +27518,39 @@ class GradedGroup extends React.Component {
27357
27518
  };
27358
27519
  // Mobile API
27359
27520
  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();
27521
+ var _this$rendererRef$cur3;
27522
+ return ((_this$rendererRef$cur3 = this.rendererRef.current) == null ? void 0 : _this$rendererRef$cur3.getInputPaths()) || [];
27363
27523
  };
27364
27524
  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);
27525
+ var _this$rendererRef$cur4;
27526
+ return (_this$rendererRef$cur4 = this.rendererRef.current) == null ? void 0 : _this$rendererRef$cur4.setInputValue(path, newValue, cb);
27368
27527
  };
27369
27528
  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();
27529
+ var _this$rendererRef$cur5;
27530
+ return !!((_this$rendererRef$cur5 = this.rendererRef.current) != null && _this$rendererRef$cur5.focus());
27373
27531
  };
27374
27532
  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);
27533
+ var _this$rendererRef$cur6;
27534
+ (_this$rendererRef$cur6 = this.rendererRef.current) == null || _this$rendererRef$cur6.focusPath(path);
27378
27535
  };
27379
27536
  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);
27537
+ var _this$rendererRef$cur7;
27538
+ (_this$rendererRef$cur7 = this.rendererRef.current) == null || _this$rendererRef$cur7.blurPath(path);
27383
27539
  };
27384
27540
  }
27385
27541
  shouldComponentUpdate(nextProps, nextState) {
27386
27542
  return nextProps !== this.props || nextState !== this.state;
27387
27543
  }
27544
+ getPromptJSON() {
27545
+ var _this$hintRendererRef, _this$props$hint, _this$rendererRef$cur8;
27546
+ // If the hint isn't expanded, we can't get the prompt JSON from the rendered widgets.
27547
+ // We'll just pass in the hint content as a string instead.
27548
+ const hint = ((_this$hintRendererRef = this.hintRendererRef.current) == null ? void 0 : _this$hintRendererRef.getPromptJSON()) || {
27549
+ content: ((_this$props$hint = this.props.hint) == null ? void 0 : _this$props$hint.content) || "",
27550
+ widgets: {}
27551
+ };
27552
+ return getPromptJSON$i(this.props.title, (_this$rendererRef$cur8 = this.rendererRef.current) == null ? void 0 : _this$rendererRef$cur8.getPromptJSON(), hint);
27553
+ }
27388
27554
  render() {
27389
27555
  const apiOptions = _.extend({}, ApiOptions.defaults, this.props.apiOptions, {
27390
27556
  // Api Rewriting to support correct onFocus/onBlur
@@ -27441,8 +27607,7 @@ class GradedGroup extends React.Component {
27441
27607
  }, !!this.props.title && /*#__PURE__*/React.createElement("div", {
27442
27608
  className: css(styles$j.title)
27443
27609
  }, this.props.title), /*#__PURE__*/React.createElement(Renderer, _extends({}, this.props, {
27444
- // eslint-disable-next-line react/no-string-refs
27445
- ref: "renderer",
27610
+ ref: this.rendererRef,
27446
27611
  apiOptions: _extends({}, apiOptions, {
27447
27612
  readOnly
27448
27613
  }),
@@ -27484,8 +27649,7 @@ class GradedGroup extends React.Component {
27484
27649
  });
27485
27650
  }
27486
27651
  }, 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",
27652
+ ref: this.hintRendererRef,
27489
27653
  apiOptions: apiOptions,
27490
27654
  linterContext: this.props.linterContext,
27491
27655
  strings: this.context.strings
@@ -27587,6 +27751,16 @@ var GradedGroup$1 = {
27587
27751
  isLintable: true
27588
27752
  };
27589
27753
 
27754
+ const getPromptJSON$h = (renderProps, activeGroupJSON) => {
27755
+ return {
27756
+ type: "graded-group-set",
27757
+ options: {
27758
+ groupCount: renderProps.gradedGroups.length,
27759
+ currentGroup: activeGroupJSON
27760
+ }
27761
+ };
27762
+ };
27763
+
27590
27764
  class Indicators extends React.Component {
27591
27765
  constructor(...args) {
27592
27766
  super(...args);
@@ -27674,6 +27848,10 @@ class GradedGroupSet extends React.Component {
27674
27848
  nextProps.gradedGroups;
27675
27849
  return nextProps !== this.props || nextState !== this.state;
27676
27850
  }
27851
+ getPromptJSON() {
27852
+ const activeGroupPromptJSON = this._childGroup.getPromptJSON();
27853
+ return getPromptJSON$h(this.props, activeGroupPromptJSON);
27854
+ }
27677
27855
  render() {
27678
27856
  // When used in the context of TranslationEditor, render the
27679
27857
  // GradedGroup widget one below another instead of using an indicator
@@ -27911,6 +28089,33 @@ const styles$h = StyleSheet.create({
27911
28089
  }
27912
28090
  });
27913
28091
 
28092
+ const getPromptJSON$g = (renderProps, userInput) => {
28093
+ const {
28094
+ type,
28095
+ coords
28096
+ } = userInput;
28097
+ const input = {
28098
+ type,
28099
+ coords
28100
+ };
28101
+ if (userInput.type === "logarithm" || userInput.type === "exponential") {
28102
+ input["asymptote"] = userInput.asymptote;
28103
+ }
28104
+ return {
28105
+ type: "grapher",
28106
+ options: {
28107
+ availableTypes: renderProps.availableTypes,
28108
+ range: renderProps.graph.range,
28109
+ labels: renderProps.graph.labels,
28110
+ tickStep: renderProps.graph.step,
28111
+ gridStep: renderProps.graph.gridStep,
28112
+ snapStep: renderProps.graph.snapStep,
28113
+ backgroundImageUrl: renderProps.graph.backgroundImage.url
28114
+ },
28115
+ userInput: input
28116
+ };
28117
+ };
28118
+
27914
28119
  /* eslint-disable one-var */
27915
28120
  // @ts-expect-error - TS2339 - Property 'Plot' does not exist on type 'typeof Graphie'.
27916
28121
  const Plot$1 = Graphie.Plot;
@@ -28434,7 +28639,7 @@ function getCoefficientsByType(data) {
28434
28639
  throw new PerseusError("Invalid grapher type", Errors.InvalidInput);
28435
28640
  }
28436
28641
  }
28437
- function grapherValidator(userInput, rubric) {
28642
+ function scoreGrapher(userInput, rubric) {
28438
28643
  if (userInput.type !== rubric.correct.type) {
28439
28644
  return {
28440
28645
  type: "points",
@@ -28583,7 +28788,7 @@ class FunctionGrapher extends React.Component {
28583
28788
  constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
28584
28789
  // Always enforce that this is a function
28585
28790
  const isFunction = _.all(this._coords(), (otherCoord, j) => {
28586
- return i === j || !otherCoord || !number.equal(coord[0], otherCoord[0]);
28791
+ return i === j || !otherCoord || !number$1.equal(coord[0], otherCoord[0]);
28587
28792
  });
28588
28793
 
28589
28794
  // Evaluate this criteria before per-point
@@ -28783,6 +28988,9 @@ class Grapher extends React.Component {
28783
28988
  getUserInput() {
28784
28989
  return Grapher.getUserInputFromProps(this.props);
28785
28990
  }
28991
+ getPromptJSON() {
28992
+ return getPromptJSON$g(this.props, this.getUserInput());
28993
+ }
28786
28994
  render() {
28787
28995
  const type = this.props.plot.type;
28788
28996
  const coords = this.props.plot.coords;
@@ -28868,7 +29076,20 @@ var Grapher$1 = {
28868
29076
  widget: Grapher,
28869
29077
  transform: propTransform$2,
28870
29078
  staticTransform: staticTransform$4,
28871
- validator: grapherValidator
29079
+ scorer: scoreGrapher
29080
+ };
29081
+
29082
+ const getPromptJSON$f = rendererJSON => {
29083
+ if (!rendererJSON) {
29084
+ return {
29085
+ type: "group",
29086
+ content: "",
29087
+ widgets: {}
29088
+ };
29089
+ }
29090
+ return _extends({}, rendererJSON, {
29091
+ type: "group"
29092
+ });
28872
29093
  };
28873
29094
 
28874
29095
  // exports has no 'transform'
@@ -28926,17 +29147,21 @@ class Group extends React.Component {
28926
29147
  var _this$rendererRef8;
28927
29148
  return (_this$rendererRef8 = this.rendererRef) == null ? void 0 : _this$rendererRef8.getUserInput();
28928
29149
  }
29150
+ getPromptJSON() {
29151
+ var _this$rendererRef9;
29152
+ return getPromptJSON$f((_this$rendererRef9 = this.rendererRef) == null ? void 0 : _this$rendererRef9.getPromptJSON());
29153
+ }
28929
29154
  // Mobile API:
28930
29155
  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 : [];
29156
+ var _this$rendererRef$get, _this$rendererRef10;
29157
+ return (_this$rendererRef$get = (_this$rendererRef10 = this.rendererRef) == null ? void 0 : _this$rendererRef10.getInputPaths()) != null ? _this$rendererRef$get : [];
28933
29158
  }
28934
29159
  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;
29160
+ var _this$rendererRef$foc, _this$rendererRef11;
29161
+ return (_this$rendererRef$foc = (_this$rendererRef11 = this.rendererRef) == null ? void 0 : _this$rendererRef11.focus()) != null ? _this$rendererRef$foc : false;
28937
29162
  }
28938
29163
  render() {
28939
- var _this$rendererRef11;
29164
+ var _this$rendererRef12;
28940
29165
  const apiOptions = _extends({}, ApiOptions.defaults, this.props.apiOptions, {
28941
29166
  // Api Rewriting to support correct onFocus/onBlur
28942
29167
  // events for the mobile API
@@ -28972,7 +29197,7 @@ class Group extends React.Component {
28972
29197
  this.change("widgets", this.rendererRef.props.widgets);
28973
29198
  }
28974
29199
  };
28975
- const score = (_this$rendererRef11 = this.rendererRef) == null ? void 0 : _this$rendererRef11.score();
29200
+ const score = (_this$rendererRef12 = this.rendererRef) == null ? void 0 : _this$rendererRef12.score();
28976
29201
  const isValid = score && score.type !== "invalid";
28977
29202
  const isInvalid = score && score.type === "invalid";
28978
29203
 
@@ -29022,7 +29247,11 @@ var Group$1 = {
29022
29247
  isLintable: true
29023
29248
  };
29024
29249
 
29025
- function iframeValidator(userInput) {
29250
+ const getPromptJSON$e = () => {
29251
+ return getUnsupportedPromptJSON("iframe");
29252
+ };
29253
+
29254
+ function scoreIframe(userInput) {
29026
29255
  // The iframe can tell us whether it's correct or incorrect,
29027
29256
  // and pass an optional message
29028
29257
  if (userInput.status === "correct") {
@@ -29098,6 +29327,9 @@ class Iframe extends React.Component {
29098
29327
  message: this.props.message
29099
29328
  };
29100
29329
  }
29330
+ getPromptJSON() {
29331
+ return getPromptJSON$e();
29332
+ }
29101
29333
  render() {
29102
29334
  const style = {
29103
29335
  width: String(this.props.width),
@@ -29172,7 +29404,19 @@ var Iframe$1 = {
29172
29404
  widget: Iframe,
29173
29405
  // Let's not expose it to all content creators yet
29174
29406
  hidden: true,
29175
- validator: iframeValidator
29407
+ scorer: scoreIframe
29408
+ };
29409
+
29410
+ const getPromptJSON$d = renderProps => {
29411
+ return {
29412
+ type: "image",
29413
+ options: {
29414
+ altText: renderProps.alt,
29415
+ title: renderProps.title,
29416
+ caption: renderProps.caption,
29417
+ imageUrl: renderProps.backgroundImage.url
29418
+ }
29419
+ };
29176
29420
  };
29177
29421
 
29178
29422
  const defaultBoxSize = 400;
@@ -29197,6 +29441,9 @@ class ImageWidget extends React.Component {
29197
29441
  return change.apply(this, args);
29198
29442
  };
29199
29443
  }
29444
+ getPromptJSON() {
29445
+ return getPromptJSON$d(this.props);
29446
+ }
29200
29447
  render() {
29201
29448
  let image;
29202
29449
  const alt = this.props.caption === this.props.alt ? "" : this.props.alt;
@@ -29352,8 +29599,12 @@ var Image$1 = {
29352
29599
  displayName: "Image",
29353
29600
  widget: ImageWidget,
29354
29601
  isLintable: true,
29355
- // TODO: things that aren't interactive shouldn't need validators
29356
- validator: () => noopValidator()
29602
+ // TODO: things that aren't interactive shouldn't need scoring functions
29603
+ scorer: () => scoreNoop()
29604
+ };
29605
+
29606
+ const getPromptJSON$c = () => {
29607
+ return getUnsupportedPromptJSON("interaction");
29357
29608
  };
29358
29609
 
29359
29610
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -29526,6 +29777,9 @@ class Interaction extends React.Component {
29526
29777
  });
29527
29778
  }
29528
29779
  }
29780
+ getPromptJSON() {
29781
+ return getPromptJSON$c();
29782
+ }
29529
29783
  render() {
29530
29784
  const range = this.props.graph.range;
29531
29785
  let labels = this.props.graph.labels;
@@ -29945,8 +30199,8 @@ var Interactive = {
29945
30199
  widget: Interaction,
29946
30200
  transform: _.identity,
29947
30201
  hidden: true,
29948
- // TODO: things that aren't interactive shouldn't need validators
29949
- validator: () => noopValidator()
30202
+ // TODO: things that aren't interactive shouldn't need scoring functions
30203
+ scorer: () => scoreNoop()
29950
30204
  };
29951
30205
 
29952
30206
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -30249,6 +30503,139 @@ Graph.defaultProps = {
30249
30503
  isMobile: false
30250
30504
  };
30251
30505
 
30506
+ const getPromptJSON$b = (props, userInput) => {
30507
+ var _props$backgroundImag;
30508
+ return {
30509
+ type: "interactive-graph",
30510
+ options: {
30511
+ graph: getGraphOptionsForProps(props),
30512
+ backgroundImageUrl: (_props$backgroundImag = props.backgroundImage) == null ? void 0 : _props$backgroundImag.url,
30513
+ range: props.range,
30514
+ labels: props.labels
30515
+ },
30516
+ userInput: getUserInput(userInput)
30517
+ };
30518
+ };
30519
+ const getGraphOptionsForProps = props => {
30520
+ var _props$graph$startCoo, _props$graph$startCoo2;
30521
+ const type = props.graph.type;
30522
+ switch (type) {
30523
+ case "angle":
30524
+ return {
30525
+ type: props.graph.type,
30526
+ angleOffsetDegrees: props.graph.angleOffsetDeg,
30527
+ startCoords: props.graph.startCoords
30528
+ };
30529
+ case "circle":
30530
+ return {
30531
+ type: props.graph.type,
30532
+ startParams: {
30533
+ center: (_props$graph$startCoo = props.graph.startCoords) == null ? void 0 : _props$graph$startCoo.center,
30534
+ radius: (_props$graph$startCoo2 = props.graph.startCoords) == null ? void 0 : _props$graph$startCoo2.radius
30535
+ }
30536
+ };
30537
+ case "linear":
30538
+ return {
30539
+ type: props.graph.type,
30540
+ startCoords: props.graph.startCoords
30541
+ };
30542
+ case "linear-system":
30543
+ return {
30544
+ type: props.graph.type,
30545
+ startCoords: props.graph.startCoords
30546
+ };
30547
+ case "point":
30548
+ return {
30549
+ type: props.graph.type,
30550
+ numPoints: props.graph.numPoints,
30551
+ startCoords: props.graph.startCoords
30552
+ };
30553
+ case "polygon":
30554
+ return {
30555
+ type: props.graph.type,
30556
+ match: props.graph.match,
30557
+ numSides: props.graph.numSides,
30558
+ startCoords: props.graph.startCoords
30559
+ };
30560
+ case "quadratic":
30561
+ return {
30562
+ type: props.graph.type,
30563
+ startCoords: props.graph.startCoords
30564
+ };
30565
+ case "ray":
30566
+ return {
30567
+ type: props.graph.type,
30568
+ startCoords: props.graph.startCoords
30569
+ };
30570
+ case "segment":
30571
+ return {
30572
+ type: props.graph.type,
30573
+ numSegments: props.graph.numSegments,
30574
+ startCoords: props.graph.startCoords
30575
+ };
30576
+ case "sinusoid":
30577
+ return {
30578
+ type: props.graph.type,
30579
+ startCoords: props.graph.startCoords
30580
+ };
30581
+ case "none":
30582
+ return {};
30583
+ default:
30584
+ throw new UnreachableCaseError(type);
30585
+ }
30586
+ };
30587
+ const getUserInput = userInput => {
30588
+ const type = userInput.type;
30589
+ switch (type) {
30590
+ case "angle":
30591
+ return {
30592
+ coords: userInput.coords,
30593
+ angleOffsetDegrees: userInput.angleOffsetDeg
30594
+ };
30595
+ case "circle":
30596
+ return {
30597
+ center: userInput.center,
30598
+ radius: userInput.radius
30599
+ };
30600
+ case "linear":
30601
+ return {
30602
+ coords: userInput.coords
30603
+ };
30604
+ case "linear-system":
30605
+ return {
30606
+ coords: userInput.coords
30607
+ };
30608
+ case "point":
30609
+ return {
30610
+ coords: userInput.coords
30611
+ };
30612
+ case "polygon":
30613
+ return {
30614
+ coords: userInput.coords
30615
+ };
30616
+ case "quadratic":
30617
+ return {
30618
+ coords: userInput.coords
30619
+ };
30620
+ case "ray":
30621
+ return {
30622
+ coords: userInput.coords
30623
+ };
30624
+ case "segment":
30625
+ return {
30626
+ coords: userInput.coords
30627
+ };
30628
+ case "sinusoid":
30629
+ return {
30630
+ coords: userInput.coords
30631
+ };
30632
+ case "none":
30633
+ return {};
30634
+ default:
30635
+ throw new UnreachableCaseError(type);
30636
+ }
30637
+ };
30638
+
30252
30639
  function pathBuilder() {
30253
30640
  return new PathBuilder();
30254
30641
  }
@@ -31313,7 +31700,7 @@ class NumberInput extends React.Component {
31313
31700
  };
31314
31701
  }
31315
31702
  componentDidUpdate(prevProps) {
31316
- if (!number.equal(this.getValue(), this.props.value)) {
31703
+ if (!number$1.equal(this.getValue(), this.props.value)) {
31317
31704
  this._setValue(this.props.value, this.state.format);
31318
31705
  }
31319
31706
  }
@@ -31482,6 +31869,10 @@ const LegacyGrid = ({
31482
31869
  * To render more complex Perseus items, see the `Item` type in the multi item
31483
31870
  * area.
31484
31871
  */
31872
+ /**
31873
+ * A "PerseusArticle" is an item that is meant to be rendered as an article.
31874
+ * This item is never scored and is rendered by the `ArticleRenderer`.
31875
+ */
31485
31876
  /**
31486
31877
  * A "MultiItem" is an advanced Perseus item. It is rendered by the
31487
31878
  * `MultiRenderer` and you can control the layout of individual parts of the
@@ -31578,6 +31969,10 @@ const ItemExtras = [
31578
31969
  // A background image applied to various widgets.
31579
31970
 
31580
31971
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
31972
+
31973
+ // 2D range: xMin, xMax, yMin, yMax
31974
+
31975
+ const lockedFigureColorNames = ["blue", "green", "grayH", "purple", "pink", "orange", "red"];
31581
31976
  const lockedFigureColors = {
31582
31977
  blue: "#3D7586",
31583
31978
  green: "#447A53",
@@ -31644,16 +32039,60 @@ function isUnlimitedGraphState(state) {
31644
32039
  * and the text outside of the $ blocks will be non-TeX text.
31645
32040
  */
31646
32041
  function replaceOutsideTeX(mathString) {
31647
- let currentlyTeX = mathString[0] === "$";
32042
+ // All the information we need is in the first section,
32043
+ // whether it's typed as "blockmath" or "paragraph"
32044
+ const firstSection = SimpleMarkdown.parse(mathString)[0];
32045
+
32046
+ // If it's blockMath, the outer level has the full math content.
32047
+ if (firstSection.type === "blockMath") {
32048
+ return firstSection.content;
32049
+ }
32050
+
32051
+ // If it's a paragraph, we need to iterate through the sections
32052
+ // to look for individual math blocks.
32053
+ const condensedNodes = condenseTextNodes(firstSection.content);
31648
32054
  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;
32055
+ for (const piece of condensedNodes) {
32056
+ piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
31654
32057
  }
31655
32058
  return result;
31656
32059
  }
32060
+ // Helper function for replaceOutsideTeX()
32061
+ // Condense adjacent text nodes into a single text node
32062
+ function condenseTextNodes(nodes) {
32063
+ const result = [];
32064
+ if (!nodes) {
32065
+ return result;
32066
+ }
32067
+ let currentText = "";
32068
+ for (const node of nodes) {
32069
+ if (node.type === "math") {
32070
+ if (currentText) {
32071
+ result.push({
32072
+ type: "text",
32073
+ content: currentText
32074
+ });
32075
+ currentText = "";
32076
+ }
32077
+ result.push(node);
32078
+ } else {
32079
+ currentText += node.content;
32080
+ }
32081
+ }
32082
+ if (currentText) {
32083
+ result.push({
32084
+ type: "text",
32085
+ content: currentText
32086
+ });
32087
+ }
32088
+ return result;
32089
+ }
32090
+
32091
+ // Helper function for replaceOutsideTeX()
32092
+ function escapeSpecialChars(str) {
32093
+ // Escape $, \, {, and } characters
32094
+ return str.replace(/([$\\{}])/g, "\\$1");
32095
+ }
31657
32096
 
31658
32097
  function LockedLabel(props) {
31659
32098
  const {
@@ -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()
41691
+ };
41692
+
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
+ };
41178
41706
  };
41179
41707
 
41180
- function numberLineValidator(state, rubric) {
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,20 +44146,26 @@ 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) {
43573
- const filterNonEmpty = function filterNonEmpty(table) {
43574
- return _.filter(table, function (row) {
43575
- // Check if row has a cell that is nonempty
43576
- return _.some(row, _.identity);
43577
- });
43578
- };
43579
- const solution = filterNonEmpty(rubric.answers);
43580
- const supplied = filterNonEmpty(state);
43581
- const hasEmptyCell = _.some(supplied, function (row) {
43582
- return _.some(row, function (cell) {
44152
+ /**
44153
+ * Filters the given table (modelled as a 2D array) to remove any rows that are
44154
+ * completely empty.
44155
+ *
44156
+ * @returns A new table with only non-empty rows.
44157
+ */
44158
+ const filterNonEmpty = function filterNonEmpty(table) {
44159
+ return table.filter(function (row) {
44160
+ // Return only rows that are non-empty.
44161
+ return row.some(cell => cell);
44162
+ });
44163
+ };
44164
+
44165
+ function validateTable(userInput) {
44166
+ const supplied = filterNonEmpty(userInput);
44167
+ const hasEmptyCell = supplied.some(function (row) {
44168
+ return row.some(function (cell) {
43583
44169
  return cell === "";
43584
44170
  });
43585
44171
  });
@@ -43589,6 +44175,16 @@ function tableValidator(state, rubric, strings) {
43589
44175
  message: null
43590
44176
  };
43591
44177
  }
44178
+ return null;
44179
+ }
44180
+
44181
+ function scoreTable(userInput, rubric, strings) {
44182
+ const validationResult = validateTable(userInput);
44183
+ if (validationResult != null) {
44184
+ return validationResult;
44185
+ }
44186
+ const supplied = filterNonEmpty(userInput);
44187
+ const solution = filterNonEmpty(rubric.answers);
43592
44188
  if (supplied.length !== solution.length) {
43593
44189
  return {
43594
44190
  type: "points",
@@ -43599,17 +44195,16 @@ function tableValidator(state, rubric, strings) {
43599
44195
  }
43600
44196
  const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
43601
44197
  let message = null;
43602
- const allCorrect = _.every(solution, function (rowSolution) {
44198
+ const allCorrect = solution.every(function (rowSolution) {
43603
44199
  for (let i = 0; i < supplied.length; i++) {
43604
44200
  const rowSupplied = supplied[i];
43605
- const correct = _.every(rowSupplied, function (cellSupplied, i) {
44201
+ const correct = rowSupplied.every(function (cellSupplied, i) {
43606
44202
  const cellSolution = rowSolution[i];
43607
44203
  const validator = createValidator(cellSolution, {
43608
44204
  simplify: true
43609
44205
  }, strings);
43610
44206
  const result = validator(cellSupplied);
43611
44207
  if (result.message) {
43612
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
43613
44208
  message = result.message;
43614
44209
  }
43615
44210
  return result.correct;
@@ -43625,7 +44220,7 @@ function tableValidator(state, rubric, strings) {
43625
44220
  type: "points",
43626
44221
  earned: allCorrect ? 1 : 0,
43627
44222
  total: 1,
43628
- message: message
44223
+ message
43629
44224
  };
43630
44225
  }
43631
44226
 
@@ -43846,7 +44441,11 @@ var Table$1 = {
43846
44441
  transform: propTransform,
43847
44442
  hidden: true,
43848
44443
  isLintable: true,
43849
- validator: tableValidator
44444
+ scorer: scoreTable
44445
+ };
44446
+
44447
+ const getPromptJSON = () => {
44448
+ return getUnsupportedPromptJSON("video");
43850
44449
  };
43851
44450
 
43852
44451
  /**
@@ -43942,6 +44541,9 @@ class Video extends React.Component {
43942
44541
  return change.apply(this, args);
43943
44542
  };
43944
44543
  }
44544
+ getPromptJSON() {
44545
+ return getPromptJSON();
44546
+ }
43945
44547
  render() {
43946
44548
  const {
43947
44549
  InitialRequestUrl
@@ -44002,8 +44604,8 @@ var Video$1 = {
44002
44604
  defaultAlignment: "block",
44003
44605
  supportedAlignments: ["block", "float-left", "float-right", "full-width"],
44004
44606
  widget: Video,
44005
- // TODO: things that aren't interactive shouldn't need validators
44006
- validator: () => noopValidator()
44607
+ // TODO: things that aren't interactive shouldn't need scoring functions
44608
+ scorer: () => scoreNoop()
44007
44609
  };
44008
44610
 
44009
44611
  /* eslint-disable import/no-named-as-default */
@@ -44022,7 +44624,7 @@ const init = function init() {
44022
44624
 
44023
44625
  // This file is processed by a Rollup plugin (replace) to inject the production
44024
44626
  const libName = "@khanacademy/perseus";
44025
- const libVersion = "41.2.0";
44627
+ const libVersion = "41.4.0";
44026
44628
  addLibraryVersionToPerseusDebug(libName, libVersion);
44027
44629
 
44028
44630
  const apiVersion = {
@@ -44759,6 +45361,9 @@ class ServerItemRenderer extends React.Component {
44759
45361
  getNumHints() {
44760
45362
  return this.props.item.hints.length;
44761
45363
  }
45364
+ getPromptJSON() {
45365
+ return this.questionRenderer.getPromptJSON();
45366
+ }
44762
45367
 
44763
45368
  /**
44764
45369
  * Grades the item.
@@ -45406,7 +46011,7 @@ function treeToItem(node) {
45406
46011
  };
45407
46012
  }
45408
46013
 
45409
- var isObject = function isObject(obj) {
46014
+ var isObject$1 = function isObject(obj) {
45410
46015
  return obj === Object(obj);
45411
46016
  };
45412
46017
  var merge = function merge() {
@@ -45422,7 +46027,7 @@ var merge = function merge() {
45422
46027
  return obj;
45423
46028
  };
45424
46029
  var clone = function clone(obj) {
45425
- if (!isObject(obj)) {
46030
+ if (!isObject$1(obj)) {
45426
46031
  return obj;
45427
46032
  }
45428
46033
  return Array.isArray(obj) ? obj.slice() : merge(obj);
@@ -45438,7 +46043,7 @@ var clone = function clone(obj) {
45438
46043
  var dispatch = function dispatch(x) {
45439
46044
  if (Array.isArray(x)) {
45440
46045
  return arr;
45441
- } else if (isObject(x)) {
46046
+ } else if (isObject$1(x)) {
45442
46047
  return obj;
45443
46048
  } else if (typeof x === "string") {
45444
46049
  return str;
@@ -47359,6 +47964,690 @@ function buildTestData(testObject) {
47359
47964
  return `{"data":{"assessmentItem":{"__typename":"AssessmentItemOrError","error":null,"item":{"__typename":"AssessmentItem","id":"x890b3c70f3e8f4a6","itemData":"${testObject}","problemType":"Type 1","sha":"c7284a3ad65214b4e62bccce236d92f7f5d35941"}}}}`;
47360
47965
  }
47361
47966
 
47967
+ function success(value) {
47968
+ return {
47969
+ type: "success",
47970
+ value
47971
+ };
47972
+ }
47973
+ function failure(detail) {
47974
+ return {
47975
+ type: "failure",
47976
+ detail
47977
+ };
47978
+ }
47979
+ function isFailure(result) {
47980
+ return result.type === "failure";
47981
+ }
47982
+ function isSuccess(result) {
47983
+ return result.type === "success";
47984
+ }
47985
+
47986
+ // Result's `all` function is similar to Promise.all: given an array of
47987
+ // results, it returns success if all succeeded, and failure if any failed.
47988
+ function all(results, combineFailureDetails = a => a) {
47989
+ const values = [];
47990
+ const failureDetails = [];
47991
+ for (const result of results) {
47992
+ if (result.type === "success") {
47993
+ values.push(result.value);
47994
+ } else {
47995
+ failureDetails.push(result.detail);
47996
+ }
47997
+ }
47998
+ if (failureDetails.length > 0) {
47999
+ return failure(failureDetails.reduce(combineFailureDetails));
48000
+ }
48001
+ return success(values);
48002
+ }
48003
+
48004
+ const any = (rawValue, ctx) => ctx.success(rawValue);
48005
+
48006
+ function array(elementParser) {
48007
+ return (rawValue, ctx) => {
48008
+ if (!Array.isArray(rawValue)) {
48009
+ return ctx.failure("array", rawValue);
48010
+ }
48011
+ const elementResults = rawValue.map((elem, i) => elementParser(elem, ctx.forSubtree(i)));
48012
+ return all(elementResults, concat);
48013
+ };
48014
+ }
48015
+ function concat(a, b) {
48016
+ return [...a, ...b];
48017
+ }
48018
+
48019
+ function boolean(rawValue, ctx) {
48020
+ if (typeof rawValue === "boolean") {
48021
+ return ctx.success(rawValue);
48022
+ }
48023
+ return ctx.failure("boolean", rawValue);
48024
+ }
48025
+
48026
+ function constant(acceptedValue) {
48027
+ return (rawValue, ctx) => {
48028
+ if (rawValue !== acceptedValue) {
48029
+ return ctx.failure(String(JSON.stringify(acceptedValue)), rawValue);
48030
+ }
48031
+ return ctx.success(acceptedValue);
48032
+ };
48033
+ }
48034
+
48035
+ function enumeration(...acceptedValues) {
48036
+ return (rawValue, ctx) => {
48037
+ if (typeof rawValue === "string") {
48038
+ const index = acceptedValues.indexOf(rawValue);
48039
+ if (index > -1) {
48040
+ return ctx.success(acceptedValues[index]);
48041
+ }
48042
+ }
48043
+ const expected = acceptedValues.map(v => JSON.stringify(v));
48044
+ return ctx.failure(expected, rawValue);
48045
+ };
48046
+ }
48047
+
48048
+ function isObject(x) {
48049
+ return x != null && Object.getPrototypeOf(x) === Object.prototype;
48050
+ }
48051
+
48052
+ function nullable(parseValue) {
48053
+ return (rawValue, ctx) => {
48054
+ if (rawValue === null) {
48055
+ return ctx.success(rawValue);
48056
+ }
48057
+ return parseValue(rawValue, ctx);
48058
+ };
48059
+ }
48060
+
48061
+ const number = (rawValue, ctx) => {
48062
+ if (typeof rawValue === "number") {
48063
+ return ctx.success(rawValue);
48064
+ }
48065
+ return ctx.failure("number", rawValue);
48066
+ };
48067
+
48068
+ function object(schema) {
48069
+ return (rawValue, ctx) => {
48070
+ if (!isObject(rawValue)) {
48071
+ return ctx.failure("object", rawValue);
48072
+ }
48073
+ const ret = _extends({}, rawValue);
48074
+ const mismatches = [];
48075
+ for (const [prop, propParser] of Object.entries(schema)) {
48076
+ const result = propParser(rawValue[prop], ctx.forSubtree(prop));
48077
+ if (isSuccess(result)) {
48078
+ ret[prop] = result.value;
48079
+ } else {
48080
+ mismatches.push(...result.detail);
48081
+ }
48082
+ }
48083
+ if (mismatches.length > 0) {
48084
+ return failure(mismatches);
48085
+ }
48086
+ return ctx.success(ret);
48087
+ };
48088
+ }
48089
+
48090
+ function optional(parseValue) {
48091
+ return (rawValue, ctx) => {
48092
+ if (rawValue === undefined) {
48093
+ return ctx.success(rawValue);
48094
+ }
48095
+ return parseValue(rawValue, ctx);
48096
+ };
48097
+ }
48098
+
48099
+ function pair(parseA, parseB) {
48100
+ return (rawValue, ctx) => {
48101
+ if (!Array.isArray(rawValue)) {
48102
+ return ctx.failure("array", rawValue);
48103
+ }
48104
+ if (rawValue.length !== 2) {
48105
+ return ctx.failure("array of length 2", rawValue);
48106
+ }
48107
+ const [rawA, rawB] = rawValue;
48108
+ const resultA = parseA(rawA, ctx.forSubtree(0));
48109
+ if (isFailure(resultA)) {
48110
+ return resultA;
48111
+ }
48112
+ const resultB = parseB(rawB, ctx.forSubtree(1));
48113
+ if (isFailure(resultB)) {
48114
+ return resultB;
48115
+ }
48116
+ return ctx.success([resultA.value, resultB.value]);
48117
+ };
48118
+ }
48119
+
48120
+ function record(parseKey, parseValue) {
48121
+ return (rawValue, ctx) => {
48122
+ if (!isObject(rawValue)) {
48123
+ return ctx.failure("object", rawValue);
48124
+ }
48125
+ const result = {};
48126
+ const mismatches = [];
48127
+ for (const [key, value] of Object.entries(rawValue)) {
48128
+ const entryCtx = ctx.forSubtree(key);
48129
+ const keyResult = parseKey(key, entryCtx);
48130
+ if (isFailure(keyResult)) {
48131
+ mismatches.push(...keyResult.detail);
48132
+ }
48133
+ const valueResult = parseValue(value, entryCtx);
48134
+ if (isFailure(valueResult)) {
48135
+ mismatches.push(...valueResult.detail);
48136
+ }
48137
+ if (isSuccess(keyResult) && isSuccess(valueResult)) {
48138
+ result[keyResult.value] = valueResult.value;
48139
+ }
48140
+ }
48141
+ if (mismatches.length > 0) {
48142
+ return failure(mismatches);
48143
+ }
48144
+ return ctx.success(result);
48145
+ };
48146
+ }
48147
+
48148
+ const string = (rawValue, ctx) => {
48149
+ if (typeof rawValue === "string") {
48150
+ return ctx.success(rawValue);
48151
+ }
48152
+ return ctx.failure("string", rawValue);
48153
+ };
48154
+
48155
+ function trio(parseA, parseB, parseC) {
48156
+ return (rawValue, ctx) => {
48157
+ if (!Array.isArray(rawValue)) {
48158
+ return ctx.failure("array", rawValue);
48159
+ }
48160
+ if (rawValue.length !== 3) {
48161
+ return ctx.failure("array of length 3", rawValue);
48162
+ }
48163
+ const resultA = parseA(rawValue[0], ctx.forSubtree(0));
48164
+ if (isFailure(resultA)) {
48165
+ return resultA;
48166
+ }
48167
+ const resultB = parseB(rawValue[1], ctx.forSubtree(1));
48168
+ if (isFailure(resultB)) {
48169
+ return resultB;
48170
+ }
48171
+ const resultC = parseC(rawValue[2], ctx.forSubtree(2));
48172
+ if (isFailure(resultC)) {
48173
+ return resultC;
48174
+ }
48175
+ return ctx.success([resultA.value, resultB.value, resultC.value]);
48176
+ };
48177
+ }
48178
+
48179
+ function union(parseBranch) {
48180
+ return new UnionBuilder(parseBranch);
48181
+ }
48182
+ class UnionBuilder {
48183
+ constructor(parser) {
48184
+ this.parser = parser;
48185
+ }
48186
+ or(newBranch) {
48187
+ return new UnionBuilder(either(this.parser, newBranch));
48188
+ }
48189
+ }
48190
+ function either(parseA, parseB) {
48191
+ return (rawValue, ctx) => {
48192
+ const resultA = parseA(rawValue, ctx);
48193
+ if (isSuccess(resultA)) {
48194
+ return resultA;
48195
+ }
48196
+ return parseB(rawValue, ctx);
48197
+ };
48198
+ }
48199
+
48200
+ function parseWidget(parseType, parseOptions) {
48201
+ return object({
48202
+ type: parseType,
48203
+ static: optional(boolean),
48204
+ graded: optional(boolean),
48205
+ alignment: optional(string),
48206
+ options: parseOptions,
48207
+ key: optional(number),
48208
+ version: object({
48209
+ major: number,
48210
+ minor: number
48211
+ })
48212
+ });
48213
+ }
48214
+
48215
+ const parseCategorizerWidget = parseWidget(constant("categorizer"), object({
48216
+ items: array(string),
48217
+ categories: array(string),
48218
+ randomizeItems: boolean,
48219
+ static: boolean,
48220
+ values: array(number),
48221
+ highlightLint: optional(boolean),
48222
+ linterContext: optional(object({
48223
+ contentType: string,
48224
+ paths: array(string),
48225
+ stack: array(string)
48226
+ }))
48227
+ }));
48228
+
48229
+ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
48230
+ programID: string,
48231
+ programType: any,
48232
+ settings: array(object({
48233
+ name: string,
48234
+ value: string
48235
+ })),
48236
+ showEditor: boolean,
48237
+ showButtons: boolean,
48238
+ width: number,
48239
+ height: number,
48240
+ static: boolean
48241
+ }));
48242
+
48243
+ const parseDefinitionWidget = parseWidget(constant("definition"), object({
48244
+ togglePrompt: string,
48245
+ definition: string,
48246
+ static: boolean
48247
+ }));
48248
+
48249
+ const parseDropdownWidget = parseWidget(constant("dropdown"), object({
48250
+ placeholder: string,
48251
+ static: boolean,
48252
+ choices: array(object({
48253
+ content: string,
48254
+ correct: boolean
48255
+ }))
48256
+ }));
48257
+
48258
+ const parseExplanationWidget = parseWidget(constant("explanation"), object({
48259
+ showPrompt: string,
48260
+ hidePrompt: string,
48261
+ explanation: string,
48262
+ // We wrap parseWidgetsMap in a function here to make sure it is not
48263
+ // referenced before it is defined. There is an import cycle between
48264
+ // this file and widgets-map.ts that could cause it to be undefined.
48265
+ widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
48266
+ static: boolean
48267
+ }));
48268
+
48269
+ const parseAnswerForm = object({
48270
+ value: string,
48271
+ form: boolean,
48272
+ simplify: boolean,
48273
+ considered: enumeration("correct", "wrong", "ungraded"),
48274
+ key: optional(string)
48275
+ });
48276
+ const parseExpressionWidget = parseWidget(constant("expression"), object({
48277
+ answerForms: array(parseAnswerForm),
48278
+ functions: array(string),
48279
+ times: boolean,
48280
+ visibleLabel: optional(string),
48281
+ ariaLabel: optional(string),
48282
+ buttonSets: array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")),
48283
+ buttonsVisible: optional(enumeration("always", "never", "focused"))
48284
+ }));
48285
+
48286
+ const parsePerseusImageBackground = object({
48287
+ url: optional(nullable(string)),
48288
+ width: optional(number),
48289
+ height: optional(number),
48290
+ top: optional(number),
48291
+ left: optional(number),
48292
+ bottom: optional(number),
48293
+ // TODO(benchristel): convert scale to a number
48294
+ scale: optional(union(number).or(string).parser)
48295
+ });
48296
+
48297
+ // Used to represent 2-D points and ranges
48298
+ const pairOfNumbers = pair(number, number);
48299
+ const parsePerseusGraphTypeAngle = object({
48300
+ type: constant("angle"),
48301
+ showAngles: optional(boolean),
48302
+ allowReflexAngles: optional(boolean),
48303
+ angleOffsetDeg: optional(number),
48304
+ snapDegrees: optional(number),
48305
+ match: optional(constant("congruent")),
48306
+ coords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
48307
+ startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))
48308
+ });
48309
+ const parsePerseusGraphTypeCircle = object({
48310
+ type: constant("circle"),
48311
+ center: optional(pairOfNumbers),
48312
+ radius: optional(number),
48313
+ startCoords: optional(object({
48314
+ center: pairOfNumbers,
48315
+ radius: number
48316
+ })),
48317
+ // TODO: remove coord? it's legacy.
48318
+ coord: optional(pairOfNumbers)
48319
+ });
48320
+ const parsePerseusGraphTypeLinear = object({
48321
+ type: constant("linear"),
48322
+ coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
48323
+ startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
48324
+ // TODO: remove coord? it's legacy.
48325
+ coord: optional(pairOfNumbers)
48326
+ });
48327
+ const parsePerseusGraphTypeLinearSystem = object({
48328
+ type: constant("linear-system"),
48329
+ // TODO(benchristel): default coords to empty array?
48330
+ coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
48331
+ startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
48332
+ // TODO: remove coord? it's legacy.
48333
+ coord: optional(pairOfNumbers)
48334
+ });
48335
+ const parsePerseusGraphTypeNone = object({
48336
+ type: constant("none")
48337
+ });
48338
+ const parsePerseusGraphTypePoint = object({
48339
+ type: constant("point"),
48340
+ numPoints: optional(union(number).or(constant("unlimited")).parser),
48341
+ coords: optional(nullable(array(pairOfNumbers))),
48342
+ startCoords: optional(array(pairOfNumbers)),
48343
+ // TODO: remove coord? it's legacy.
48344
+ coord: optional(pairOfNumbers)
48345
+ });
48346
+ const parsePerseusGraphTypePolygon = object({
48347
+ type: constant("polygon"),
48348
+ numSides: optional(union(number).or(constant("unlimited")).parser),
48349
+ showAngles: optional(boolean),
48350
+ showSides: optional(boolean),
48351
+ snapTo: optional(enumeration("grid", "angles", "sides")),
48352
+ match: optional(enumeration("similar", "congruent", "approx")),
48353
+ startCoords: optional(array(pairOfNumbers)),
48354
+ // TODO: remove coord? it's legacy.
48355
+ coord: optional(pairOfNumbers)
48356
+ });
48357
+ const parsePerseusGraphTypeQuadratic = object({
48358
+ type: constant("quadratic"),
48359
+ coords: optional(nullable(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))),
48360
+ startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
48361
+ // TODO: remove coord? it's legacy.
48362
+ coord: optional(pairOfNumbers)
48363
+ });
48364
+ const parsePerseusGraphTypeRay = object({
48365
+ type: constant("ray"),
48366
+ coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
48367
+ startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
48368
+ // TODO: remove coord? it's legacy.
48369
+ coord: optional(pairOfNumbers)
48370
+ });
48371
+ const parsePerseusGraphTypeSegment = object({
48372
+ type: constant("segment"),
48373
+ // TODO(benchristel): default numSegments?
48374
+ numSegments: optional(number),
48375
+ coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
48376
+ startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
48377
+ // TODO: remove coord? it's legacy.
48378
+ coord: optional(pairOfNumbers)
48379
+ });
48380
+ const parsePerseusGraphTypeSinusoid = object({
48381
+ type: constant("sinusoid"),
48382
+ coords: optional(nullable(array(pairOfNumbers))),
48383
+ startCoords: optional(array(pairOfNumbers)),
48384
+ // TODO: remove coord? it's legacy.
48385
+ coord: optional(pairOfNumbers)
48386
+ });
48387
+ 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;
48388
+ const parseLockedFigureColor = enumeration(...lockedFigureColorNames);
48389
+ const parseLockedFigureFillType = enumeration("none", "white", "translucent", "solid");
48390
+ const parseLockedLineStyle = enumeration("solid", "dashed");
48391
+ const parseLockedLabelType = object({
48392
+ type: constant("label"),
48393
+ coord: pairOfNumbers,
48394
+ text: string,
48395
+ color: parseLockedFigureColor,
48396
+ size: enumeration("small", "medium", "large")
48397
+ });
48398
+ const parseLockedPointType = object({
48399
+ type: constant("point"),
48400
+ coord: pairOfNumbers,
48401
+ color: parseLockedFigureColor,
48402
+ filled: boolean,
48403
+ // TODO(benchristel): default labels to empty array?
48404
+ labels: optional(array(parseLockedLabelType)),
48405
+ ariaLabel: optional(string)
48406
+ });
48407
+ const parseLockedLineType = object({
48408
+ type: constant("line"),
48409
+ kind: enumeration("line", "ray", "segment"),
48410
+ points: pair(parseLockedPointType, parseLockedPointType),
48411
+ color: parseLockedFigureColor,
48412
+ lineStyle: parseLockedLineStyle,
48413
+ showPoint1: boolean,
48414
+ showPoint2: boolean,
48415
+ // TODO(benchristel): default labels to empty array?
48416
+ labels: optional(array(parseLockedLabelType)),
48417
+ ariaLabel: optional(string)
48418
+ });
48419
+ const parseLockedVectorType = object({
48420
+ type: constant("vector"),
48421
+ points: pair(pairOfNumbers, pairOfNumbers),
48422
+ color: parseLockedFigureColor,
48423
+ // TODO(benchristel): default labels to empty array?
48424
+ labels: optional(array(parseLockedLabelType)),
48425
+ ariaLabel: optional(string)
48426
+ });
48427
+ const parseLockedEllipseType = object({
48428
+ type: constant("ellipse"),
48429
+ center: pairOfNumbers,
48430
+ radius: pairOfNumbers,
48431
+ angle: number,
48432
+ color: parseLockedFigureColor,
48433
+ fillStyle: parseLockedFigureFillType,
48434
+ strokeStyle: parseLockedLineStyle,
48435
+ // TODO(benchristel): default labels to empty array?
48436
+ labels: optional(array(parseLockedLabelType)),
48437
+ ariaLabel: optional(string)
48438
+ });
48439
+ const parseLockedPolygonType = object({
48440
+ type: constant("polygon"),
48441
+ points: array(pairOfNumbers),
48442
+ color: parseLockedFigureColor,
48443
+ showVertices: boolean,
48444
+ fillStyle: parseLockedFigureFillType,
48445
+ strokeStyle: parseLockedLineStyle,
48446
+ // TODO(benchristel): default labels to empty array?
48447
+ labels: optional(array(parseLockedLabelType)),
48448
+ ariaLabel: optional(string)
48449
+ });
48450
+ const parseLockedFunctionType = object({
48451
+ type: constant("function"),
48452
+ color: parseLockedFigureColor,
48453
+ strokeStyle: parseLockedLineStyle,
48454
+ equation: string,
48455
+ directionalAxis: enumeration("x", "y"),
48456
+ domain: optional(pairOfNumbers),
48457
+ // TODO(benchristel): default labels to empty array?
48458
+ labels: optional(array(parseLockedLabelType)),
48459
+ ariaLabel: optional(string)
48460
+ });
48461
+ const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
48462
+ const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
48463
+ step: pairOfNumbers,
48464
+ gridStep: pairOfNumbers,
48465
+ snapStep: pairOfNumbers,
48466
+ backgroundImage: optional(parsePerseusImageBackground),
48467
+ markings: enumeration("graph", "grid", "none"),
48468
+ labels: array(string),
48469
+ showProtractor: boolean,
48470
+ showRuler: optional(boolean),
48471
+ showTooltips: optional(boolean),
48472
+ rulerLabel: optional(string),
48473
+ rulerTicks: optional(number),
48474
+ range: pair(pairOfNumbers, pairOfNumbers),
48475
+ graph: parsePerseusGraphType,
48476
+ correct: parsePerseusGraphType,
48477
+ // TODO(benchristel): default lockedFigures to empty array
48478
+ lockedFigures: optional(array(parseLockedFigure)),
48479
+ fullGraphLabel: optional(string),
48480
+ fullGraphAriaDescription: optional(string)
48481
+ }));
48482
+
48483
+ const parseWidgetsMap = (rawValue, ctx) => {
48484
+ if (!isObject(rawValue)) {
48485
+ return ctx.failure("PerseusWidgetsMap", rawValue);
48486
+ }
48487
+ const widgetsMap = {};
48488
+ for (const key of Object.keys(rawValue)) {
48489
+ // parseWidgetsMapEntry modifies the widgetsMap. This is kind of gross,
48490
+ // but it's the only way I could find to make TypeScript check the key
48491
+ // against the widget type.
48492
+ const entryResult = parseWidgetsMapEntry([key, rawValue[key]], widgetsMap, ctx.forSubtree(key));
48493
+ if (isFailure(entryResult)) {
48494
+ return entryResult;
48495
+ }
48496
+ }
48497
+ return ctx.success(widgetsMap);
48498
+ };
48499
+ const parseWidgetsMapEntry = ([key, widget], widgetMap, ctx) => {
48500
+ const keyComponentsResult = parseWidgetMapKeyComponents(key.split(" "), ctx);
48501
+ if (isFailure(keyComponentsResult)) {
48502
+ return keyComponentsResult;
48503
+ }
48504
+ const [type, id] = keyComponentsResult.value;
48505
+ function parseAndAssign(key, parse) {
48506
+ const widgetResult = parse(widget, ctx);
48507
+ if (isFailure(widgetResult)) {
48508
+ return widgetResult;
48509
+ }
48510
+ widgetMap[key] = widgetResult.value;
48511
+ return ctx.success(undefined);
48512
+ }
48513
+ switch (type) {
48514
+ case "categorizer":
48515
+ return parseAndAssign(`categorizer ${id}`, parseCategorizerWidget);
48516
+ case "cs-program":
48517
+ return parseAndAssign(`cs-program ${id}`, parseCSProgramWidget);
48518
+ case "definition":
48519
+ return parseAndAssign(`definition ${id}`, parseDefinitionWidget);
48520
+ case "dropdown":
48521
+ return parseAndAssign(`dropdown ${id}`, parseDropdownWidget);
48522
+ case "explanation":
48523
+ return parseAndAssign(`explanation ${id}`, parseExplanationWidget);
48524
+ case "expression":
48525
+ return parseAndAssign(`expression ${id}`, parseExpressionWidget);
48526
+ case "grapher":
48527
+ // TODO(LEMS-2585): implement a real parser for this widget
48528
+ return parseAndAssign(`grapher ${id}`, any);
48529
+ case "group":
48530
+ // TODO(LEMS-2585): implement a real parser for this widget
48531
+ return parseAndAssign(`group ${id}`, any);
48532
+ case "graded-group":
48533
+ // TODO(LEMS-2585): implement a real parser for this widget
48534
+ return parseAndAssign(`graded-group ${id}`, any);
48535
+ case "graded-group-set":
48536
+ // TODO(LEMS-2585): implement a real parser for this widget
48537
+ return parseAndAssign(`graded-group-set ${id}`, any);
48538
+ case "iframe":
48539
+ // TODO(LEMS-2585): implement a real parser for this widget
48540
+ return parseAndAssign(`iframe ${id}`, any);
48541
+ case "image":
48542
+ // TODO(LEMS-2585): implement a real parser for this widget
48543
+ return parseAndAssign(`image ${id}`, any);
48544
+ case "input-number":
48545
+ // TODO(LEMS-2585): implement a real parser for this widget
48546
+ return parseAndAssign(`input-number ${id}`, any);
48547
+ case "interaction":
48548
+ // TODO(LEMS-2585): implement a real parser for this widget
48549
+ return parseAndAssign(`interaction ${id}`, any);
48550
+ case "interactive-graph":
48551
+ return parseAndAssign(`interactive-graph ${id}`, parseInteractiveGraphWidget);
48552
+ case "label-image":
48553
+ // TODO(LEMS-2585): implement a real parser for this widget
48554
+ return parseAndAssign(`label-image ${id}`, any);
48555
+ case "matcher":
48556
+ // TODO(LEMS-2585): implement a real parser for this widget
48557
+ return parseAndAssign(`matcher ${id}`, any);
48558
+ case "matrix":
48559
+ // TODO(LEMS-2585): implement a real parser for this widget
48560
+ return parseAndAssign(`matrix ${id}`, any);
48561
+ case "measurer":
48562
+ // TODO(LEMS-2585): implement a real parser for this widget
48563
+ return parseAndAssign(`measurer ${id}`, any);
48564
+ case "molecule-renderer":
48565
+ // TODO(LEMS-2585): implement a real parser for this widget
48566
+ return parseAndAssign(`molecule-renderer ${id}`, any);
48567
+ case "number-line":
48568
+ // TODO(LEMS-2585): implement a real parser for this widget
48569
+ return parseAndAssign(`number-line ${id}`, any);
48570
+ case "numeric-input":
48571
+ // TODO(LEMS-2585): implement a real parser for this widget
48572
+ return parseAndAssign(`numeric-input ${id}`, any);
48573
+ case "orderer":
48574
+ // TODO(LEMS-2585): implement a real parser for this widget
48575
+ return parseAndAssign(`orderer ${id}`, any);
48576
+ case "passage":
48577
+ // TODO(LEMS-2585): implement a real parser for this widget
48578
+ return parseAndAssign(`passage ${id}`, any);
48579
+ case "passage-ref":
48580
+ // TODO(LEMS-2585): implement a real parser for this widget
48581
+ return parseAndAssign(`passage-ref ${id}`, any);
48582
+ case "passage-ref-target":
48583
+ // TODO(LEMS-2585): implement a real parser for this widget
48584
+ return parseAndAssign(`passage-ref-target ${id}`, any);
48585
+ case "phet-simulation":
48586
+ // TODO(LEMS-2585): implement a real parser for this widget
48587
+ return parseAndAssign(`phet-simulation ${id}`, any);
48588
+ case "plotter":
48589
+ // TODO(LEMS-2585): implement a real parser for this widget
48590
+ return parseAndAssign(`plotter ${id}`, any);
48591
+ case "python-program":
48592
+ // TODO(LEMS-2585): implement a real parser for this widget
48593
+ return parseAndAssign(`python-program ${id}`, any);
48594
+ case "radio":
48595
+ // TODO(LEMS-2585): implement a real parser for this widget
48596
+ return parseAndAssign(`radio ${id}`, any);
48597
+ case "sorter":
48598
+ // TODO(LEMS-2585): implement a real parser for this widget
48599
+ return parseAndAssign(`sorter ${id}`, any);
48600
+ case "table":
48601
+ // TODO(LEMS-2585): implement a real parser for this widget
48602
+ return parseAndAssign(`table ${id}`, any);
48603
+ case "video":
48604
+ // TODO(LEMS-2585): implement a real parser for this widget
48605
+ return parseAndAssign(`video ${id}`, any);
48606
+ default:
48607
+ return ctx.failure("a valid widget type", type);
48608
+ }
48609
+ };
48610
+ const parseStringToPositiveInt = (rawValue, ctx) => {
48611
+ if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
48612
+ return ctx.failure("numeric string", rawValue);
48613
+ }
48614
+ return ctx.success(+rawValue);
48615
+ };
48616
+ const parseWidgetMapKeyComponents = pair(string, parseStringToPositiveInt);
48617
+
48618
+ const parseHint = object({
48619
+ replace: optional(boolean),
48620
+ content: string,
48621
+ widgets: parseWidgetsMap,
48622
+ metadata: optional(array(string)),
48623
+ images: record(string, object({
48624
+ width: number,
48625
+ height: number
48626
+ }))
48627
+ });
48628
+
48629
+ const parsePerseusRenderer = object({
48630
+ content: string,
48631
+ widgets: parseWidgetsMap,
48632
+ metadata: optional(array(string)),
48633
+ images: record(string, object({
48634
+ width: number,
48635
+ height: number
48636
+ }))
48637
+ });
48638
+
48639
+ object({
48640
+ question: parsePerseusRenderer,
48641
+ hints: array(parseHint),
48642
+ answerArea: record(enumeration(...ItemExtras), boolean),
48643
+ itemDataVersion: object({
48644
+ major: number,
48645
+ minor: number
48646
+ }),
48647
+ // Deprecated field
48648
+ answer: any
48649
+ });
48650
+
47362
48651
  /**
47363
48652
  * Helper to parse PerseusItem JSON
47364
48653
  * Why not just use JSON.parse? We want: