@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/index.js CHANGED
@@ -24,7 +24,7 @@ var kmath = require('@khanacademy/kmath');
24
24
  var wonderStuffCore = require('@khanacademy/wonder-stuff-core');
25
25
  var wonderBlocksProgressSpinner = require('@khanacademy/wonder-blocks-progress-spinner');
26
26
  var SimpleMarkdown = require('@khanacademy/simple-markdown');
27
- var pureMarkdown = require('@khanacademy/pure-markdown');
27
+ var SimpleMarkdown$1 = require('@khanacademy/pure-markdown');
28
28
  var wonderBlocksForm = require('@khanacademy/wonder-blocks-form');
29
29
  var Button = require('@khanacademy/wonder-blocks-button');
30
30
  var wonderBlocksLayout = require('@khanacademy/wonder-blocks-layout');
@@ -77,6 +77,7 @@ var Clickable__default = /*#__PURE__*/_interopDefaultLegacy(Clickable);
77
77
  var $__default = /*#__PURE__*/_interopDefaultLegacy($);
78
78
  var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
79
79
  var SimpleMarkdown__default = /*#__PURE__*/_interopDefaultLegacy(SimpleMarkdown);
80
+ var SimpleMarkdown__namespace = /*#__PURE__*/_interopNamespace(SimpleMarkdown$1);
80
81
  var Button__default = /*#__PURE__*/_interopDefaultLegacy(Button);
81
82
  var Banner__default = /*#__PURE__*/_interopDefaultLegacy(Banner);
82
83
  var caretDown__default = /*#__PURE__*/_interopDefaultLegacy(caretDown);
@@ -1293,6 +1294,41 @@ const ClassNames = {
1293
1294
  MOBILE: "perseus-mobile"
1294
1295
  };
1295
1296
 
1297
+ const getPromptJSON$t = (renderProps, userInput) => {
1298
+ return {
1299
+ type: "expression",
1300
+ label: renderProps.visibleLabel,
1301
+ userInput: {
1302
+ value: userInput
1303
+ }
1304
+ };
1305
+ };
1306
+
1307
+ /**
1308
+ * Get the character used for separating decimals.
1309
+ */
1310
+ const getDecimalSeparator = locale => {
1311
+ switch (locale) {
1312
+ // TODO(somewhatabstract): Remove this when Chrome supports the `ka`
1313
+ // locale properly.
1314
+ // https://github.com/formatjs/formatjs/issues/1526#issuecomment-559891201
1315
+ //
1316
+ // Supported locales in Chrome:
1317
+ // https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/scripts/chrome_ui_languages.list
1318
+ case "ka":
1319
+ return ",";
1320
+ default:
1321
+ const numberWithDecimalSeparator = 1.1;
1322
+ // TODO(FEI-3647): Update to use .formatToParts() once we no longer have to
1323
+ // support Safari 12.
1324
+ const match = new Intl.NumberFormat(locale).format(numberWithDecimalSeparator)
1325
+ // 0x661 is ARABIC-INDIC DIGIT ONE
1326
+ // 0x6F1 is EXTENDED ARABIC-INDIC DIGIT ONE
1327
+ .match(/[^\d\u0661\u06F1]/);
1328
+ return match?.[0] ?? ".";
1329
+ }
1330
+ };
1331
+
1296
1332
  // TODO(LP-11481): Change the primary API we use for logging in Perseus to use
1297
1333
  // getDependencies() internally... simplify usage to just using a "singleton"
1298
1334
  // which get's the injected logger.
@@ -1552,7 +1588,7 @@ const MAXERROR_EPSILON = Math.pow(2, -42);
1552
1588
  // TOOD(kevinb): Figure out how this relates to KEScore in
1553
1589
  // perseus-all-package/types.js and see if there's a way to
1554
1590
  // unify these types.
1555
- /*
1591
+ /**
1556
1592
  * Answer types
1557
1593
  *
1558
1594
  * Utility for creating answerable questions displayed in exercises
@@ -1595,7 +1631,6 @@ const MAXERROR_EPSILON = Math.pow(2, -42);
1595
1631
  * TODO(alpert): Think of a less-absurd name for createValidatorFunctional.
1596
1632
  *
1597
1633
  */
1598
-
1599
1634
  const KhanAnswerTypes = {
1600
1635
  /*
1601
1636
  * predicate answer type
@@ -2067,9 +2102,7 @@ const KhanAnswerTypes = {
2067
2102
  })];
2068
2103
  },
2069
2104
  createValidatorFunctional: function (correctAnswer, options, strings) {
2070
- return KhanAnswerTypes.predicate.createValidatorFunctional(
2071
- // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
2072
- ...KhanAnswerTypes.number.convertToPredicate(correctAnswer, options), strings);
2105
+ return KhanAnswerTypes.predicate.createValidatorFunctional(...KhanAnswerTypes.number.convertToPredicate(correctAnswer, options), strings);
2073
2106
  }
2074
2107
  },
2075
2108
  /*
@@ -2225,31 +2258,6 @@ const KhanAnswerTypes = {
2225
2258
  }
2226
2259
  };
2227
2260
 
2228
- /**
2229
- * Get the character used for separating decimals.
2230
- */
2231
- const getDecimalSeparator = locale => {
2232
- switch (locale) {
2233
- // TODO(somewhatabstract): Remove this when Chrome supports the `ka`
2234
- // locale properly.
2235
- // https://github.com/formatjs/formatjs/issues/1526#issuecomment-559891201
2236
- //
2237
- // Supported locales in Chrome:
2238
- // https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/scripts/chrome_ui_languages.list
2239
- case "ka":
2240
- return ",";
2241
- default:
2242
- const numberWithDecimalSeparator = 1.1;
2243
- // TODO(FEI-3647): Update to use .formatToParts() once we no longer have to
2244
- // support Safari 12.
2245
- const match = new Intl.NumberFormat(locale).format(numberWithDecimalSeparator)
2246
- // 0x661 is ARABIC-INDIC DIGIT ONE
2247
- // 0x6F1 is EXTENDED ARABIC-INDIC DIGIT ONE
2248
- .match(/[^\d\u0661\u06F1]/);
2249
- return match?.[0] ?? ".";
2250
- }
2251
- };
2252
-
2253
2261
  /* Content creators input a list of answers which are matched from top to
2254
2262
  * bottom. The intent is that they can include spcific solutions which should
2255
2263
  * be graded as correct or incorrect (or ungraded!) first, then get more
@@ -2268,7 +2276,7 @@ const getDecimalSeparator = locale => {
2268
2276
  * show the user an error. TODO(joel) - what error?
2269
2277
  * - Otherwise, pass through the resulting points and message.
2270
2278
  */
2271
- function expressionValidator(userInput, rubric, strings, locale) {
2279
+ function scoreExpression(userInput, rubric, strings, locale) {
2272
2280
  const options = ___default["default"].clone(rubric);
2273
2281
  ___default["default"].extend(options, {
2274
2282
  decimal_separator: getDecimalSeparator(locale)
@@ -2480,6 +2488,9 @@ class Expression extends React__namespace.Component {
2480
2488
  getUserInput() {
2481
2489
  return Expression.getUserInputFromProps(this.props);
2482
2490
  }
2491
+ getPromptJSON() {
2492
+ return getPromptJSON$t(this.props, this.getUserInput());
2493
+ }
2483
2494
  change = (() => {
2484
2495
  var _this = this;
2485
2496
  return function () {
@@ -2764,7 +2775,7 @@ var Expression$1 = {
2764
2775
  propUpgrades: propUpgrades$2,
2765
2776
  // For use by the editor
2766
2777
  isLintable: true,
2767
- validator: expressionValidator,
2778
+ scorer: scoreExpression,
2768
2779
  getOneCorrectAnswerFromRubric(rubric) {
2769
2780
  const correctAnswers = (rubric.answerForms || []).filter(answerForm => answerForm.considered === "correct");
2770
2781
  if (correctAnswers.length === 0) {
@@ -18502,10 +18513,10 @@ const styles$C = aphrodite.StyleSheet.create({
18502
18513
 
18503
18514
  /* eslint-disable no-useless-escape, no-prototype-builtins */
18504
18515
  const rules$1 = {
18505
- ...pureMarkdown.pureMarkdownRules,
18516
+ ...SimpleMarkdown$1.pureMarkdownRules,
18506
18517
  // NOTE: basically ignored by JIPT. wraps everything at the outer layer
18507
18518
  columns: {
18508
- ...pureMarkdown.pureMarkdownRules.columns,
18519
+ ...SimpleMarkdown$1.pureMarkdownRules.columns,
18509
18520
  react: (node, output, state) => {
18510
18521
  return /*#__PURE__*/React__namespace.createElement("div", {
18511
18522
  className: "perseus-two-columns",
@@ -18533,7 +18544,7 @@ const rules$1 = {
18533
18544
  // for exercises the renderer just renders the crowdin id to the
18534
18545
  // renderer div.
18535
18546
  crowdinId: {
18536
- ...pureMarkdown.pureMarkdownRules.crowdinId,
18547
+ ...SimpleMarkdown$1.pureMarkdownRules.crowdinId,
18537
18548
  react: (node, output, state) => node.id
18538
18549
  },
18539
18550
  // Ensure that the table is focusable in sequential keyboard navigation
@@ -18542,7 +18553,7 @@ const rules$1 = {
18542
18553
  // left/right gesture. Note, setting `tabindex` doesn't appear required for
18543
18554
  // iOS 14.4, in order for VoiceOver to work.
18544
18555
  table: {
18545
- ...pureMarkdown.pureMarkdownRules.table,
18556
+ ...SimpleMarkdown$1.pureMarkdownRules.table,
18546
18557
  react: function (node, output, state) {
18547
18558
  const table = SimpleMarkdown__default["default"].defaultRules.table.react(node, output, state);
18548
18559
  if (!state.isMobile) {
@@ -18560,7 +18571,7 @@ const rules$1 = {
18560
18571
  // our table regex into tableCapture[1..], and we pass tableCapture to
18561
18572
  // our nptable regex
18562
18573
  titledTable: {
18563
- ...pureMarkdown.pureMarkdownRules.titledTable,
18574
+ ...SimpleMarkdown$1.pureMarkdownRules.titledTable,
18564
18575
  react: (node, output, state) => {
18565
18576
  let contents;
18566
18577
  if (!node.table) {
@@ -18620,7 +18631,7 @@ const rules$1 = {
18620
18631
  }
18621
18632
  },
18622
18633
  widget: {
18623
- ...pureMarkdown.pureMarkdownRules.widget,
18634
+ ...SimpleMarkdown$1.pureMarkdownRules.widget,
18624
18635
  react: (node, output, state) => {
18625
18636
  // The actual output is handled in the renderer, where
18626
18637
  // we know the current widget props/state. This is
@@ -18631,7 +18642,7 @@ const rules$1 = {
18631
18642
  }
18632
18643
  },
18633
18644
  blockMath: {
18634
- ...pureMarkdown.pureMarkdownRules.blockMath,
18645
+ ...SimpleMarkdown$1.pureMarkdownRules.blockMath,
18635
18646
  react: (node, output, state) => {
18636
18647
  const {
18637
18648
  TeX
@@ -18645,7 +18656,7 @@ const rules$1 = {
18645
18656
  }
18646
18657
  },
18647
18658
  math: {
18648
- ...pureMarkdown.pureMarkdownRules.math,
18659
+ ...SimpleMarkdown$1.pureMarkdownRules.math,
18649
18660
  react: (node, output, state) => {
18650
18661
  const {
18651
18662
  TeX
@@ -18659,7 +18670,7 @@ const rules$1 = {
18659
18670
  }
18660
18671
  },
18661
18672
  unescapedDollar: {
18662
- ...pureMarkdown.pureMarkdownRules.unescapedDollar,
18673
+ ...SimpleMarkdown$1.pureMarkdownRules.unescapedDollar,
18663
18674
  react: (node, output, state) => {
18664
18675
  // Unescaped dollar signs render correctly, but result in
18665
18676
  // untranslatable text after the i18n python linter flags it
@@ -18669,7 +18680,7 @@ const rules$1 = {
18669
18680
  // Extend the SimpleMarkdown link parser to make the link open in a new
18670
18681
  // window and handle reverse tabnapping phishing attacks
18671
18682
  link: {
18672
- ...pureMarkdown.pureMarkdownRules.link,
18683
+ ...SimpleMarkdown$1.pureMarkdownRules.link,
18673
18684
  react: function (node, output, state) {
18674
18685
  const link = SimpleMarkdown__default["default"].defaultRules.link.react(node, output, state);
18675
18686
  const href = link.props.href;
@@ -18696,7 +18707,7 @@ const rules$1 = {
18696
18707
  }
18697
18708
  },
18698
18709
  codeBlock: {
18699
- ...pureMarkdown.pureMarkdownRules.codeBlock,
18710
+ ...SimpleMarkdown$1.pureMarkdownRules.codeBlock,
18700
18711
  react: (node, output, state) => {
18701
18712
  // ideally this should be a different rule, with only an
18702
18713
  // output function, but right now that breaks the parser.
@@ -18714,7 +18725,7 @@ const rules$1 = {
18714
18725
  // transform the tree to add lint nodes. This rule is here
18715
18726
  // just for the react() function
18716
18727
  lint: {
18717
- ...pureMarkdown.pureMarkdownRules.lint,
18728
+ ...SimpleMarkdown$1.pureMarkdownRules.lint,
18718
18729
  react: (node, output, state) => {
18719
18730
  return /*#__PURE__*/React__namespace.createElement(Lint, {
18720
18731
  message: node.message,
@@ -18964,8 +18975,8 @@ const getWidget = name => {
18964
18975
  const getWidgetExport = name => {
18965
18976
  return widgets[name] ?? null;
18966
18977
  };
18967
- const getWidgetValidator = name => {
18968
- return widgets[name]?.validator ?? null;
18978
+ const getWidgetScorer = name => {
18979
+ return widgets[name]?.scorer ?? null;
18969
18980
  };
18970
18981
  const getEditor = name => {
18971
18982
  return ___default["default"].has(editors, name) ? editors[name] : null;
@@ -19273,7 +19284,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19273
19284
  replaceDeprecatedEditors: replaceDeprecatedEditors,
19274
19285
  getWidget: getWidget,
19275
19286
  getWidgetExport: getWidgetExport,
19276
- getWidgetValidator: getWidgetValidator,
19287
+ getWidgetScorer: getWidgetScorer,
19277
19288
  getEditor: getEditor,
19278
19289
  getTransform: getTransform,
19279
19290
  getVersion: getVersion,
@@ -19305,12 +19316,12 @@ widgetIds, userInputMap, strings, locale) {
19305
19316
  }
19306
19317
  let score = null;
19307
19318
  const userInput = userInputMap[id];
19308
- const validator = getWidgetValidator(widget.type);
19319
+ const scorer = getWidgetScorer(widget.type);
19309
19320
  if (widget.type === "group") {
19310
19321
  const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19311
19322
  score = Util.flattenScores(scores);
19312
- } else if (validator) {
19313
- score = validator(userInput, widget.options, strings, locale);
19323
+ } else if (scorer) {
19324
+ score = scorer(userInput, widget.options, strings, locale);
19314
19325
  }
19315
19326
  if (score) {
19316
19327
  return Util.scoreIsEmpty(score);
@@ -19335,12 +19346,12 @@ widgetIds, userInputMap, strings, locale) {
19335
19346
  return;
19336
19347
  }
19337
19348
  const userInput = userInputMap[id];
19338
- const validator = getWidgetValidator(widget.type);
19349
+ const scorer = getWidgetScorer(widget.type);
19339
19350
  if (widget.type === "group") {
19340
19351
  const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19341
19352
  widgetScores[id] = Util.flattenScores(scores);
19342
- } else if (validator) {
19343
- widgetScores[id] = validator(userInput, widget.options, strings, locale);
19353
+ } else if (scorer) {
19354
+ widgetScores[id] = scorer(userInput, widget.options, strings, locale);
19344
19355
  }
19345
19356
  });
19346
19357
  return widgetScores;
@@ -19824,9 +19835,9 @@ class Renderer extends React__namespace.Component {
19824
19835
  // WidgetContainers don't update their widgets' props when
19825
19836
  // they are re-rendered, so even if they've been
19826
19837
  // re-rendered we need to call these methods on them.
19827
- ___default["default"].each(this.widgetIds, id => {
19838
+ this.widgetIds.forEach(id => {
19828
19839
  const container = this._widgetContainers.get(makeContainerId(id));
19829
- container && container.replaceWidgetProps(this.getWidgetProps(id));
19840
+ container?.replaceWidgetProps(this.getWidgetProps(id));
19830
19841
  });
19831
19842
  if (this.props.serializedState && !___default["default"].isEqual(this.props.serializedState, this.getSerializedState())) {
19832
19843
  this.restoreSerializedState(this.props.serializedState);
@@ -19965,25 +19976,25 @@ class Renderer extends React__namespace.Component {
19965
19976
  }
19966
19977
  return null;
19967
19978
  };
19968
- getWidgetProps = id => {
19979
+ getWidgetProps(widgetId) {
19969
19980
  var _this = this;
19970
19981
  const apiOptions = this.getApiOptions();
19971
- const widgetProps = this.state.widgetProps[id] || {};
19982
+ const widgetProps = this.state.widgetProps[widgetId] || {};
19972
19983
 
19973
19984
  // The widget needs access to its "rubric" at all times when in review
19974
19985
  // mode (which is really just part of its widget info).
19975
- const widgetInfo = this.state.widgetInfo[id];
19986
+ const widgetInfo = this.state.widgetInfo[widgetId];
19976
19987
  const reviewModeRubric = this.props.reviewMode && widgetInfo ? widgetInfo.options : null;
19977
19988
  if (!this._interactionTrackers) {
19978
19989
  this._interactionTrackers = {};
19979
19990
  }
19980
- let interactionTracker = this._interactionTrackers[id];
19991
+ let interactionTracker = this._interactionTrackers[widgetId];
19981
19992
  if (!interactionTracker) {
19982
- interactionTracker = this._interactionTrackers[id] = new InteractionTracker(apiOptions.trackInteraction, widgetInfo && widgetInfo.type, id, getTracking(widgetInfo && widgetInfo.type));
19993
+ interactionTracker = this._interactionTrackers[widgetId] = new InteractionTracker(apiOptions.trackInteraction, widgetInfo && widgetInfo.type, widgetId, getTracking(widgetInfo && widgetInfo.type));
19983
19994
  }
19984
19995
  return {
19985
19996
  ...widgetProps,
19986
- widgetId: id,
19997
+ widgetId: widgetId,
19987
19998
  alignment: widgetInfo && widgetInfo.alignment,
19988
19999
  static: widgetInfo?.static,
19989
20000
  problemNum: this.props.problemNum,
@@ -19991,19 +20002,19 @@ class Renderer extends React__namespace.Component {
19991
20002
  keypadElement: this.props.keypadElement,
19992
20003
  questionCompleted: this.props.questionCompleted,
19993
20004
  showSolutions: this.props.showSolutions,
19994
- onFocus: ___default["default"].partial(this._onWidgetFocus, id),
19995
- onBlur: ___default["default"].partial(this._onWidgetBlur, id),
20005
+ onFocus: ___default["default"].partial(this._onWidgetFocus, widgetId),
20006
+ onBlur: ___default["default"].partial(this._onWidgetBlur, widgetId),
19996
20007
  findWidgets: this.findWidgets,
19997
20008
  reviewModeRubric: reviewModeRubric,
19998
20009
  reviewMode: this.props.reviewMode,
19999
20010
  onChange: function (newProps, cb) {
20000
20011
  let silent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
20001
- _this._setWidgetProps(id, newProps, cb, silent);
20012
+ _this._setWidgetProps(widgetId, newProps, cb, silent);
20002
20013
  },
20003
20014
  trackInteraction: interactionTracker.track,
20004
- isLastUsedWidget: id === this.state.lastUsedWidgetId
20015
+ isLastUsedWidget: widgetId === this.state.lastUsedWidgetId
20005
20016
  };
20006
- };
20017
+ }
20007
20018
 
20008
20019
  /**
20009
20020
  * Serializes the questions state so it can be recovered.
@@ -20892,6 +20903,25 @@ class Renderer extends React__namespace.Component {
20892
20903
  return this.widgetIds;
20893
20904
  };
20894
20905
 
20906
+ /**
20907
+ * Returns a JSON representation of the content and widgets
20908
+ * that can be passed to an LLM for prompt context.
20909
+ */
20910
+ getPromptJSON() {
20911
+ const {
20912
+ content
20913
+ } = this.props;
20914
+ const widgetJSON = {};
20915
+ this.widgetIds.forEach(id => {
20916
+ const widget = this.getWidgetInstance(id);
20917
+ widgetJSON[id] = widget?.getPromptJSON?.() || {};
20918
+ });
20919
+ return {
20920
+ content,
20921
+ widgets: widgetJSON
20922
+ };
20923
+ }
20924
+
20895
20925
  /**
20896
20926
  * Returns an object mapping from widget ID to perseus-style score.
20897
20927
  * The keys of this object are the values of the array returned
@@ -21618,6 +21648,19 @@ SimpleKeypadInput.propTypes = {
21618
21648
  value: PropTypes__default["default"].oneOfType([PropTypes__default["default"].string, PropTypes__default["default"].number])
21619
21649
  };
21620
21650
 
21651
+ const getPromptJSON$s = (renderProps, userInput) => {
21652
+ return {
21653
+ type: "input-number",
21654
+ options: {
21655
+ simplify: renderProps.simplify,
21656
+ answerType: renderProps.answerType
21657
+ },
21658
+ userInput: {
21659
+ value: userInput.currentValue
21660
+ }
21661
+ };
21662
+ };
21663
+
21621
21664
  /*
21622
21665
  * In this file, an `expression` is some portion of valid TeX enclosed in
21623
21666
  * curly brackets.
@@ -21808,7 +21851,7 @@ const answerTypes = {
21808
21851
  forms: "pi"
21809
21852
  }
21810
21853
  };
21811
- function inputNumberValidator(userInput, rubric, strings) {
21854
+ function scoreInputNumber(userInput, rubric, strings) {
21812
21855
  if (rubric.answerType == null) {
21813
21856
  rubric.answerType = "number";
21814
21857
  }
@@ -21939,6 +21982,9 @@ class InputNumber extends React__namespace.Component {
21939
21982
  getUserInput() {
21940
21983
  return InputNumber.getUserInputFromProps(this.props);
21941
21984
  }
21985
+ getPromptJSON() {
21986
+ return getPromptJSON$s(this.props, this.getUserInput());
21987
+ }
21942
21988
  examples = () => {
21943
21989
  const {
21944
21990
  strings
@@ -22038,7 +22084,7 @@ var InputNumber$1 = {
22038
22084
  widget: InputNumber,
22039
22085
  transform: propTransform$3,
22040
22086
  isLintable: true,
22041
- validator: inputNumberValidator,
22087
+ scorer: scoreInputNumber,
22042
22088
  getOneCorrectAnswerFromRubric(rubric) {
22043
22089
  if (rubric.value == null) {
22044
22090
  return;
@@ -22051,6 +22097,16 @@ var InputNumber$1 = {
22051
22097
  }
22052
22098
  };
22053
22099
 
22100
+ const getPromptJSON$r = (renderProps, userInput) => {
22101
+ return {
22102
+ type: "numeric-input",
22103
+ label: renderProps.labelText,
22104
+ userInput: {
22105
+ value: userInput.currentValue
22106
+ }
22107
+ };
22108
+ };
22109
+
22054
22110
  const ParseTex = TexWrangler.parseTex;
22055
22111
  const answerFormButtons = [{
22056
22112
  title: "Integers",
@@ -22109,7 +22165,7 @@ function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
22109
22165
  // Otherwise, we return input valuåe (number) stripped of the "%".
22110
22166
  return value;
22111
22167
  }
22112
- function numericInputValidator(userInput, rubric, strings) {
22168
+ function scoreNumericInput(userInput, rubric, strings) {
22113
22169
  const defaultAnswerForms = answerFormButtons.map(e => e["value"])
22114
22170
  // Don't default to validating the answer as a pi answer
22115
22171
  // if answerForm isn't set on the answer
@@ -22271,6 +22327,9 @@ class NumericInput extends React__namespace.Component {
22271
22327
  getUserInput() {
22272
22328
  return NumericInput.getUserInputFromProps(this.props);
22273
22329
  }
22330
+ getPromptJSON() {
22331
+ return getPromptJSON$r(this.props, this.getUserInput());
22332
+ }
22274
22333
  handleChange = (newValue, cb) => {
22275
22334
  this.props.onChange({
22276
22335
  currentValue: newValue
@@ -22396,7 +22455,7 @@ var NumericInput$1 = {
22396
22455
  widget: NumericInput,
22397
22456
  transform: propsTransform,
22398
22457
  isLintable: true,
22399
- validator: numericInputValidator,
22458
+ scorer: scoreNumericInput,
22400
22459
  getOneCorrectAnswerFromRubric(rubric) {
22401
22460
  const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
22402
22461
  const answerStrings = correctAnswers.map(answer => {
@@ -22427,15 +22486,43 @@ var NumericInput$1 = {
22427
22486
  }
22428
22487
  };
22429
22488
 
22489
+ const getPromptJSON$q = (renderProps, userInput) => {
22490
+ const choices = renderProps.choices || [];
22491
+ const options = choices.map(choice => {
22492
+ return {
22493
+ value: choice.content
22494
+ };
22495
+ });
22496
+ return {
22497
+ type: "radio",
22498
+ hasNoneOfTheAbove: !!renderProps.hasNoneOfTheAbove,
22499
+ options,
22500
+ userInput: {
22501
+ selectedOptions: userInput.choicesSelected.slice()
22502
+ }
22503
+ };
22504
+ };
22505
+
22506
+ const getPromptJSON$p = renderProps => {
22507
+ return {
22508
+ type: "passage-ref",
22509
+ options: {
22510
+ passageNumber: renderProps.passageNumber,
22511
+ referenceNumber: renderProps.referenceNumber,
22512
+ summaryText: renderProps.summaryText
22513
+ }
22514
+ };
22515
+ };
22516
+
22430
22517
  /**
22431
- * Several widgets don't have "right"/"wrong" validation logic,
22518
+ * Several widgets don't have "right"/"wrong" scoring logic,
22432
22519
  * so this just says to move on past those widgets
22433
22520
  *
22434
22521
  * TODO(LEMS-2543) widgets that use this probably shouldn't have any
22435
- * validation logic and the thing validating an exercise
22522
+ * scoring logic and the thing scoring an exercise
22436
22523
  * should just know to skip these
22437
22524
  */
22438
- function noopValidator() {
22525
+ function scoreNoop() {
22439
22526
  let points = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
22440
22527
  return {
22441
22528
  type: "points",
@@ -23829,6 +23916,17 @@ const styles$x = aphrodite.StyleSheet.create({
23829
23916
  }
23830
23917
  });
23831
23918
 
23919
+ const getPromptJSON$o = renderProps => {
23920
+ return {
23921
+ type: "passage",
23922
+ options: {
23923
+ passageTitle: renderProps.passageTitle,
23924
+ passageText: renderProps.passageText,
23925
+ footnotes: renderProps.footnotes
23926
+ }
23927
+ };
23928
+ };
23929
+
23832
23930
  function getInitialParseState() {
23833
23931
  return {
23834
23932
  currentRef: [],
@@ -24461,6 +24559,9 @@ class Passage extends React__namespace.Component {
24461
24559
  content: refContent
24462
24560
  };
24463
24561
  }
24562
+ getPromptJSON() {
24563
+ return getPromptJSON$o(this.props);
24564
+ }
24464
24565
 
24465
24566
  /**
24466
24567
  * Rendering
@@ -24606,7 +24707,7 @@ var Passage$1 = {
24606
24707
  return ___default["default"].pick(editorProps, "passageTitle", "passageText", "footnotes", "showLineNumbers");
24607
24708
  },
24608
24709
  isLintable: true,
24609
- validator: () => noopValidator()
24710
+ scorer: () => scoreNoop()
24610
24711
  };
24611
24712
 
24612
24713
  /**
@@ -24671,6 +24772,9 @@ class PassageRef extends React__namespace.Component {
24671
24772
  return change.apply(_this, args);
24672
24773
  };
24673
24774
  })();
24775
+ getPromptJSON() {
24776
+ return getPromptJSON$p(this.props);
24777
+ }
24674
24778
  _deferredUpdateRange = () => {
24675
24779
  ___default["default"].defer(this._updateRange);
24676
24780
  };
@@ -24740,8 +24844,8 @@ var PassageRef$1 = {
24740
24844
  major: 0,
24741
24845
  minor: 1
24742
24846
  },
24743
- // TODO: things that aren't interactive shouldn't need validators
24744
- validator: () => noopValidator()
24847
+ // TODO: things that aren't interactive shouldn't need scoring functions
24848
+ scorer: () => scoreNoop()
24745
24849
  };
24746
24850
 
24747
24851
  /**
@@ -26109,7 +26213,7 @@ const styles$p = aphrodite.StyleSheet.create({
26109
26213
  }
26110
26214
  });
26111
26215
 
26112
- function radioValidator(userInput, rubric, strings) {
26216
+ function scoreRadio(userInput, rubric, strings) {
26113
26217
  const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
26114
26218
  return sum + (selected ? 1 : 0);
26115
26219
  }, 0);
@@ -26119,10 +26223,10 @@ function radioValidator(userInput, rubric, strings) {
26119
26223
  message: null
26120
26224
  };
26121
26225
  }
26122
-
26123
- // TODO(LEMS-2541): should numCorrect actually be on the rubric
26124
- // instead of the userInput?
26125
- if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
26226
+ const numCorrect = rubric.choices.reduce((sum, currentChoice) => {
26227
+ return currentChoice.correct ? sum + 1 : sum;
26228
+ }, 0);
26229
+ if (numCorrect > 1 && numSelected !== numCorrect) {
26126
26230
  return {
26127
26231
  type: "invalid",
26128
26232
  message: strings.chooseCorrectNum
@@ -26130,9 +26234,8 @@ function radioValidator(userInput, rubric, strings) {
26130
26234
  // If NOTA and some other answer are checked, ...
26131
26235
  }
26132
26236
 
26133
- // TODO(LEMS-2541): should noneOfTheAboveSelected be replaced with a
26134
- // combination of choicesSelected and noneOfTheAboveIndex?
26135
- if (userInput.noneOfTheAboveSelected && numSelected > 1) {
26237
+ const noneOfTheAboveSelected = rubric.choices.some((choice, index) => choice.isNoneOfTheAbove && userInput.choicesSelected[index]);
26238
+ if (noneOfTheAboveSelected && numSelected > 1) {
26136
26239
  return {
26137
26240
  type: "invalid",
26138
26241
  message: strings.notNoneOfTheAbove
@@ -26140,9 +26243,7 @@ function radioValidator(userInput, rubric, strings) {
26140
26243
  }
26141
26244
  const correct = userInput.choicesSelected.every((selected, i) => {
26142
26245
  let isCorrect;
26143
- // TODO(LEMS-2541): should noneOfTheAboveIndex actually be on the rubric
26144
- // instead of the userInput?
26145
- if (userInput.noneOfTheAboveIndex === i) {
26246
+ if (rubric.choices[i].isNoneOfTheAbove) {
26146
26247
  isCorrect = rubric.choices.every((choice, j) => {
26147
26248
  return i === j || !choice.correct;
26148
26249
  });
@@ -26174,31 +26275,19 @@ class Radio$1 extends React__namespace.Component {
26174
26275
  showSolutions: "none"
26175
26276
  };
26176
26277
  static getUserInputFromProps(props) {
26278
+ let unshuffle = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
26177
26279
  // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
26178
26280
  // future timeline implementers: this used to be {value: i} before
26179
26281
  // multiple select was added)
26180
26282
  if (props.choiceStates) {
26181
- let noneOfTheAboveIndex = null;
26182
- let noneOfTheAboveSelected = false;
26183
26283
  const choiceStates = props.choiceStates;
26184
26284
  const choicesSelected = choiceStates.map(() => false);
26185
- const numCorrect = props.numCorrect;
26186
26285
  for (let i = 0; i < choicesSelected.length; i++) {
26187
- const index = props.choices[i].originalIndex;
26286
+ const index = unshuffle ? props.choices[i].originalIndex : i;
26188
26287
  choicesSelected[index] = choiceStates[i].selected;
26189
- if (props.choices[i].isNoneOfTheAbove) {
26190
- // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'null'.
26191
- noneOfTheAboveIndex = index;
26192
- if (choicesSelected[i]) {
26193
- noneOfTheAboveSelected = true;
26194
- }
26195
- }
26196
26288
  }
26197
26289
  return {
26198
- choicesSelected,
26199
- numCorrect,
26200
- noneOfTheAboveIndex,
26201
- noneOfTheAboveSelected
26290
+ choicesSelected
26202
26291
  };
26203
26292
  // Support legacy choiceState implementation
26204
26293
  }
@@ -26207,27 +26296,14 @@ class Radio$1 extends React__namespace.Component {
26207
26296
  values
26208
26297
  } = props;
26209
26298
  if (values) {
26210
- let noneOfTheAboveIndex = null;
26211
- let noneOfTheAboveSelected = false;
26212
26299
  const choicesSelected = [...values];
26213
- const numCorrect = props.numCorrect;
26214
26300
  const valuesLength = values.length;
26215
26301
  for (let i = 0; i < valuesLength; i++) {
26216
- const index = props.choices[i].originalIndex;
26302
+ const index = unshuffle ? props.choices[i].originalIndex : i;
26217
26303
  choicesSelected[index] = values[i];
26218
- if (props.choices[i].isNoneOfTheAbove) {
26219
- // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'null'.
26220
- noneOfTheAboveIndex = index;
26221
- if (choicesSelected[i]) {
26222
- noneOfTheAboveSelected = true;
26223
- }
26224
- }
26225
26304
  }
26226
26305
  return {
26227
- choicesSelected,
26228
- noneOfTheAboveIndex,
26229
- noneOfTheAboveSelected,
26230
- numCorrect
26306
+ choicesSelected
26231
26307
  };
26232
26308
  }
26233
26309
  // Nothing checked
@@ -26355,6 +26431,10 @@ class Radio$1 extends React__namespace.Component {
26355
26431
  getUserInput() {
26356
26432
  return Radio$1.getUserInputFromProps(this.props);
26357
26433
  }
26434
+ getPromptJSON() {
26435
+ const userInput = Radio$1.getUserInputFromProps(this.props, false);
26436
+ return getPromptJSON$q(this.props, userInput);
26437
+ }
26358
26438
 
26359
26439
  /**
26360
26440
  * Turn on rationale display for the currently selected choices. Note that
@@ -26367,7 +26447,7 @@ class Radio$1 extends React__namespace.Component {
26367
26447
  choiceStates
26368
26448
  } = this.props;
26369
26449
  if (choiceStates) {
26370
- const score = radioValidator(this.getUserInput(), rubric, this.context.strings);
26450
+ const score = scoreRadio(this.getUserInput(), rubric, this.context.strings);
26371
26451
  const widgetCorrect = score.type === "points" && score.total === score.earned;
26372
26452
  const newStates = choiceStates.map(state => ({
26373
26453
  ...state,
@@ -26649,13 +26729,26 @@ var Radio = {
26649
26729
  },
26650
26730
  propUpgrades: propUpgrades$1,
26651
26731
  isLintable: true,
26652
- validator: radioValidator
26732
+ scorer: scoreRadio
26653
26733
  };
26654
26734
 
26655
26735
  /* eslint-disable import/no-named-as-default */
26656
26736
  var basicWidgets = [Radio, InputNumber$1, NumericInput$1, Expression$1];
26657
26737
 
26658
- function categorizerValidator(userInput, rubric, strings) {
26738
+ const getPromptJSON$n = (renderProps, userInput) => {
26739
+ return {
26740
+ type: "categorizer",
26741
+ options: {
26742
+ items: renderProps.items,
26743
+ categories: renderProps.categories
26744
+ },
26745
+ userInput: {
26746
+ itemToCategoryMapping: userInput.values
26747
+ }
26748
+ };
26749
+ };
26750
+
26751
+ function scoreCategorizer(userInput, rubric, strings) {
26659
26752
  let completed = true;
26660
26753
  let allCorrect = true;
26661
26754
  rubric.values.forEach((value, i) => {
@@ -26710,6 +26803,9 @@ class Categorizer extends React__namespace.Component {
26710
26803
  getUserInput() {
26711
26804
  return Categorizer.getUserInputFromProps(this.props);
26712
26805
  }
26806
+ getPromptJSON() {
26807
+ return getPromptJSON$n(this.props, this.getUserInput());
26808
+ }
26713
26809
  onChange(itemNum, catNum) {
26714
26810
  const values = [...this.props.values];
26715
26811
  // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'never'.
@@ -26856,7 +26952,7 @@ var Categorizer$1 = {
26856
26952
  return ___default["default"].pick(editorProps, "items", "categories", "values", "randomizeItems");
26857
26953
  },
26858
26954
  isLintable: true,
26859
- validator: categorizerValidator
26955
+ scorer: scoreCategorizer
26860
26956
  };
26861
26957
 
26862
26958
  /**
@@ -26906,7 +27002,20 @@ const parseUrl = url => new URL(url, location().origin);
26906
27002
  */
26907
27003
  const toAbsoluteUrl = url => parseUrl(url).href;
26908
27004
 
26909
- function csProgramValidator(state) {
27005
+ const getUnsupportedPromptJSON = function (widgetType) {
27006
+ let message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "";
27007
+ return {
27008
+ type: widgetType,
27009
+ isSupported: false,
27010
+ message
27011
+ };
27012
+ };
27013
+
27014
+ const getPromptJSON$m = () => {
27015
+ return getUnsupportedPromptJSON("cs-program");
27016
+ };
27017
+
27018
+ function scoreCSProgram(state) {
26910
27019
  // The iframe can tell us whether it's correct or incorrect,
26911
27020
  // and pass an optional message
26912
27021
  if (state.status === "correct") {
@@ -27002,6 +27111,9 @@ class CSProgram extends React__namespace.Component {
27002
27111
  message: this.props.message
27003
27112
  };
27004
27113
  }
27114
+ getPromptJSON() {
27115
+ return getPromptJSON$m();
27116
+ }
27005
27117
  render() {
27006
27118
  if (!this.props.programID) {
27007
27119
  return /*#__PURE__*/React__namespace.createElement("div", null);
@@ -27077,7 +27189,15 @@ var CSProgram$1 = {
27077
27189
  supportedAlignments: ["block", "full-width"],
27078
27190
  widget: CSProgram,
27079
27191
  hidden: true,
27080
- validator: csProgramValidator
27192
+ scorer: scoreCSProgram
27193
+ };
27194
+
27195
+ const getPromptJSON$l = renderProps => {
27196
+ return {
27197
+ type: "definition",
27198
+ definition: renderProps.definition,
27199
+ togglePrompt: renderProps.togglePrompt
27200
+ };
27081
27201
  };
27082
27202
 
27083
27203
  class Definition extends React__namespace.Component {
@@ -27090,6 +27210,9 @@ class Definition extends React__namespace.Component {
27090
27210
  // this just helps with TS weak typing when a Widget
27091
27211
  // doesn't implement any Widget methods
27092
27212
  isWidget = true;
27213
+ getPromptJSON() {
27214
+ return getPromptJSON$l(this.props);
27215
+ }
27093
27216
  render() {
27094
27217
  return /*#__PURE__*/React__namespace.createElement(DefinitionConsumer, null, _ref => {
27095
27218
  let {
@@ -27146,8 +27269,8 @@ var Definition$1 = {
27146
27269
  defaultAlignment: "inline",
27147
27270
  widget: Definition,
27148
27271
  transform: x => x,
27149
- // TODO: things that aren't interactive shouldn't need validators
27150
- validator: () => noopValidator()
27272
+ // TODO: things that aren't interactive shouldn't need scoring functions
27273
+ scorer: () => scoreNoop()
27151
27274
  };
27152
27275
 
27153
27276
  // The props are type `any` on purpose so that this can receive props
@@ -27175,11 +27298,23 @@ var DeprecatedStandin$1 = {
27175
27298
  displayName: "Deprecated Standin",
27176
27299
  widget: DeprecatedStandin,
27177
27300
  hidden: true,
27178
- // TODO: things that aren't interactive shouldn't need validators
27179
- validator: () => noopValidator(1)
27301
+ // TODO: things that aren't interactive shouldn't need scoring functions
27302
+ scorer: () => scoreNoop(1)
27303
+ };
27304
+
27305
+ const getPromptJSON$k = (renderProps, userInput) => {
27306
+ return {
27307
+ type: "dropdown",
27308
+ options: {
27309
+ items: renderProps.choices
27310
+ },
27311
+ userInput: {
27312
+ selectedIndex: userInput.value - 1 // Offset to account for placeholder
27313
+ }
27314
+ };
27180
27315
  };
27181
27316
 
27182
- function dropdownValidator(userInput, rubric) {
27317
+ function scoreDropdown(userInput, rubric) {
27183
27318
  const selected = userInput.value;
27184
27319
  if (selected === 0) {
27185
27320
  return {
@@ -27225,6 +27360,9 @@ class Dropdown extends React__namespace.Component {
27225
27360
  value: this.props.selected
27226
27361
  };
27227
27362
  }
27363
+ getPromptJSON() {
27364
+ return getPromptJSON$k(this.props, this.getUserInput());
27365
+ }
27228
27366
  render() {
27229
27367
  const children = [/*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.OptionItem, {
27230
27368
  key: "placeholder",
@@ -27266,7 +27404,15 @@ var Dropdown$1 = {
27266
27404
  accessible: true,
27267
27405
  widget: Dropdown,
27268
27406
  transform: optionsTransform,
27269
- validator: dropdownValidator
27407
+ scorer: scoreDropdown
27408
+ };
27409
+
27410
+ const getPromptJSON$j = renderProps => {
27411
+ return {
27412
+ type: "explanation",
27413
+ showPrompt: renderProps.showPrompt,
27414
+ explanation: renderProps.explanation
27415
+ };
27270
27416
  };
27271
27417
 
27272
27418
  // transform = _.identity
@@ -27315,6 +27461,9 @@ class Explanation extends React__namespace.Component {
27315
27461
  });
27316
27462
  this.props.trackInteraction();
27317
27463
  };
27464
+ getPromptJSON() {
27465
+ return getPromptJSON$j(this.props);
27466
+ }
27318
27467
  render() {
27319
27468
  const promptText = this.state.expanded ? this.props.hidePrompt : this.props.showPrompt;
27320
27469
  const caretIcon = this.state.expanded ? caretUp__default["default"] : caretDown__default["default"];
@@ -27421,8 +27570,26 @@ var Explanation$1 = {
27421
27570
  widget: Explanation,
27422
27571
  transform: ___default["default"].identity,
27423
27572
  isLintable: true,
27424
- // TODO: things that aren't interactive shouldn't need validators
27425
- validator: () => noopValidator()
27573
+ // TODO: things that aren't interactive shouldn't need scoring functions
27574
+ scorer: () => scoreNoop()
27575
+ };
27576
+
27577
+ const getPromptJSON$i = (title, rendererJSON, hintRendererJSON) => {
27578
+ if (!rendererJSON) {
27579
+ return {
27580
+ type: "graded-group",
27581
+ title,
27582
+ content: "",
27583
+ widgets: {},
27584
+ hint: hintRendererJSON
27585
+ };
27586
+ }
27587
+ return {
27588
+ ...rendererJSON,
27589
+ title,
27590
+ type: "graded-group",
27591
+ hint: hintRendererJSON
27592
+ };
27426
27593
  };
27427
27594
 
27428
27595
  class GradedGroupAnswerBar extends React__namespace.Component {
@@ -27566,6 +27733,8 @@ class GradedGroup extends React__namespace.Component {
27566
27733
  message: "",
27567
27734
  answerBarState: "HIDDEN"
27568
27735
  };
27736
+ rendererRef = /*#__PURE__*/React__namespace.createRef();
27737
+ hintRendererRef = /*#__PURE__*/React__namespace.createRef();
27569
27738
  shouldComponentUpdate(nextProps, nextState) {
27570
27739
  return nextProps !== this.props || nextState !== this.state;
27571
27740
  }
@@ -27591,13 +27760,9 @@ class GradedGroup extends React__namespace.Component {
27591
27760
  status: GRADING_STATUSES.ungraded,
27592
27761
  message: ""
27593
27762
  });
27594
-
27595
- // eslint-disable-next-line react/no-string-refs
27596
- if (this.refs.renderer) {
27763
+ if (this.rendererRef.current) {
27597
27764
  this.change("widgets", this.props.widgets);
27598
- // eslint-disable-next-line react/no-string-refs
27599
- // @ts-expect-error - TS2339 - Property 'emptyWidgets' does not exist on type 'ReactInstance'.
27600
- const emptyWidgets = this.refs.renderer.emptyWidgets();
27765
+ const emptyWidgets = this.rendererRef.current.emptyWidgets();
27601
27766
  const answerable = emptyWidgets.length === 0;
27602
27767
  const answerBarState = this.state.answerBarState;
27603
27768
  this.setState({
@@ -27606,12 +27771,10 @@ class GradedGroup extends React__namespace.Component {
27606
27771
  }
27607
27772
  };
27608
27773
  _checkAnswer = () => {
27609
- // eslint-disable-next-line react/no-string-refs
27610
- // @ts-expect-error - TS2339 - Property 'showRationalesForCurrentlySelectedChoices' does not exist on type 'ReactInstance'.
27611
- this.refs.renderer.showRationalesForCurrentlySelectedChoices();
27612
- // eslint-disable-next-line react/no-string-refs
27613
- // @ts-expect-error - TS2339 - Property 'score' does not exist on type 'ReactInstance'.
27614
- const score = this.refs.renderer.score();
27774
+ this.rendererRef.current?.showRationalesForCurrentlySelectedChoices();
27775
+ const score = this.rendererRef.current?.score() || {
27776
+ type: "invalid"
27777
+ };
27615
27778
  const {
27616
27779
  INVALID_MESSAGE_PREFIX,
27617
27780
  DEFAULT_INVALID_MESSAGE_1,
@@ -27632,29 +27795,28 @@ class GradedGroup extends React__namespace.Component {
27632
27795
 
27633
27796
  // Mobile API
27634
27797
  getInputPaths = () => {
27635
- // eslint-disable-next-line react/no-string-refs
27636
- // @ts-expect-error - TS2339 - Property 'getInputPaths' does not exist on type 'ReactInstance'.
27637
- return this.refs.renderer.getInputPaths();
27798
+ return this.rendererRef.current?.getInputPaths() || [];
27638
27799
  };
27800
+ getPromptJSON() {
27801
+ // If the hint isn't expanded, we can't get the prompt JSON from the rendered widgets.
27802
+ // We'll just pass in the hint content as a string instead.
27803
+ const hint = this.hintRendererRef.current?.getPromptJSON() || {
27804
+ content: this.props.hint?.content || "",
27805
+ widgets: {}
27806
+ };
27807
+ return getPromptJSON$i(this.props.title, this.rendererRef.current?.getPromptJSON(), hint);
27808
+ }
27639
27809
  setInputValue = (path, newValue, cb) => {
27640
- // eslint-disable-next-line react/no-string-refs
27641
- // @ts-expect-error - TS2339 - Property 'setInputValue' does not exist on type 'ReactInstance'.
27642
- return this.refs.renderer.setInputValue(path, newValue, cb);
27810
+ return this.rendererRef.current?.setInputValue(path, newValue, cb);
27643
27811
  };
27644
27812
  focus = () => {
27645
- // eslint-disable-next-line react/no-string-refs
27646
- // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
27647
- return this.refs.renderer.focus();
27813
+ return !!this.rendererRef.current?.focus();
27648
27814
  };
27649
27815
  focusInputPath = path => {
27650
- // eslint-disable-next-line react/no-string-refs
27651
- // @ts-expect-error - TS2339 - Property 'focusPath' does not exist on type 'ReactInstance'.
27652
- this.refs.renderer.focusPath(path);
27816
+ this.rendererRef.current?.focusPath(path);
27653
27817
  };
27654
27818
  blurInputPath = path => {
27655
- // eslint-disable-next-line react/no-string-refs
27656
- // @ts-expect-error - TS2339 - Property 'blurPath' does not exist on type 'ReactInstance'.
27657
- this.refs.renderer.blurPath(path);
27819
+ this.rendererRef.current?.blurPath(path);
27658
27820
  };
27659
27821
  render() {
27660
27822
  const apiOptions = ___default["default"].extend({}, ApiOptions.defaults, this.props.apiOptions, {
@@ -27712,8 +27874,7 @@ class GradedGroup extends React__namespace.Component {
27712
27874
  }, !!this.props.title && /*#__PURE__*/React__namespace.createElement("div", {
27713
27875
  className: aphrodite.css(styles$j.title)
27714
27876
  }, this.props.title), /*#__PURE__*/React__namespace.createElement(Renderer, _extends({}, this.props, {
27715
- // eslint-disable-next-line react/no-string-refs
27716
- ref: "renderer",
27877
+ ref: this.rendererRef,
27717
27878
  apiOptions: {
27718
27879
  ...apiOptions,
27719
27880
  readOnly
@@ -27756,8 +27917,7 @@ class GradedGroup extends React__namespace.Component {
27756
27917
  });
27757
27918
  }
27758
27919
  }, this.context.strings.hideExplanation), /*#__PURE__*/React__namespace.createElement(Renderer, _extends({}, this.props.hint, {
27759
- // eslint-disable-next-line react/no-string-refs
27760
- ref: "hints-renderer",
27920
+ ref: this.hintRendererRef,
27761
27921
  apiOptions: apiOptions,
27762
27922
  linterContext: this.props.linterContext,
27763
27923
  strings: this.context.strings
@@ -27849,6 +28009,16 @@ var GradedGroup$1 = {
27849
28009
  isLintable: true
27850
28010
  };
27851
28011
 
28012
+ const getPromptJSON$h = (renderProps, activeGroupJSON) => {
28013
+ return {
28014
+ type: "graded-group-set",
28015
+ options: {
28016
+ groupCount: renderProps.gradedGroups.length,
28017
+ currentGroup: activeGroupJSON
28018
+ }
28019
+ };
28020
+ };
28021
+
27852
28022
  class Indicators extends React__namespace.Component {
27853
28023
  static contextType = PerseusI18nContext;
27854
28024
  handleKeyDown = (e, i) => {
@@ -27927,6 +28097,10 @@ class GradedGroupSet extends React__namespace.Component {
27927
28097
  getInputPaths = () => {
27928
28098
  return this._childGroup.getInputPaths();
27929
28099
  };
28100
+ getPromptJSON() {
28101
+ const activeGroupPromptJSON = this._childGroup.getPromptJSON();
28102
+ return getPromptJSON$h(this.props, activeGroupPromptJSON);
28103
+ }
27930
28104
  setInputValue = (path, newValue, cb) => {
27931
28105
  return this._childGroup.setInputValue(path, newValue, cb);
27932
28106
  };
@@ -28180,6 +28354,33 @@ const styles$h = aphrodite.StyleSheet.create({
28180
28354
  }
28181
28355
  });
28182
28356
 
28357
+ const getPromptJSON$g = (renderProps, userInput) => {
28358
+ const {
28359
+ type,
28360
+ coords
28361
+ } = userInput;
28362
+ const input = {
28363
+ type,
28364
+ coords
28365
+ };
28366
+ if (userInput.type === "logarithm" || userInput.type === "exponential") {
28367
+ input["asymptote"] = userInput.asymptote;
28368
+ }
28369
+ return {
28370
+ type: "grapher",
28371
+ options: {
28372
+ availableTypes: renderProps.availableTypes,
28373
+ range: renderProps.graph.range,
28374
+ labels: renderProps.graph.labels,
28375
+ tickStep: renderProps.graph.step,
28376
+ gridStep: renderProps.graph.gridStep,
28377
+ snapStep: renderProps.graph.snapStep,
28378
+ backgroundImageUrl: renderProps.graph.backgroundImage.url
28379
+ },
28380
+ userInput: input
28381
+ };
28382
+ };
28383
+
28183
28384
  /* eslint-disable one-var */
28184
28385
  // @ts-expect-error - TS2339 - Property 'Plot' does not exist on type 'typeof Graphie'.
28185
28386
  const Plot$1 = Graphie.Plot;
@@ -28703,7 +28904,7 @@ function getCoefficientsByType(data) {
28703
28904
  throw new perseusCore.PerseusError("Invalid grapher type", perseusCore.Errors.InvalidInput);
28704
28905
  }
28705
28906
  }
28706
- function grapherValidator(userInput, rubric) {
28907
+ function scoreGrapher(userInput, rubric) {
28707
28908
  if (userInput.type !== rubric.correct.type) {
28708
28909
  return {
28709
28910
  type: "points",
@@ -29054,6 +29255,9 @@ class Grapher extends React__namespace.Component {
29054
29255
  getUserInput() {
29055
29256
  return Grapher.getUserInputFromProps(this.props);
29056
29257
  }
29258
+ getPromptJSON() {
29259
+ return getPromptJSON$g(this.props, this.getUserInput());
29260
+ }
29057
29261
  render() {
29058
29262
  const type = this.props.plot.type;
29059
29263
  const coords = this.props.plot.coords;
@@ -29143,7 +29347,21 @@ var Grapher$1 = {
29143
29347
  widget: Grapher,
29144
29348
  transform: propTransform$2,
29145
29349
  staticTransform: staticTransform$4,
29146
- validator: grapherValidator
29350
+ scorer: scoreGrapher
29351
+ };
29352
+
29353
+ const getPromptJSON$f = rendererJSON => {
29354
+ if (!rendererJSON) {
29355
+ return {
29356
+ type: "group",
29357
+ content: "",
29358
+ widgets: {}
29359
+ };
29360
+ }
29361
+ return {
29362
+ ...rendererJSON,
29363
+ type: "group"
29364
+ };
29147
29365
  };
29148
29366
 
29149
29367
  // exports has no 'transform'
@@ -29182,6 +29400,9 @@ class Group extends React__namespace.Component {
29182
29400
  getUserInput() {
29183
29401
  return this.rendererRef?.getUserInput();
29184
29402
  }
29403
+ getPromptJSON() {
29404
+ return getPromptJSON$f(this.rendererRef?.getPromptJSON());
29405
+ }
29185
29406
  getSerializedState = () => {
29186
29407
  return this.rendererRef?.getSerializedState();
29187
29408
  };
@@ -29295,7 +29516,11 @@ var Group$1 = {
29295
29516
  isLintable: true
29296
29517
  };
29297
29518
 
29298
- function iframeValidator(userInput) {
29519
+ const getPromptJSON$e = () => {
29520
+ return getUnsupportedPromptJSON("iframe");
29521
+ };
29522
+
29523
+ function scoreIframe(userInput) {
29299
29524
  // The iframe can tell us whether it's correct or incorrect,
29300
29525
  // and pass an optional message
29301
29526
  if (userInput.status === "correct") {
@@ -29352,6 +29577,9 @@ class Iframe extends React__namespace.Component {
29352
29577
  message: this.props.message
29353
29578
  };
29354
29579
  }
29580
+ getPromptJSON() {
29581
+ return getPromptJSON$e();
29582
+ }
29355
29583
  handleMessageEvent = e => {
29356
29584
  // We receive data from the iframe that contains {passed: true/false}
29357
29585
  // and use that to set the status
@@ -29449,7 +29677,19 @@ var Iframe$1 = {
29449
29677
  widget: Iframe,
29450
29678
  // Let's not expose it to all content creators yet
29451
29679
  hidden: true,
29452
- validator: iframeValidator
29680
+ scorer: scoreIframe
29681
+ };
29682
+
29683
+ const getPromptJSON$d = renderProps => {
29684
+ return {
29685
+ type: "image",
29686
+ options: {
29687
+ altText: renderProps.alt,
29688
+ title: renderProps.title,
29689
+ caption: renderProps.caption,
29690
+ imageUrl: renderProps.backgroundImage.url
29691
+ }
29692
+ };
29453
29693
  };
29454
29694
 
29455
29695
  const defaultBoxSize = 400;
@@ -29490,6 +29730,9 @@ class ImageWidget extends React__namespace.Component {
29490
29730
  return change.apply(_this, args);
29491
29731
  };
29492
29732
  })();
29733
+ getPromptJSON() {
29734
+ return getPromptJSON$d(this.props);
29735
+ }
29493
29736
  render() {
29494
29737
  let image;
29495
29738
  const alt = this.props.caption === this.props.alt ? "" : this.props.alt;
@@ -29636,8 +29879,12 @@ var Image$1 = {
29636
29879
  displayName: "Image",
29637
29880
  widget: ImageWidget,
29638
29881
  isLintable: true,
29639
- // TODO: things that aren't interactive shouldn't need validators
29640
- validator: () => noopValidator()
29882
+ // TODO: things that aren't interactive shouldn't need scoring functions
29883
+ scorer: () => scoreNoop()
29884
+ };
29885
+
29886
+ const getPromptJSON$c = () => {
29887
+ return getUnsupportedPromptJSON("interaction");
29641
29888
  };
29642
29889
 
29643
29890
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -29826,6 +30073,9 @@ class Interaction extends React__namespace.Component {
29826
30073
  return change.apply(_this, args);
29827
30074
  };
29828
30075
  })();
30076
+ getPromptJSON() {
30077
+ return getPromptJSON$c();
30078
+ }
29829
30079
  render() {
29830
30080
  const range = this.props.graph.range;
29831
30081
  let labels = this.props.graph.labels;
@@ -30234,8 +30484,8 @@ var Interactive = {
30234
30484
  widget: Interaction,
30235
30485
  transform: ___default["default"].identity,
30236
30486
  hidden: true,
30237
- // TODO: things that aren't interactive shouldn't need validators
30238
- validator: () => noopValidator()
30487
+ // TODO: things that aren't interactive shouldn't need scoring functions
30488
+ scorer: () => scoreNoop()
30239
30489
  };
30240
30490
 
30241
30491
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -30539,6 +30789,137 @@ class Graph extends React__namespace.Component {
30539
30789
  }
30540
30790
  }
30541
30791
 
30792
+ const getPromptJSON$b = (props, userInput) => {
30793
+ return {
30794
+ type: "interactive-graph",
30795
+ options: {
30796
+ graph: getGraphOptionsForProps(props),
30797
+ backgroundImageUrl: props.backgroundImage?.url,
30798
+ range: props.range,
30799
+ labels: props.labels
30800
+ },
30801
+ userInput: getUserInput(userInput)
30802
+ };
30803
+ };
30804
+ const getGraphOptionsForProps = props => {
30805
+ const type = props.graph.type;
30806
+ switch (type) {
30807
+ case "angle":
30808
+ return {
30809
+ type: props.graph.type,
30810
+ angleOffsetDegrees: props.graph.angleOffsetDeg,
30811
+ startCoords: props.graph.startCoords
30812
+ };
30813
+ case "circle":
30814
+ return {
30815
+ type: props.graph.type,
30816
+ startParams: {
30817
+ center: props.graph.startCoords?.center,
30818
+ radius: props.graph.startCoords?.radius
30819
+ }
30820
+ };
30821
+ case "linear":
30822
+ return {
30823
+ type: props.graph.type,
30824
+ startCoords: props.graph.startCoords
30825
+ };
30826
+ case "linear-system":
30827
+ return {
30828
+ type: props.graph.type,
30829
+ startCoords: props.graph.startCoords
30830
+ };
30831
+ case "point":
30832
+ return {
30833
+ type: props.graph.type,
30834
+ numPoints: props.graph.numPoints,
30835
+ startCoords: props.graph.startCoords
30836
+ };
30837
+ case "polygon":
30838
+ return {
30839
+ type: props.graph.type,
30840
+ match: props.graph.match,
30841
+ numSides: props.graph.numSides,
30842
+ startCoords: props.graph.startCoords
30843
+ };
30844
+ case "quadratic":
30845
+ return {
30846
+ type: props.graph.type,
30847
+ startCoords: props.graph.startCoords
30848
+ };
30849
+ case "ray":
30850
+ return {
30851
+ type: props.graph.type,
30852
+ startCoords: props.graph.startCoords
30853
+ };
30854
+ case "segment":
30855
+ return {
30856
+ type: props.graph.type,
30857
+ numSegments: props.graph.numSegments,
30858
+ startCoords: props.graph.startCoords
30859
+ };
30860
+ case "sinusoid":
30861
+ return {
30862
+ type: props.graph.type,
30863
+ startCoords: props.graph.startCoords
30864
+ };
30865
+ case "none":
30866
+ return {};
30867
+ default:
30868
+ throw new wonderStuffCore.UnreachableCaseError(type);
30869
+ }
30870
+ };
30871
+ const getUserInput = userInput => {
30872
+ const type = userInput.type;
30873
+ switch (type) {
30874
+ case "angle":
30875
+ return {
30876
+ coords: userInput.coords,
30877
+ angleOffsetDegrees: userInput.angleOffsetDeg
30878
+ };
30879
+ case "circle":
30880
+ return {
30881
+ center: userInput.center,
30882
+ radius: userInput.radius
30883
+ };
30884
+ case "linear":
30885
+ return {
30886
+ coords: userInput.coords
30887
+ };
30888
+ case "linear-system":
30889
+ return {
30890
+ coords: userInput.coords
30891
+ };
30892
+ case "point":
30893
+ return {
30894
+ coords: userInput.coords
30895
+ };
30896
+ case "polygon":
30897
+ return {
30898
+ coords: userInput.coords
30899
+ };
30900
+ case "quadratic":
30901
+ return {
30902
+ coords: userInput.coords
30903
+ };
30904
+ case "ray":
30905
+ return {
30906
+ coords: userInput.coords
30907
+ };
30908
+ case "segment":
30909
+ return {
30910
+ coords: userInput.coords
30911
+ };
30912
+ case "sinusoid":
30913
+ return {
30914
+ coords: userInput.coords
30915
+ };
30916
+ case "none":
30917
+ return {};
30918
+ default:
30919
+ throw new wonderStuffCore.UnreachableCaseError(type);
30920
+ }
30921
+ };
30922
+
30542
30923
  function pathBuilder() {
30543
30924
  return new PathBuilder();
30544
30925
  }
@@ -31790,6 +32171,10 @@ const LegacyGrid = _ref => {
31790
32171
  * To render more complex Perseus items, see the `Item` type in the multi item
31791
32172
  * area.
31792
32173
  */
32174
+ /**
32175
+ * A "PerseusArticle" is an item that is meant to be rendered as an article.
32176
+ * This item is never scored and is rendered by the `ArticleRenderer`.
32177
+ */
31793
32178
  /**
31794
32179
  * A "MultiItem" is an advanced Perseus item. It is rendered by the
31795
32180
  * `MultiRenderer` and you can control the layout of individual parts of the
@@ -31886,6 +32271,10 @@ const ItemExtras = [
31886
32271
  // A background image applied to various widgets.
31887
32272
 
31888
32273
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
32274
+
32275
+ // 2D range: xMin, xMax, yMin, yMax
32276
+
32277
+ const lockedFigureColorNames = ["blue", "green", "grayH", "purple", "pink", "orange", "red"];
31889
32278
  const lockedFigureColors = {
31890
32279
  blue: "#3D7586",
31891
32280
  green: "#447A53",
@@ -31953,17 +32342,61 @@ function isUnlimitedGraphState(state) {
31953
32342
  * and the text outside of the $ blocks will be non-TeX text.
31954
32343
  */
31955
32344
  function replaceOutsideTeX(mathString) {
31956
- let currentlyTeX = mathString[0] === "$";
32345
+ // All the information we need is in the first section,
32346
+ // whether it's typed as "blockmath" or "paragraph"
32347
+ const firstSection = SimpleMarkdown__namespace.parse(mathString)[0];
32348
+
32349
+ // If it's blockMath, the outer level has the full math content.
32350
+ if (firstSection.type === "blockMath") {
32351
+ return firstSection.content;
32352
+ }
32353
+
32354
+ // If it's a paragraph, we need to iterate through the sections
32355
+ // to look for individual math blocks.
32356
+ const condensedNodes = condenseTextNodes(firstSection.content);
31957
32357
  let result = "";
31958
- const splitString = mathString.split("$").filter(part => part !== "");
31959
- for (let i = 0; i < splitString.length; i++) {
31960
- const part = splitString[i];
31961
- result += currentlyTeX ? part : `\\text{${part}}`;
31962
- currentlyTeX = !currentlyTeX;
32358
+ for (const piece of condensedNodes) {
32359
+ piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
32360
+ }
32361
+ return result;
32362
+ }
32363
+ // Helper function for replaceOutsideTeX()
32364
+ // Condense adjacent text nodes into a single text node
32365
+ function condenseTextNodes(nodes) {
32366
+ const result = [];
32367
+ if (!nodes) {
32368
+ return result;
32369
+ }
32370
+ let currentText = "";
32371
+ for (const node of nodes) {
32372
+ if (node.type === "math") {
32373
+ if (currentText) {
32374
+ result.push({
32375
+ type: "text",
32376
+ content: currentText
32377
+ });
32378
+ currentText = "";
32379
+ }
32380
+ result.push(node);
32381
+ } else {
32382
+ currentText += node.content;
32383
+ }
32384
+ }
32385
+ if (currentText) {
32386
+ result.push({
32387
+ type: "text",
32388
+ content: currentText
32389
+ });
31963
32390
  }
31964
32391
  return result;
31965
32392
  }
31966
32393
 
32394
+ // Helper function for replaceOutsideTeX()
32395
+ function escapeSpecialChars(str) {
32396
+ // Escape $, \, {, and } characters
32397
+ return str.replace(/([$\\{}])/g, "\\$1");
32398
+ }
32399
+
31967
32400
  function LockedLabel(props) {
31968
32401
  const {
31969
32402
  coord,
@@ -36298,7 +36731,7 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
36298
36731
 
36299
36732
  const eq$1 = Util.eq;
36300
36733
  const deepEq$2 = Util.deepEq;
36301
- function interactiveGraphValidator(userInput, rubric) {
36734
+ function scoreInteractiveGraph(userInput, rubric) {
36302
36735
  // None-type graphs are not graded
36303
36736
  if (userInput.type === "none" && rubric.correct.type === "none") {
36304
36737
  return {
@@ -37796,6 +38229,9 @@ class InteractiveGraph extends React__namespace.Component {
37796
38229
  }
37797
38230
  throw new perseusCore.PerseusError("Cannot getUserInput from a graph that has never rendered", perseusCore.Errors.NotAllowed);
37798
38231
  }
38232
+ getPromptJSON() {
38233
+ return getPromptJSON$b(this.props, this.getUserInput());
38234
+ }
37799
38235
  render() {
37800
38236
  // Mafs shim
37801
38237
  const mafsFlags = this.props.apiOptions?.flags?.["mafs"];
@@ -38160,7 +38596,7 @@ var InteractiveGraph$1 = {
38160
38596
  displayName: "Interactive graph (Assessments only)",
38161
38597
  widget: InteractiveGraph,
38162
38598
  staticTransform: staticTransform$3,
38163
- validator: interactiveGraphValidator
38599
+ scorer: scoreInteractiveGraph
38164
38600
  };
38165
38601
 
38166
38602
  // TODO: can we use WB typography?
@@ -38172,6 +38608,32 @@ const bodyXsmallBold = {
38172
38608
  lineHeight: "22px"
38173
38609
  };
38174
38610
 
38611
+ const getPromptJSON$a = (renderProps, userInput) => {
38612
+ const propMarkers = renderProps.markers.map(marker => {
38613
+ return {
38614
+ label: marker.label
38615
+ };
38616
+ });
38617
+ const inputMarkers = userInput.markers.map(marker => {
38618
+ return {
38619
+ label: marker.label,
38620
+ selected: marker.selected
38621
+ };
38622
+ });
38623
+ return {
38624
+ type: "label-image",
38625
+ options: {
38626
+ choices: renderProps.choices,
38627
+ imageUrl: renderProps.imageUrl,
38628
+ imageAlt: renderProps.imageAlt,
38629
+ markers: propMarkers
38630
+ },
38631
+ userInput: {
38632
+ markers: inputMarkers
38633
+ }
38634
+ };
38635
+ };
38636
+
38175
38637
  const AnswerChoices = props => {
38176
38638
  const {
38177
38639
  strings
@@ -38257,59 +38719,6 @@ const styles$d = aphrodite.StyleSheet.create({
38257
38719
  }
38258
38720
  });
38259
38721
 
38260
- // Question state for marker as result of user selected answers.
38261
-
38262
- function scoreMarker(marker) {
38263
- const score = {
38264
- hasAnswers: false,
38265
- isCorrect: false
38266
- };
38267
- if (marker.selected && marker.selected.length > 0) {
38268
- score.hasAnswers = true;
38269
- }
38270
- if (marker.answers.length > 0) {
38271
- if (marker.selected && marker.selected.length === marker.answers.length) {
38272
- // All correct answers are selected by the user.
38273
- score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
38274
- }
38275
- } else if (!marker.selected || marker.selected.length === 0) {
38276
- // Correct as no answers should be selected by the user.
38277
- score.isCorrect = true;
38278
- }
38279
- return score;
38280
- }
38281
-
38282
- // TODO(LEMS-2440): May need to pull answers out of PerseusLabelImageWidgetOptions[markers] for the rubric
38283
- function labelImageValidator(userInput, rubric) {
38284
- let numAnswered = 0;
38285
- let numCorrect = 0;
38286
- for (const marker of userInput.markers) {
38287
- const score = scoreMarker(marker);
38288
- if (score.hasAnswers) {
38289
- numAnswered++;
38290
- }
38291
- if (score.isCorrect) {
38292
- numCorrect++;
38293
- }
38294
- }
38295
-
38296
- // We expect all question markers to be answered before grading.
38297
- if (numAnswered !== userInput.markers.length) {
38298
- return {
38299
- type: "invalid",
38300
- message: null
38301
- };
38302
- }
38303
- return {
38304
- type: "points",
38305
- // Markers with no expected answers are graded as correct if user
38306
- // makes no answer selection.
38307
- earned: numCorrect === userInput.markers.length ? 1 : 0,
38308
- total: 1,
38309
- message: null
38310
- };
38311
- }
38312
-
38313
38722
  const BringToFront = {
38314
38723
  boxShadow: `0 8px 8px ${wonderBlocksTokens.color.offBlack64}`,
38315
38724
  zIndex: 1000
@@ -38565,6 +38974,59 @@ const styles$b = aphrodite.StyleSheet.create({
38565
38974
  }
38566
38975
  });
38567
38976
 
38977
+ // Question state for marker as result of user selected answers.
38978
+
38979
+ function scoreMarker(marker) {
38980
+ const score = {
38981
+ hasAnswers: false,
38982
+ isCorrect: false
38983
+ };
38984
+ if (marker.selected && marker.selected.length > 0) {
38985
+ score.hasAnswers = true;
38986
+ }
38987
+ if (marker.answers.length > 0) {
38988
+ if (marker.selected && marker.selected.length === marker.answers.length) {
38989
+ // All correct answers are selected by the user.
38990
+ score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
38991
+ }
38992
+ } else if (!marker.selected || marker.selected.length === 0) {
38993
+ // Correct as no answers should be selected by the user.
38994
+ score.isCorrect = true;
38995
+ }
38996
+ return score;
38997
+ }
38998
+
38999
+ // TODO(LEMS-2440): May need to pull answers out of PerseusLabelImageWidgetOptions[markers] for the rubric
39000
+ function scoreLabelImage(userInput, rubric) {
39001
+ let numAnswered = 0;
39002
+ let numCorrect = 0;
39003
+ for (const marker of userInput.markers) {
39004
+ const score = scoreMarker(marker);
39005
+ if (score.hasAnswers) {
39006
+ numAnswered++;
39007
+ }
39008
+ if (score.isCorrect) {
39009
+ numCorrect++;
39010
+ }
39011
+ }
39012
+
39013
+ // We expect all question markers to be answered before grading.
39014
+ if (numAnswered !== userInput.markers.length) {
39015
+ return {
39016
+ type: "invalid",
39017
+ message: null
39018
+ };
39019
+ }
39020
+ return {
39021
+ type: "points",
39022
+ // Markers with no expected answers are graded as correct if user
39023
+ // makes no answer selection.
39024
+ earned: numCorrect === userInput.markers.length ? 1 : 0,
39025
+ total: 1,
39026
+ message: null
39027
+ };
39028
+ }
39029
+
38568
39030
  /**
38569
39031
  * Represents a direction vector.
38570
39032
  * No diagonals allowed.
@@ -38775,6 +39237,9 @@ class LabelImage extends React__namespace.Component {
38775
39237
  markers
38776
39238
  };
38777
39239
  }
39240
+ getPromptJSON() {
39241
+ return getPromptJSON$a(this.props, this.getUserInput());
39242
+ }
38778
39243
 
38779
39244
  // TODO(LEMS-2544): Investigate impact on scoring; possibly pull out &/or remove rubric parameter.
38780
39245
  showRationalesForCurrentlySelectedChoices(rubric) {
@@ -39140,7 +39605,7 @@ var LabelImage$1 = {
39140
39605
  widget: LabelImageWithDependencies,
39141
39606
  accessible: true,
39142
39607
  isLintable: true,
39143
- validator: labelImageValidator
39608
+ scorer: scoreLabelImage
39144
39609
  };
39145
39610
 
39146
39611
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -39850,7 +40315,23 @@ const styles$9 = aphrodite.StyleSheet.create({
39850
40315
  }
39851
40316
  });
39852
40317
 
39853
- function matcherValidator(state, rubric) {
40318
+ const getPromptJSON$9 = (renderProps, userInput) => {
40319
+ return {
40320
+ type: "matcher",
40321
+ options: {
40322
+ labels: renderProps.labels,
40323
+ left: renderProps.left,
40324
+ right: renderProps.right,
40325
+ orderMatters: renderProps.orderMatters
40326
+ },
40327
+ userInput: {
40328
+ left: userInput.left,
40329
+ right: userInput.right
40330
+ }
40331
+ };
40332
+ };
40333
+
40334
+ function scoreMatcher(state, rubric) {
39854
40335
  const correct = ___default["default"].isEqual(state.left, rubric.left) && ___default["default"].isEqual(state.right, rubric.right);
39855
40336
  return {
39856
40337
  type: "points",
@@ -39917,6 +40398,9 @@ class Matcher extends React__namespace.Component {
39917
40398
  right: this.refs.right.getOptions()
39918
40399
  };
39919
40400
  };
40401
+ getPromptJSON() {
40402
+ return getPromptJSON$9(this.props, this.getUserInput());
40403
+ }
39920
40404
 
39921
40405
  // Programatic API for moving options
39922
40406
  // This is used by testing
@@ -40060,10 +40544,23 @@ var Matcher$1 = {
40060
40544
  displayName: "Matcher (two column)",
40061
40545
  widget: Matcher,
40062
40546
  isLintable: true,
40063
- validator: matcherValidator
40547
+ scorer: scoreMatcher
40064
40548
  };
40065
40549
 
40066
- function matrixValidator(userInput, rubric, strings) {
40550
+ const getPromptJSON$8 = (renderProps, userInput) => {
40551
+ return {
40552
+ type: "matrix",
40553
+ options: {
40554
+ height: renderProps.matrixBoardSize[0],
40555
+ width: renderProps.matrixBoardSize[1]
40556
+ },
40557
+ userInput: {
40558
+ answerRows: userInput.answers
40559
+ }
40560
+ };
40561
+ };
40562
+
40563
+ function scoreMatrix(userInput, rubric, strings) {
40067
40564
  const solution = rubric.answers;
40068
40565
  const supplied = userInput.answers;
40069
40566
  const solutionSize = getMatrixSize(solution);
@@ -40340,6 +40837,9 @@ class Matrix extends React__namespace.Component {
40340
40837
  answers: this.props.answers
40341
40838
  };
40342
40839
  }
40840
+ getPromptJSON() {
40841
+ return getPromptJSON$8(this.props, this.getUserInput());
40842
+ }
40343
40843
  render() {
40344
40844
  // Set the input sizes through JS so we can control the size of the
40345
40845
  // brackets. (If we set them in CSS we won't know values until the
@@ -40525,7 +41025,11 @@ var Matrix$1 = {
40525
41025
  transform: propTransform$1,
40526
41026
  staticTransform: staticTransform$2,
40527
41027
  isLintable: true,
40528
- validator: matrixValidator
41028
+ scorer: scoreMatrix
41029
+ };
41030
+
41031
+ const getPromptJSON$7 = () => {
41032
+ return getUnsupportedPromptJSON("measurer");
40529
41033
  };
40530
41034
 
40531
41035
  const defaultImage = {
@@ -40602,6 +41106,9 @@ class Measurer extends React__namespace.Component {
40602
41106
  });
40603
41107
  }
40604
41108
  }
41109
+ getPromptJSON() {
41110
+ return getPromptJSON$7();
41111
+ }
40605
41112
  render() {
40606
41113
  const image = ___default["default"].extend({}, defaultImage, this.props.image);
40607
41114
 
@@ -40649,8 +41156,8 @@ var Measurer$1 = {
40649
41156
  minor: 0
40650
41157
  },
40651
41158
  propUpgrades: propUpgrades,
40652
- // TODO: things that aren't interactive shouldn't need validators
40653
- validator: () => noopValidator(1)
41159
+ // TODO: things that aren't interactive shouldn't need scoring functions
41160
+ scorer: () => scoreNoop(1)
40654
41161
  };
40655
41162
 
40656
41163
  /**
@@ -41539,10 +42046,25 @@ var Molecule$1 = {
41539
42046
  displayName: "Molecule renderer",
41540
42047
  hidden: true,
41541
42048
  widget: MoleculeWidget,
41542
- validator: () => noopValidator()
42049
+ scorer: () => scoreNoop()
42050
+ };
42051
+
42052
+ const getPromptJSON$6 = (renderProps, userInput) => {
42053
+ return {
42054
+ type: "number-line",
42055
+ options: {
42056
+ range: renderProps.range,
42057
+ numDivisions: renderProps.numDivisions,
42058
+ snapDivisions: renderProps.snapDivisions
42059
+ },
42060
+ userInput: {
42061
+ numLinePosition: userInput.numLinePosition,
42062
+ numDivisions: userInput.numDivisions
42063
+ }
42064
+ };
41543
42065
  };
41544
42066
 
41545
- function numberLineValidator(state, rubric) {
42067
+ function scoreNumberLine(state, rubric) {
41546
42068
  const range = rubric.range;
41547
42069
  const divisionRange = state.divisionRange;
41548
42070
  const start = rubric.initialX != null ? rubric.initialX : range[0];
@@ -42031,6 +42553,9 @@ class NumberLine extends React__namespace.Component {
42031
42553
  divisionRange: this.props.divisionRange
42032
42554
  };
42033
42555
  }
42556
+ getPromptJSON() {
42557
+ return getPromptJSON$6(this.props, this.getUserInput());
42558
+ }
42034
42559
  render() {
42035
42560
  const {
42036
42561
  strings
@@ -42135,10 +42660,22 @@ var NumberLine$1 = {
42135
42660
  widget: NumberLine,
42136
42661
  transform: numberLineTransform,
42137
42662
  staticTransform: staticTransform$1,
42138
- validator: numberLineValidator
42663
+ scorer: scoreNumberLine
42139
42664
  };
42140
42665
 
42141
- function ordererValidator(userInput, rubric) {
42666
+ const getPromptJSON$5 = (renderProps, userInput) => {
42667
+ return {
42668
+ type: "orderer",
42669
+ options: {
42670
+ options: renderProps.options.map(option => option.content)
42671
+ },
42672
+ userInput: {
42673
+ values: userInput.current
42674
+ }
42675
+ };
42676
+ };
42677
+
42678
+ function scoreOrderer(userInput, rubric) {
42142
42679
  if (userInput.current.length === 0) {
42143
42680
  return {
42144
42681
  type: "invalid",
@@ -42584,6 +43121,9 @@ class Orderer extends React__namespace.Component {
42584
43121
  })
42585
43122
  };
42586
43123
  }
43124
+ getPromptJSON() {
43125
+ return getPromptJSON$5(this.props, this.getUserInput());
43126
+ }
42587
43127
  render() {
42588
43128
  // This is the card we are currently dragging
42589
43129
  const dragging = this.state.dragging && /*#__PURE__*/React__namespace.createElement(Card
@@ -42694,7 +43234,7 @@ var Orderer$1 = {
42694
43234
  hidden: true,
42695
43235
  widget: Orderer,
42696
43236
  isLintable: true,
42697
- validator: ordererValidator
43237
+ scorer: scoreOrderer
42698
43238
  };
42699
43239
 
42700
43240
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -42748,8 +43288,12 @@ var PassageRefTarget$1 = {
42748
43288
  minor: 0
42749
43289
  },
42750
43290
  isLintable: true,
42751
- // TODO: things that aren't interactive shouldn't need validators
42752
- validator: () => noopValidator()
43291
+ // TODO: things that aren't interactive shouldn't need scoring functions
43292
+ scorer: () => scoreNoop()
43293
+ };
43294
+
43295
+ const getPromptJSON$4 = () => {
43296
+ return getUnsupportedPromptJSON("phet-simulation");
42753
43297
  };
42754
43298
 
42755
43299
  /**
@@ -42801,6 +43345,9 @@ class PhetSimulation extends React__namespace.Component {
42801
43345
  return kaLocale;
42802
43346
  }
42803
43347
  };
43348
+ getPromptJSON() {
43349
+ return getPromptJSON$4();
43350
+ }
42804
43351
  displayLoadFailure = () => {
42805
43352
  this.setState({
42806
43353
  url: null,
@@ -42955,10 +43502,14 @@ var PhetSimulation$1 = {
42955
43502
  isLintable: true
42956
43503
  };
42957
43504
 
43505
+ const getPromptJSON$3 = () => {
43506
+ return getUnsupportedPromptJSON("plotter");
43507
+ };
43508
+
42958
43509
  const {
42959
43510
  deepEq: deepEq$1
42960
43511
  } = Util;
42961
- function plotterValidator(userInput, rubric) {
43512
+ function scorePlotter(userInput, rubric) {
42962
43513
  if (deepEq$1(userInput, rubric.starting)) {
42963
43514
  return {
42964
43515
  type: "invalid",
@@ -43746,6 +44297,9 @@ class Plotter extends React__namespace.Component {
43746
44297
  getUserInput() {
43747
44298
  return this.state.values;
43748
44299
  }
44300
+ getPromptJSON() {
44301
+ return getPromptJSON$3();
44302
+ }
43749
44303
  render() {
43750
44304
  // TODO(kevinb) actually compute the size of the graphie correctly and
43751
44305
  // make it that size so we don't have to add extra padding. The value
@@ -43772,7 +44326,11 @@ var Plotter$1 = {
43772
44326
  hidden: true,
43773
44327
  widget: Plotter,
43774
44328
  staticTransform: staticTransform,
43775
- validator: plotterValidator
44329
+ scorer: scorePlotter
44330
+ };
44331
+
44332
+ const getPromptJSON$2 = () => {
44333
+ return getUnsupportedPromptJSON("python-program");
43776
44334
  };
43777
44335
 
43778
44336
  /**
@@ -43798,6 +44356,9 @@ class PythonProgram extends React__namespace.Component {
43798
44356
  getDOMNodeForPath(path) {
43799
44357
  return null;
43800
44358
  }
44359
+ getPromptJSON() {
44360
+ return getPromptJSON$2();
44361
+ }
43801
44362
  render() {
43802
44363
  const url = getUrlFromProgramID(this.props.programID);
43803
44364
  const iframeStyle = {
@@ -43832,7 +44393,17 @@ var PythonProgram$1 = {
43832
44393
  widget: PythonProgram
43833
44394
  };
43834
44395
 
43835
- function sorterValidator(userInput, rubric) {
44396
+ const getPromptJSON$1 = userInput => {
44397
+ return {
44398
+ type: "sorter",
44399
+ userInput: {
44400
+ values: userInput.options,
44401
+ changed: userInput.changed
44402
+ }
44403
+ };
44404
+ };
44405
+
44406
+ function scoreSorter(userInput, rubric) {
43836
44407
  // If the sorter widget hasn't been changed yet, we treat it as "empty" which
43837
44408
  // prevents the "Check" button from becoming active. We want the user
43838
44409
  // to make a change before trying to move forward. This makes an
@@ -43904,6 +44475,9 @@ class Sorter extends React__namespace.Component {
43904
44475
  changed: this.state.changed
43905
44476
  };
43906
44477
  }
44478
+ getPromptJSON() {
44479
+ return getPromptJSON$1(this.getUserInput());
44480
+ }
43907
44481
  moveOptionToIndex = (option, index) => {
43908
44482
  // eslint-disable-next-line react/no-string-refs
43909
44483
  // @ts-expect-error - TS2339 - Property 'moveOptionToIndex' does not exist on type 'ReactInstance'.
@@ -43935,20 +44509,26 @@ var Sorter$1 = {
43935
44509
  displayName: "Sorter",
43936
44510
  widget: Sorter,
43937
44511
  isLintable: true,
43938
- validator: sorterValidator
44512
+ scorer: scoreSorter
43939
44513
  };
43940
44514
 
43941
- function tableValidator(state, rubric, strings) {
43942
- const filterNonEmpty = function (table) {
43943
- return ___default["default"].filter(table, function (row) {
43944
- // Check if row has a cell that is nonempty
43945
- return ___default["default"].some(row, ___default["default"].identity);
43946
- });
43947
- };
43948
- const solution = filterNonEmpty(rubric.answers);
43949
- const supplied = filterNonEmpty(state);
43950
- const hasEmptyCell = ___default["default"].some(supplied, function (row) {
43951
- return ___default["default"].some(row, function (cell) {
44515
+ /**
44516
+ * Filters the given table (modelled as a 2D array) to remove any rows that are
44517
+ * completely empty.
44518
+ *
44519
+ * @returns A new table with only non-empty rows.
44520
+ */
44521
+ const filterNonEmpty = function (table) {
44522
+ return table.filter(function (row) {
44523
+ // Return only rows that are non-empty.
44524
+ return row.some(cell => cell);
44525
+ });
44526
+ };
44527
+
44528
+ function validateTable(userInput) {
44529
+ const supplied = filterNonEmpty(userInput);
44530
+ const hasEmptyCell = supplied.some(function (row) {
44531
+ return row.some(function (cell) {
43952
44532
  return cell === "";
43953
44533
  });
43954
44534
  });
@@ -43958,6 +44538,16 @@ function tableValidator(state, rubric, strings) {
43958
44538
  message: null
43959
44539
  };
43960
44540
  }
44541
+ return null;
44542
+ }
44543
+
44544
+ function scoreTable(userInput, rubric, strings) {
44545
+ const validationResult = validateTable(userInput);
44546
+ if (validationResult != null) {
44547
+ return validationResult;
44548
+ }
44549
+ const supplied = filterNonEmpty(userInput);
44550
+ const solution = filterNonEmpty(rubric.answers);
43961
44551
  if (supplied.length !== solution.length) {
43962
44552
  return {
43963
44553
  type: "points",
@@ -43968,17 +44558,16 @@ function tableValidator(state, rubric, strings) {
43968
44558
  }
43969
44559
  const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
43970
44560
  let message = null;
43971
- const allCorrect = ___default["default"].every(solution, function (rowSolution) {
44561
+ const allCorrect = solution.every(function (rowSolution) {
43972
44562
  for (let i = 0; i < supplied.length; i++) {
43973
44563
  const rowSupplied = supplied[i];
43974
- const correct = ___default["default"].every(rowSupplied, function (cellSupplied, i) {
44564
+ const correct = rowSupplied.every(function (cellSupplied, i) {
43975
44565
  const cellSolution = rowSolution[i];
43976
44566
  const validator = createValidator(cellSolution, {
43977
44567
  simplify: true
43978
44568
  }, strings);
43979
44569
  const result = validator(cellSupplied);
43980
44570
  if (result.message) {
43981
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
43982
44571
  message = result.message;
43983
44572
  }
43984
44573
  return result.correct;
@@ -43994,7 +44583,7 @@ function tableValidator(state, rubric, strings) {
43994
44583
  type: "points",
43995
44584
  earned: allCorrect ? 1 : 0,
43996
44585
  total: 1,
43997
- message: message
44586
+ message
43998
44587
  };
43999
44588
  }
44000
44589
 
@@ -44212,7 +44801,11 @@ var Table$1 = {
44212
44801
  transform: propTransform,
44213
44802
  hidden: true,
44214
44803
  isLintable: true,
44215
- validator: tableValidator
44804
+ scorer: scoreTable
44805
+ };
44806
+
44807
+ const getPromptJSON = () => {
44808
+ return getUnsupportedPromptJSON("video");
44216
44809
  };
44217
44810
 
44218
44811
  /**
@@ -44311,6 +44904,9 @@ class Video extends React__namespace.Component {
44311
44904
  return change.apply(_this, args);
44312
44905
  };
44313
44906
  })();
44907
+ getPromptJSON() {
44908
+ return getPromptJSON();
44909
+ }
44314
44910
  render() {
44315
44911
  const {
44316
44912
  InitialRequestUrl
@@ -44370,8 +44966,8 @@ var Video$1 = {
44370
44966
  defaultAlignment: "block",
44371
44967
  supportedAlignments: ["block", "float-left", "float-right", "full-width"],
44372
44968
  widget: Video,
44373
- // TODO: things that aren't interactive shouldn't need validators
44374
- validator: () => noopValidator()
44969
+ // TODO: things that aren't interactive shouldn't need scoring functions
44970
+ scorer: () => scoreNoop()
44375
44971
  };
44376
44972
 
44377
44973
  /* eslint-disable import/no-named-as-default */
@@ -44390,7 +44986,7 @@ const init = function () {
44390
44986
 
44391
44987
  // This file is processed by a Rollup plugin (replace) to inject the production
44392
44988
  const libName = "@khanacademy/perseus";
44393
- const libVersion = "41.2.0";
44989
+ const libVersion = "41.4.0";
44394
44990
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
44395
44991
 
44396
44992
  const apiVersion = {
@@ -45126,6 +45722,9 @@ class ServerItemRenderer extends React__namespace.Component {
45126
45722
  getNumHints() {
45127
45723
  return this.props.item.hints.length;
45128
45724
  }
45725
+ getPromptJSON() {
45726
+ return this.questionRenderer.getPromptJSON();
45727
+ }
45129
45728
 
45130
45729
  /**
45131
45730
  * Grades the item.
@@ -45783,7 +46382,7 @@ function treeToItem(node) {
45783
46382
  };
45784
46383
  }
45785
46384
 
45786
- var isObject = function (obj) {
46385
+ var isObject$1 = function (obj) {
45787
46386
  return obj === Object(obj);
45788
46387
  };
45789
46388
  var merge = function () {
@@ -45799,7 +46398,7 @@ var merge = function () {
45799
46398
  return obj;
45800
46399
  };
45801
46400
  var clone = function (obj) {
45802
- if (!isObject(obj)) {
46401
+ if (!isObject$1(obj)) {
45803
46402
  return obj;
45804
46403
  }
45805
46404
  return Array.isArray(obj) ? obj.slice() : merge(obj);
@@ -45815,7 +46414,7 @@ var clone = function (obj) {
45815
46414
  var dispatch = function (x) {
45816
46415
  if (Array.isArray(x)) {
45817
46416
  return arr;
45818
- } else if (isObject(x)) {
46417
+ } else if (isObject$1(x)) {
45819
46418
  return obj;
45820
46419
  } else if (typeof x === "string") {
45821
46420
  return str;
@@ -47739,6 +48338,697 @@ function buildTestData(testObject) {
47739
48338
  return `{"data":{"assessmentItem":{"__typename":"AssessmentItemOrError","error":null,"item":{"__typename":"AssessmentItem","id":"x890b3c70f3e8f4a6","itemData":"${testObject}","problemType":"Type 1","sha":"c7284a3ad65214b4e62bccce236d92f7f5d35941"}}}}`;
47740
48339
  }
47741
48340
 
48341
+ function success(value) {
48342
+ return {
48343
+ type: "success",
48344
+ value
48345
+ };
48346
+ }
48347
+ function failure(detail) {
48348
+ return {
48349
+ type: "failure",
48350
+ detail
48351
+ };
48352
+ }
48353
+ function isFailure(result) {
48354
+ return result.type === "failure";
48355
+ }
48356
+ function isSuccess(result) {
48357
+ return result.type === "success";
48358
+ }
48359
+
48360
+ // Result's `all` function is similar to Promise.all: given an array of
48361
+ // results, it returns success if all succeeded, and failure if any failed.
48362
+ function all(results) {
48363
+ let combineFailureDetails = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : a => a;
48364
+ const values = [];
48365
+ const failureDetails = [];
48366
+ for (const result of results) {
48367
+ if (result.type === "success") {
48368
+ values.push(result.value);
48369
+ } else {
48370
+ failureDetails.push(result.detail);
48371
+ }
48372
+ }
48373
+ if (failureDetails.length > 0) {
48374
+ return failure(failureDetails.reduce(combineFailureDetails));
48375
+ }
48376
+ return success(values);
48377
+ }
48378
+
48379
+ const any = (rawValue, ctx) => ctx.success(rawValue);
48380
+
48381
+ function array(elementParser) {
48382
+ return (rawValue, ctx) => {
48383
+ if (!Array.isArray(rawValue)) {
48384
+ return ctx.failure("array", rawValue);
48385
+ }
48386
+ const elementResults = rawValue.map((elem, i) => elementParser(elem, ctx.forSubtree(i)));
48387
+ return all(elementResults, concat);
48388
+ };
48389
+ }
48390
+ function concat(a, b) {
48391
+ return [...a, ...b];
48392
+ }
48393
+
48394
+ function boolean(rawValue, ctx) {
48395
+ if (typeof rawValue === "boolean") {
48396
+ return ctx.success(rawValue);
48397
+ }
48398
+ return ctx.failure("boolean", rawValue);
48399
+ }
48400
+
48401
+ function constant(acceptedValue) {
48402
+ return (rawValue, ctx) => {
48403
+ if (rawValue !== acceptedValue) {
48404
+ return ctx.failure(String(JSON.stringify(acceptedValue)), rawValue);
48405
+ }
48406
+ return ctx.success(acceptedValue);
48407
+ };
48408
+ }
48409
+
48410
+ function enumeration() {
48411
+ for (var _len = arguments.length, acceptedValues = new Array(_len), _key = 0; _key < _len; _key++) {
48412
+ acceptedValues[_key] = arguments[_key];
48413
+ }
48414
+ return (rawValue, ctx) => {
48415
+ if (typeof rawValue === "string") {
48416
+ const index = acceptedValues.indexOf(rawValue);
48417
+ if (index > -1) {
48418
+ return ctx.success(acceptedValues[index]);
48419
+ }
48420
+ }
48421
+ const expected = acceptedValues.map(v => JSON.stringify(v));
48422
+ return ctx.failure(expected, rawValue);
48423
+ };
48424
+ }
48425
+
48426
+ function isObject(x) {
48427
+ return x != null && Object.getPrototypeOf(x) === Object.prototype;
48428
+ }
48429
+
48430
+ function nullable(parseValue) {
48431
+ return (rawValue, ctx) => {
48432
+ if (rawValue === null) {
48433
+ return ctx.success(rawValue);
48434
+ }
48435
+ return parseValue(rawValue, ctx);
48436
+ };
48437
+ }
48438
+
48439
+ const number = (rawValue, ctx) => {
48440
+ if (typeof rawValue === "number") {
48441
+ return ctx.success(rawValue);
48442
+ }
48443
+ return ctx.failure("number", rawValue);
48444
+ };
48445
+
48446
+ function object(schema) {
48447
+ return (rawValue, ctx) => {
48448
+ if (!isObject(rawValue)) {
48449
+ return ctx.failure("object", rawValue);
48450
+ }
48451
+ const ret = {
48452
+ ...rawValue
48453
+ };
48454
+ const mismatches = [];
48455
+ for (const [prop, propParser] of Object.entries(schema)) {
48456
+ const result = propParser(rawValue[prop], ctx.forSubtree(prop));
48457
+ if (isSuccess(result)) {
48458
+ ret[prop] = result.value;
48459
+ } else {
48460
+ mismatches.push(...result.detail);
48461
+ }
48462
+ }
48463
+ if (mismatches.length > 0) {
48464
+ return failure(mismatches);
48465
+ }
48466
+ return ctx.success(ret);
48467
+ };
48468
+ }
48469
+
48470
+ function optional(parseValue) {
48471
+ return (rawValue, ctx) => {
48472
+ if (rawValue === undefined) {
48473
+ return ctx.success(rawValue);
48474
+ }
48475
+ return parseValue(rawValue, ctx);
48476
+ };
48477
+ }
48478
+
48479
+ function pair(parseA, parseB) {
48480
+ return (rawValue, ctx) => {
48481
+ if (!Array.isArray(rawValue)) {
48482
+ return ctx.failure("array", rawValue);
48483
+ }
48484
+ if (rawValue.length !== 2) {
48485
+ return ctx.failure("array of length 2", rawValue);
48486
+ }
48487
+ const [rawA, rawB] = rawValue;
48488
+ const resultA = parseA(rawA, ctx.forSubtree(0));
48489
+ if (isFailure(resultA)) {
48490
+ return resultA;
48491
+ }
48492
+ const resultB = parseB(rawB, ctx.forSubtree(1));
48493
+ if (isFailure(resultB)) {
48494
+ return resultB;
48495
+ }
48496
+ return ctx.success([resultA.value, resultB.value]);
48497
+ };
48498
+ }
48499
+
48500
+ function record(parseKey, parseValue) {
48501
+ return (rawValue, ctx) => {
48502
+ if (!isObject(rawValue)) {
48503
+ return ctx.failure("object", rawValue);
48504
+ }
48505
+ const result = {};
48506
+ const mismatches = [];
48507
+ for (const [key, value] of Object.entries(rawValue)) {
48508
+ const entryCtx = ctx.forSubtree(key);
48509
+ const keyResult = parseKey(key, entryCtx);
48510
+ if (isFailure(keyResult)) {
48511
+ mismatches.push(...keyResult.detail);
48512
+ }
48513
+ const valueResult = parseValue(value, entryCtx);
48514
+ if (isFailure(valueResult)) {
48515
+ mismatches.push(...valueResult.detail);
48516
+ }
48517
+ if (isSuccess(keyResult) && isSuccess(valueResult)) {
48518
+ result[keyResult.value] = valueResult.value;
48519
+ }
48520
+ }
48521
+ if (mismatches.length > 0) {
48522
+ return failure(mismatches);
48523
+ }
48524
+ return ctx.success(result);
48525
+ };
48526
+ }
48527
+
48528
+ const string = (rawValue, ctx) => {
48529
+ if (typeof rawValue === "string") {
48530
+ return ctx.success(rawValue);
48531
+ }
48532
+ return ctx.failure("string", rawValue);
48533
+ };
48534
+
48535
+ function trio(parseA, parseB, parseC) {
48536
+ return (rawValue, ctx) => {
48537
+ if (!Array.isArray(rawValue)) {
48538
+ return ctx.failure("array", rawValue);
48539
+ }
48540
+ if (rawValue.length !== 3) {
48541
+ return ctx.failure("array of length 3", rawValue);
48542
+ }
48543
+ const resultA = parseA(rawValue[0], ctx.forSubtree(0));
48544
+ if (isFailure(resultA)) {
48545
+ return resultA;
48546
+ }
48547
+ const resultB = parseB(rawValue[1], ctx.forSubtree(1));
48548
+ if (isFailure(resultB)) {
48549
+ return resultB;
48550
+ }
48551
+ const resultC = parseC(rawValue[2], ctx.forSubtree(2));
48552
+ if (isFailure(resultC)) {
48553
+ return resultC;
48554
+ }
48555
+ return ctx.success([resultA.value, resultB.value, resultC.value]);
48556
+ };
48557
+ }
48558
+
48559
+ function union(parseBranch) {
48560
+ return new UnionBuilder(parseBranch);
48561
+ }
48562
+ class UnionBuilder {
48563
+ constructor(parser) {
48564
+ this.parser = parser;
48565
+ }
48566
+ or(newBranch) {
48567
+ return new UnionBuilder(either(this.parser, newBranch));
48568
+ }
48569
+ }
48570
+ function either(parseA, parseB) {
48571
+ return (rawValue, ctx) => {
48572
+ const resultA = parseA(rawValue, ctx);
48573
+ if (isSuccess(resultA)) {
48574
+ return resultA;
48575
+ }
48576
+ return parseB(rawValue, ctx);
48577
+ };
48578
+ }
48579
+
48580
+ function parseWidget(parseType, parseOptions) {
48581
+ return object({
48582
+ type: parseType,
48583
+ static: optional(boolean),
48584
+ graded: optional(boolean),
48585
+ alignment: optional(string),
48586
+ options: parseOptions,
48587
+ key: optional(number),
48588
+ version: object({
48589
+ major: number,
48590
+ minor: number
48591
+ })
48592
+ });
48593
+ }
48594
+
48595
+ const parseCategorizerWidget = parseWidget(constant("categorizer"), object({
48596
+ items: array(string),
48597
+ categories: array(string),
48598
+ randomizeItems: boolean,
48599
+ static: boolean,
48600
+ values: array(number),
48601
+ highlightLint: optional(boolean),
48602
+ linterContext: optional(object({
48603
+ contentType: string,
48604
+ paths: array(string),
48605
+ stack: array(string)
48606
+ }))
48607
+ }));
48608
+
48609
+ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
48610
+ programID: string,
48611
+ programType: any,
48612
+ settings: array(object({
48613
+ name: string,
48614
+ value: string
48615
+ })),
48616
+ showEditor: boolean,
48617
+ showButtons: boolean,
48618
+ width: number,
48619
+ height: number,
48620
+ static: boolean
48621
+ }));
48622
+
48623
+ const parseDefinitionWidget = parseWidget(constant("definition"), object({
48624
+ togglePrompt: string,
48625
+ definition: string,
48626
+ static: boolean
48627
+ }));
48628
+
48629
+ const parseDropdownWidget = parseWidget(constant("dropdown"), object({
48630
+ placeholder: string,
48631
+ static: boolean,
48632
+ choices: array(object({
48633
+ content: string,
48634
+ correct: boolean
48635
+ }))
48636
+ }));
48637
+
48638
+ const parseExplanationWidget = parseWidget(constant("explanation"), object({
48639
+ showPrompt: string,
48640
+ hidePrompt: string,
48641
+ explanation: string,
48642
+ // We wrap parseWidgetsMap in a function here to make sure it is not
48643
+ // referenced before it is defined. There is an import cycle between
48644
+ // this file and widgets-map.ts that could cause it to be undefined.
48645
+ widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
48646
+ static: boolean
48647
+ }));
48648
+
48649
+ const parseAnswerForm = object({
48650
+ value: string,
48651
+ form: boolean,
48652
+ simplify: boolean,
48653
+ considered: enumeration("correct", "wrong", "ungraded"),
48654
+ key: optional(string)
48655
+ });
48656
+ const parseExpressionWidget = parseWidget(constant("expression"), object({
48657
+ answerForms: array(parseAnswerForm),
48658
+ functions: array(string),
48659
+ times: boolean,
48660
+ visibleLabel: optional(string),
48661
+ ariaLabel: optional(string),
48662
+ buttonSets: array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")),
48663
+ buttonsVisible: optional(enumeration("always", "never", "focused"))
48664
+ }));
48665
+
48666
+ const parsePerseusImageBackground = object({
48667
+ url: optional(nullable(string)),
48668
+ width: optional(number),
48669
+ height: optional(number),
48670
+ top: optional(number),
48671
+ left: optional(number),
48672
+ bottom: optional(number),
48673
+ // TODO(benchristel): convert scale to a number
48674
+ scale: optional(union(number).or(string).parser)
48675
+ });
48676
+
48677
+ // Used to represent 2-D points and ranges
48678
+ const pairOfNumbers = pair(number, number);
48679
+ const parsePerseusGraphTypeAngle = object({
48680
+ type: constant("angle"),
48681
+ showAngles: optional(boolean),
48682
+ allowReflexAngles: optional(boolean),
48683
+ angleOffsetDeg: optional(number),
48684
+ snapDegrees: optional(number),
48685
+ match: optional(constant("congruent")),
48686
+ coords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
48687
+ startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))
48688
+ });
48689
+ const parsePerseusGraphTypeCircle = object({
48690
+ type: constant("circle"),
48691
+ center: optional(pairOfNumbers),
48692
+ radius: optional(number),
48693
+ startCoords: optional(object({
48694
+ center: pairOfNumbers,
48695
+ radius: number
48696
+ })),
48697
+ // TODO: remove coord? it's legacy.
48698
+ coord: optional(pairOfNumbers)
48699
+ });
48700
+ const parsePerseusGraphTypeLinear = object({
48701
+ type: constant("linear"),
48702
+ coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
48703
+ startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
48704
+ // TODO: remove coord? it's legacy.
48705
+ coord: optional(pairOfNumbers)
48706
+ });
48707
+ const parsePerseusGraphTypeLinearSystem = object({
48708
+ type: constant("linear-system"),
48709
+ // TODO(benchristel): default coords to empty array?
48710
+ coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
48711
+ startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
48712
+ // TODO: remove coord? it's legacy.
48713
+ coord: optional(pairOfNumbers)
48714
+ });
48715
+ const parsePerseusGraphTypeNone = object({
48716
+ type: constant("none")
48717
+ });
48718
+ const parsePerseusGraphTypePoint = object({
48719
+ type: constant("point"),
48720
+ numPoints: optional(union(number).or(constant("unlimited")).parser),
48721
+ coords: optional(nullable(array(pairOfNumbers))),
48722
+ startCoords: optional(array(pairOfNumbers)),
48723
+ // TODO: remove coord? it's legacy.
48724
+ coord: optional(pairOfNumbers)
48725
+ });
48726
+ const parsePerseusGraphTypePolygon = object({
48727
+ type: constant("polygon"),
48728
+ numSides: optional(union(number).or(constant("unlimited")).parser),
48729
+ showAngles: optional(boolean),
48730
+ showSides: optional(boolean),
48731
+ snapTo: optional(enumeration("grid", "angles", "sides")),
48732
+ match: optional(enumeration("similar", "congruent", "approx")),
48733
+ startCoords: optional(array(pairOfNumbers)),
48734
+ // TODO: remove coord? it's legacy.
48735
+ coord: optional(pairOfNumbers)
48736
+ });
48737
+ const parsePerseusGraphTypeQuadratic = object({
48738
+ type: constant("quadratic"),
48739
+ coords: optional(nullable(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))),
48740
+ startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
48741
+ // TODO: remove coord? it's legacy.
48742
+ coord: optional(pairOfNumbers)
48743
+ });
48744
+ const parsePerseusGraphTypeRay = object({
48745
+ type: constant("ray"),
48746
+ coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
48747
+ startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
48748
+ // TODO: remove coord? it's legacy.
48749
+ coord: optional(pairOfNumbers)
48750
+ });
48751
+ const parsePerseusGraphTypeSegment = object({
48752
+ type: constant("segment"),
48753
+ // TODO(benchristel): default numSegments?
48754
+ numSegments: optional(number),
48755
+ coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
48756
+ startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
48757
+ // TODO: remove coord? it's legacy.
48758
+ coord: optional(pairOfNumbers)
48759
+ });
48760
+ const parsePerseusGraphTypeSinusoid = object({
48761
+ type: constant("sinusoid"),
48762
+ coords: optional(nullable(array(pairOfNumbers))),
48763
+ startCoords: optional(array(pairOfNumbers)),
48764
+ // TODO: remove coord? it's legacy.
48765
+ coord: optional(pairOfNumbers)
48766
+ });
48767
+ 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;
48768
+ const parseLockedFigureColor = enumeration(...lockedFigureColorNames);
48769
+ const parseLockedFigureFillType = enumeration("none", "white", "translucent", "solid");
48770
+ const parseLockedLineStyle = enumeration("solid", "dashed");
48771
+ const parseLockedLabelType = object({
48772
+ type: constant("label"),
48773
+ coord: pairOfNumbers,
48774
+ text: string,
48775
+ color: parseLockedFigureColor,
48776
+ size: enumeration("small", "medium", "large")
48777
+ });
48778
+ const parseLockedPointType = object({
48779
+ type: constant("point"),
48780
+ coord: pairOfNumbers,
48781
+ color: parseLockedFigureColor,
48782
+ filled: boolean,
48783
+ // TODO(benchristel): default labels to empty array?
48784
+ labels: optional(array(parseLockedLabelType)),
48785
+ ariaLabel: optional(string)
48786
+ });
48787
+ const parseLockedLineType = object({
48788
+ type: constant("line"),
48789
+ kind: enumeration("line", "ray", "segment"),
48790
+ points: pair(parseLockedPointType, parseLockedPointType),
48791
+ color: parseLockedFigureColor,
48792
+ lineStyle: parseLockedLineStyle,
48793
+ showPoint1: boolean,
48794
+ showPoint2: boolean,
48795
+ // TODO(benchristel): default labels to empty array?
48796
+ labels: optional(array(parseLockedLabelType)),
48797
+ ariaLabel: optional(string)
48798
+ });
48799
+ const parseLockedVectorType = object({
48800
+ type: constant("vector"),
48801
+ points: pair(pairOfNumbers, pairOfNumbers),
48802
+ color: parseLockedFigureColor,
48803
+ // TODO(benchristel): default labels to empty array?
48804
+ labels: optional(array(parseLockedLabelType)),
48805
+ ariaLabel: optional(string)
48806
+ });
48807
+ const parseLockedEllipseType = object({
48808
+ type: constant("ellipse"),
48809
+ center: pairOfNumbers,
48810
+ radius: pairOfNumbers,
48811
+ angle: number,
48812
+ color: parseLockedFigureColor,
48813
+ fillStyle: parseLockedFigureFillType,
48814
+ strokeStyle: parseLockedLineStyle,
48815
+ // TODO(benchristel): default labels to empty array?
48816
+ labels: optional(array(parseLockedLabelType)),
48817
+ ariaLabel: optional(string)
48818
+ });
48819
+ const parseLockedPolygonType = object({
48820
+ type: constant("polygon"),
48821
+ points: array(pairOfNumbers),
48822
+ color: parseLockedFigureColor,
48823
+ showVertices: boolean,
48824
+ fillStyle: parseLockedFigureFillType,
48825
+ strokeStyle: parseLockedLineStyle,
48826
+ // TODO(benchristel): default labels to empty array?
48827
+ labels: optional(array(parseLockedLabelType)),
48828
+ ariaLabel: optional(string)
48829
+ });
48830
+ const parseLockedFunctionType = object({
48831
+ type: constant("function"),
48832
+ color: parseLockedFigureColor,
48833
+ strokeStyle: parseLockedLineStyle,
48834
+ equation: string,
48835
+ directionalAxis: enumeration("x", "y"),
48836
+ domain: optional(pairOfNumbers),
48837
+ // TODO(benchristel): default labels to empty array?
48838
+ labels: optional(array(parseLockedLabelType)),
48839
+ ariaLabel: optional(string)
48840
+ });
48841
+ const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
48842
+ const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
48843
+ step: pairOfNumbers,
48844
+ gridStep: pairOfNumbers,
48845
+ snapStep: pairOfNumbers,
48846
+ backgroundImage: optional(parsePerseusImageBackground),
48847
+ markings: enumeration("graph", "grid", "none"),
48848
+ labels: array(string),
48849
+ showProtractor: boolean,
48850
+ showRuler: optional(boolean),
48851
+ showTooltips: optional(boolean),
48852
+ rulerLabel: optional(string),
48853
+ rulerTicks: optional(number),
48854
+ range: pair(pairOfNumbers, pairOfNumbers),
48855
+ graph: parsePerseusGraphType,
48856
+ correct: parsePerseusGraphType,
48857
+ // TODO(benchristel): default lockedFigures to empty array
48858
+ lockedFigures: optional(array(parseLockedFigure)),
48859
+ fullGraphLabel: optional(string),
48860
+ fullGraphAriaDescription: optional(string)
48861
+ }));
48862
+
48863
+ const parseWidgetsMap = (rawValue, ctx) => {
48864
+ if (!isObject(rawValue)) {
48865
+ return ctx.failure("PerseusWidgetsMap", rawValue);
48866
+ }
48867
+ const widgetsMap = {};
48868
+ for (const key of Object.keys(rawValue)) {
48869
+ // parseWidgetsMapEntry modifies the widgetsMap. This is kind of gross,
48870
+ // but it's the only way I could find to make TypeScript check the key
48871
+ // against the widget type.
48872
+ const entryResult = parseWidgetsMapEntry([key, rawValue[key]], widgetsMap, ctx.forSubtree(key));
48873
+ if (isFailure(entryResult)) {
48874
+ return entryResult;
48875
+ }
48876
+ }
48877
+ return ctx.success(widgetsMap);
48878
+ };
48879
+ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
48880
+ let [key, widget] = _ref;
48881
+ const keyComponentsResult = parseWidgetMapKeyComponents(key.split(" "), ctx);
48882
+ if (isFailure(keyComponentsResult)) {
48883
+ return keyComponentsResult;
48884
+ }
48885
+ const [type, id] = keyComponentsResult.value;
48886
+ function parseAndAssign(key, parse) {
48887
+ const widgetResult = parse(widget, ctx);
48888
+ if (isFailure(widgetResult)) {
48889
+ return widgetResult;
48890
+ }
48891
+ widgetMap[key] = widgetResult.value;
48892
+ return ctx.success(undefined);
48893
+ }
48894
+ switch (type) {
48895
+ case "categorizer":
48896
+ return parseAndAssign(`categorizer ${id}`, parseCategorizerWidget);
48897
+ case "cs-program":
48898
+ return parseAndAssign(`cs-program ${id}`, parseCSProgramWidget);
48899
+ case "definition":
48900
+ return parseAndAssign(`definition ${id}`, parseDefinitionWidget);
48901
+ case "dropdown":
48902
+ return parseAndAssign(`dropdown ${id}`, parseDropdownWidget);
48903
+ case "explanation":
48904
+ return parseAndAssign(`explanation ${id}`, parseExplanationWidget);
48905
+ case "expression":
48906
+ return parseAndAssign(`expression ${id}`, parseExpressionWidget);
48907
+ case "grapher":
48908
+ // TODO(LEMS-2585): implement a real parser for this widget
48909
+ return parseAndAssign(`grapher ${id}`, any);
48910
+ case "group":
48911
+ // TODO(LEMS-2585): implement a real parser for this widget
48912
+ return parseAndAssign(`group ${id}`, any);
48913
+ case "graded-group":
48914
+ // TODO(LEMS-2585): implement a real parser for this widget
48915
+ return parseAndAssign(`graded-group ${id}`, any);
48916
+ case "graded-group-set":
48917
+ // TODO(LEMS-2585): implement a real parser for this widget
48918
+ return parseAndAssign(`graded-group-set ${id}`, any);
48919
+ case "iframe":
48920
+ // TODO(LEMS-2585): implement a real parser for this widget
48921
+ return parseAndAssign(`iframe ${id}`, any);
48922
+ case "image":
48923
+ // TODO(LEMS-2585): implement a real parser for this widget
48924
+ return parseAndAssign(`image ${id}`, any);
48925
+ case "input-number":
48926
+ // TODO(LEMS-2585): implement a real parser for this widget
48927
+ return parseAndAssign(`input-number ${id}`, any);
48928
+ case "interaction":
48929
+ // TODO(LEMS-2585): implement a real parser for this widget
48930
+ return parseAndAssign(`interaction ${id}`, any);
48931
+ case "interactive-graph":
48932
+ return parseAndAssign(`interactive-graph ${id}`, parseInteractiveGraphWidget);
48933
+ case "label-image":
48934
+ // TODO(LEMS-2585): implement a real parser for this widget
48935
+ return parseAndAssign(`label-image ${id}`, any);
48936
+ case "matcher":
48937
+ // TODO(LEMS-2585): implement a real parser for this widget
48938
+ return parseAndAssign(`matcher ${id}`, any);
48939
+ case "matrix":
48940
+ // TODO(LEMS-2585): implement a real parser for this widget
48941
+ return parseAndAssign(`matrix ${id}`, any);
48942
+ case "measurer":
48943
+ // TODO(LEMS-2585): implement a real parser for this widget
48944
+ return parseAndAssign(`measurer ${id}`, any);
48945
+ case "molecule-renderer":
48946
+ // TODO(LEMS-2585): implement a real parser for this widget
48947
+ return parseAndAssign(`molecule-renderer ${id}`, any);
48948
+ case "number-line":
48949
+ // TODO(LEMS-2585): implement a real parser for this widget
48950
+ return parseAndAssign(`number-line ${id}`, any);
48951
+ case "numeric-input":
48952
+ // TODO(LEMS-2585): implement a real parser for this widget
48953
+ return parseAndAssign(`numeric-input ${id}`, any);
48954
+ case "orderer":
48955
+ // TODO(LEMS-2585): implement a real parser for this widget
48956
+ return parseAndAssign(`orderer ${id}`, any);
48957
+ case "passage":
48958
+ // TODO(LEMS-2585): implement a real parser for this widget
48959
+ return parseAndAssign(`passage ${id}`, any);
48960
+ case "passage-ref":
48961
+ // TODO(LEMS-2585): implement a real parser for this widget
48962
+ return parseAndAssign(`passage-ref ${id}`, any);
48963
+ case "passage-ref-target":
48964
+ // TODO(LEMS-2585): implement a real parser for this widget
48965
+ return parseAndAssign(`passage-ref-target ${id}`, any);
48966
+ case "phet-simulation":
48967
+ // TODO(LEMS-2585): implement a real parser for this widget
48968
+ return parseAndAssign(`phet-simulation ${id}`, any);
48969
+ case "plotter":
48970
+ // TODO(LEMS-2585): implement a real parser for this widget
48971
+ return parseAndAssign(`plotter ${id}`, any);
48972
+ case "python-program":
48973
+ // TODO(LEMS-2585): implement a real parser for this widget
48974
+ return parseAndAssign(`python-program ${id}`, any);
48975
+ case "radio":
48976
+ // TODO(LEMS-2585): implement a real parser for this widget
48977
+ return parseAndAssign(`radio ${id}`, any);
48978
+ case "sorter":
48979
+ // TODO(LEMS-2585): implement a real parser for this widget
48980
+ return parseAndAssign(`sorter ${id}`, any);
48981
+ case "table":
48982
+ // TODO(LEMS-2585): implement a real parser for this widget
48983
+ return parseAndAssign(`table ${id}`, any);
48984
+ case "video":
48985
+ // TODO(LEMS-2585): implement a real parser for this widget
48986
+ return parseAndAssign(`video ${id}`, any);
48987
+ default:
48988
+ return ctx.failure("a valid widget type", type);
48989
+ }
48990
+ };
48991
+ const parseStringToPositiveInt = (rawValue, ctx) => {
48992
+ if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
48993
+ return ctx.failure("numeric string", rawValue);
48994
+ }
48995
+ return ctx.success(+rawValue);
48996
+ };
48997
+ const parseWidgetMapKeyComponents = pair(string, parseStringToPositiveInt);
48998
+
48999
+ const parseHint = object({
49000
+ replace: optional(boolean),
49001
+ content: string,
49002
+ widgets: parseWidgetsMap,
49003
+ metadata: optional(array(string)),
49004
+ images: record(string, object({
49005
+ width: number,
49006
+ height: number
49007
+ }))
49008
+ });
49009
+
49010
+ const parsePerseusRenderer = object({
49011
+ content: string,
49012
+ widgets: parseWidgetsMap,
49013
+ metadata: optional(array(string)),
49014
+ images: record(string, object({
49015
+ width: number,
49016
+ height: number
49017
+ }))
49018
+ });
49019
+
49020
+ object({
49021
+ question: parsePerseusRenderer,
49022
+ hints: array(parseHint),
49023
+ answerArea: record(enumeration(...ItemExtras), boolean),
49024
+ itemDataVersion: object({
49025
+ major: number,
49026
+ minor: number
49027
+ }),
49028
+ // Deprecated field
49029
+ answer: any
49030
+ });
49031
+
47742
49032
  /**
47743
49033
  * Helper to parse PerseusItem JSON
47744
49034
  * Why not just use JSON.parse? We want: