@khanacademy/perseus 41.2.0 → 41.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/all-widgets.d.ts +1 -1
  2. package/dist/article-renderer.d.ts +2 -2
  3. package/dist/basic-widgets.d.ts +1 -1
  4. package/dist/es/index.js +1609 -335
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/extra-widgets.d.ts +1 -1
  7. package/dist/index.d.ts +4 -3
  8. package/dist/index.js +1528 -253
  9. package/dist/index.js.map +1 -1
  10. package/dist/perseus-api.d.ts +3 -3
  11. package/dist/perseus-types.d.ts +7 -2
  12. package/dist/prompt-types.d.ts +33 -0
  13. package/dist/renderer.d.ts +8 -2
  14. package/dist/server-item-renderer.d.ts +6 -4
  15. package/dist/types.d.ts +4 -2
  16. package/dist/util/parse-perseus-json/error-tracking-parse-context.d.ts +9 -0
  17. package/dist/util/parse-perseus-json/general-purpose-parsers/any.d.ts +2 -0
  18. package/dist/util/parse-perseus-json/general-purpose-parsers/array.d.ts +2 -0
  19. package/dist/util/parse-perseus-json/general-purpose-parsers/array.typetest.d.ts +1 -0
  20. package/dist/util/parse-perseus-json/general-purpose-parsers/boolean.d.ts +2 -0
  21. package/dist/util/parse-perseus-json/general-purpose-parsers/compose-parsers.d.ts +2 -0
  22. package/dist/util/parse-perseus-json/general-purpose-parsers/constant.d.ts +4 -0
  23. package/dist/util/parse-perseus-json/general-purpose-parsers/defaulted.d.ts +2 -0
  24. package/dist/util/parse-perseus-json/general-purpose-parsers/enumeration.d.ts +2 -0
  25. package/dist/util/parse-perseus-json/general-purpose-parsers/enumeration.typetest.d.ts +1 -0
  26. package/dist/util/parse-perseus-json/general-purpose-parsers/index.d.ts +15 -0
  27. package/dist/util/parse-perseus-json/general-purpose-parsers/is-object.d.ts +1 -0
  28. package/dist/util/parse-perseus-json/general-purpose-parsers/nullable.d.ts +2 -0
  29. package/dist/util/parse-perseus-json/general-purpose-parsers/number.d.ts +2 -0
  30. package/dist/util/parse-perseus-json/general-purpose-parsers/object.d.ts +6 -0
  31. package/dist/util/parse-perseus-json/general-purpose-parsers/optional.d.ts +2 -0
  32. package/dist/util/parse-perseus-json/general-purpose-parsers/pair.d.ts +2 -0
  33. package/dist/util/parse-perseus-json/general-purpose-parsers/record.d.ts +2 -0
  34. package/dist/util/parse-perseus-json/general-purpose-parsers/string.d.ts +2 -0
  35. package/dist/util/parse-perseus-json/general-purpose-parsers/test-helpers.d.ts +8 -0
  36. package/dist/util/parse-perseus-json/general-purpose-parsers/trio.d.ts +2 -0
  37. package/dist/util/parse-perseus-json/general-purpose-parsers/union.d.ts +7 -0
  38. package/dist/util/parse-perseus-json/general-purpose-parsers/union.typetest.d.ts +1 -0
  39. package/dist/util/{parse-perseus-json.d.ts → parse-perseus-json/index.d.ts} +7 -1
  40. package/dist/util/parse-perseus-json/object-path.d.ts +2 -0
  41. package/dist/util/parse-perseus-json/parse-failure-detail.d.ts +2 -0
  42. package/dist/util/parse-perseus-json/parse.d.ts +3 -0
  43. package/dist/util/parse-perseus-json/parser-types.d.ts +30 -0
  44. package/dist/util/parse-perseus-json/perseus-parsers/categorizer-widget.d.ts +3 -0
  45. package/dist/util/parse-perseus-json/perseus-parsers/cs-program-widget.d.ts +3 -0
  46. package/dist/util/parse-perseus-json/perseus-parsers/definition-widget.d.ts +3 -0
  47. package/dist/util/parse-perseus-json/perseus-parsers/dropdown-widget.d.ts +3 -0
  48. package/dist/util/parse-perseus-json/perseus-parsers/explanation-widget.d.ts +3 -0
  49. package/dist/util/parse-perseus-json/perseus-parsers/expression-widget.d.ts +3 -0
  50. package/dist/util/parse-perseus-json/perseus-parsers/hint.d.ts +3 -0
  51. package/dist/util/parse-perseus-json/perseus-parsers/interactive-graph-widget.d.ts +3 -0
  52. package/dist/util/parse-perseus-json/perseus-parsers/perseus-image-background.d.ts +3 -0
  53. package/dist/util/parse-perseus-json/perseus-parsers/perseus-item.d.ts +3 -0
  54. package/dist/util/parse-perseus-json/perseus-parsers/perseus-renderer.d.ts +3 -0
  55. package/dist/util/parse-perseus-json/perseus-parsers/widget.d.ts +3 -0
  56. package/dist/util/parse-perseus-json/perseus-parsers/widgets-map.d.ts +3 -0
  57. package/dist/util/parse-perseus-json/result.d.ts +16 -0
  58. package/dist/validation.types.d.ts +0 -3
  59. package/dist/widget-ai-utils/categorizer/prompt-utils.d.ts +14 -0
  60. package/dist/widget-ai-utils/cs-program/prompt-utils.d.ts +2 -0
  61. package/dist/widget-ai-utils/definition/prompt-utils.d.ts +8 -0
  62. package/dist/widget-ai-utils/dropdown/prompt-utils.d.ts +13 -0
  63. package/dist/widget-ai-utils/explanation/prompt-utils.d.ts +8 -0
  64. package/dist/widget-ai-utils/expression/prompt-utils.d.ts +11 -0
  65. package/dist/widget-ai-utils/graded-group/prompt-utils.d.ts +7 -0
  66. package/dist/widget-ai-utils/graded-group-set/prompt-utils.d.ts +11 -0
  67. package/dist/widget-ai-utils/grapher/prompt-utils.d.ts +18 -0
  68. package/dist/widget-ai-utils/group/prompt-utils.d.ts +5 -0
  69. package/dist/widget-ai-utils/iframe/prompt-utils.d.ts +2 -0
  70. package/dist/widget-ai-utils/image/prompt-utils.d.ts +12 -0
  71. package/dist/widget-ai-utils/input-number/prompt-utils.d.ts +14 -0
  72. package/dist/widget-ai-utils/interaction/prompt-utils.d.ts +2 -0
  73. package/dist/widget-ai-utils/interactive-graph/prompt-utils.d.ts +94 -0
  74. package/dist/widget-ai-utils/label-image/prompt-utils.d.ts +24 -0
  75. package/dist/widget-ai-utils/matcher/prompt-utils.d.ts +17 -0
  76. package/dist/widget-ai-utils/matrix/prompt-utils.d.ts +14 -0
  77. package/dist/widget-ai-utils/measurer/prompt-utils.d.ts +2 -0
  78. package/dist/widget-ai-utils/number-line/prompt-utils.d.ts +16 -0
  79. package/dist/widget-ai-utils/numeric-input/prompt-utils.d.ts +11 -0
  80. package/dist/widget-ai-utils/orderer/prompt-utils.d.ts +13 -0
  81. package/dist/widget-ai-utils/passage/prompt-utils.d.ts +11 -0
  82. package/dist/widget-ai-utils/passage-ref/prompt-utils.d.ts +11 -0
  83. package/dist/widget-ai-utils/phet-simulation/prompt-utils.d.ts +2 -0
  84. package/dist/widget-ai-utils/plotter/prompt-utils.d.ts +2 -0
  85. package/dist/widget-ai-utils/python-program/prompt-utils.d.ts +2 -0
  86. package/dist/widget-ai-utils/radio/prompt-utils.d.ts +15 -0
  87. package/dist/widget-ai-utils/sorter/prompt-utils.d.ts +9 -0
  88. package/dist/widget-ai-utils/unsupported-widget.d.ts +11 -0
  89. package/dist/widget-ai-utils/video/prompt-utils.d.ts +2 -0
  90. package/dist/widgets/__shared__/{noop-validator.d.ts → score-noop.d.ts} +4 -4
  91. package/dist/widgets/categorizer/categorizer.d.ts +10 -19
  92. package/dist/widgets/categorizer/{categorizer-validator.d.ts → score-categorizer.d.ts} +2 -2
  93. package/dist/widgets/cs-program/cs-program.d.ts +8 -19
  94. package/dist/widgets/cs-program/{cs-program-validator.d.ts → score-cs-program.d.ts} +2 -1
  95. package/dist/widgets/definition/definition.d.ts +8 -19
  96. package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +4 -19
  97. package/dist/widgets/dropdown/dropdown.d.ts +10 -19
  98. package/dist/widgets/dropdown/score-dropdown.d.ts +4 -0
  99. package/dist/widgets/explanation/explanation.d.ts +9 -19
  100. package/dist/widgets/expression/expression.d.ts +26 -290
  101. package/dist/widgets/expression/{expression-validator.d.ts → score-expression.d.ts} +2 -2
  102. package/dist/widgets/graded-group/graded-group.d.ts +13 -21
  103. package/dist/widgets/graded-group-set/graded-group-set.d.ts +9 -20
  104. package/dist/widgets/grapher/grapher.d.ts +9 -19
  105. package/dist/widgets/grapher/score-grapher.d.ts +4 -0
  106. package/dist/widgets/group/group.d.ts +7 -19
  107. package/dist/widgets/iframe/iframe.d.ts +7 -19
  108. package/dist/widgets/iframe/{iframe-validator.d.ts → score-iframe.d.ts} +1 -1
  109. package/dist/widgets/image/image.d.ts +9 -19
  110. package/dist/widgets/input-number/input-number.d.ts +11 -19
  111. package/dist/widgets/input-number/{input-number-validator.d.ts → score-input-number.d.ts} +2 -2
  112. package/dist/widgets/interaction/interaction.d.ts +7 -19
  113. package/dist/widgets/interactive-graph.d.ts +8 -19
  114. package/dist/widgets/interactive-graphs/score-interactive-graph.d.ts +4 -0
  115. package/dist/widgets/interactive-graphs/utils.d.ts +1 -1
  116. package/dist/widgets/label-image/label-image.d.ts +10 -86
  117. package/dist/widgets/label-image/{label-image-validator.d.ts → score-label-image.d.ts} +2 -2
  118. package/dist/widgets/matcher/matcher.d.ts +7 -19
  119. package/dist/widgets/matcher/score-matcher.d.ts +4 -0
  120. package/dist/widgets/matrix/matrix.d.ts +10 -19
  121. package/dist/widgets/matrix/{matrix-validator.d.ts → score-matrix.d.ts} +2 -2
  122. package/dist/widgets/measurer/measurer.d.ts +13 -19
  123. package/dist/widgets/molecule/molecule.d.ts +4 -19
  124. package/dist/widgets/number-line/number-line.d.ts +8 -19
  125. package/dist/widgets/number-line/score-number-line.d.ts +4 -0
  126. package/dist/widgets/numeric-input/numeric-input.d.ts +22 -19
  127. package/dist/widgets/numeric-input/{numeric-input-validator.d.ts → score-numeric-input.d.ts} +2 -2
  128. package/dist/widgets/orderer/orderer.d.ts +8 -19
  129. package/dist/widgets/orderer/{orderer-validator.d.ts → score-orderer.d.ts} +1 -1
  130. package/dist/widgets/passage/passage.d.ts +8 -19
  131. package/dist/widgets/passage-ref/passage-ref.d.ts +12 -19
  132. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +11 -19
  133. package/dist/widgets/phet-simulation/phet-simulation.d.ts +5 -19
  134. package/dist/widgets/plotter/plotter.d.ts +8 -19
  135. package/dist/widgets/plotter/score-plotter.d.ts +4 -0
  136. package/dist/widgets/python-program/python-program.d.ts +4 -19
  137. package/dist/widgets/radio/radio-component.d.ts +3 -1
  138. package/dist/widgets/radio/radio.d.ts +18 -19
  139. package/dist/widgets/radio/{radio-validator.d.ts → score-radio.d.ts} +2 -2
  140. package/dist/widgets/sorter/score-sorter.d.ts +4 -0
  141. package/dist/widgets/sorter/sorter.d.ts +7 -19
  142. package/dist/widgets/table/{table-validator.d.ts → score-table.d.ts} +2 -2
  143. package/dist/widgets/table/table.d.ts +8 -19
  144. package/dist/widgets/video/video.d.ts +7 -19
  145. package/dist/widgets.d.ts +2 -2
  146. package/package.json +4 -4
  147. package/dist/widgets/dropdown/dropdown-validator.d.ts +0 -4
  148. package/dist/widgets/grapher/grapher-validator.d.ts +0 -4
  149. package/dist/widgets/interactive-graphs/interactive-graph-validator.d.ts +0 -4
  150. package/dist/widgets/matcher/matcher-validator.d.ts +0 -4
  151. package/dist/widgets/number-line/number-line-validator.d.ts +0 -4
  152. package/dist/widgets/plotter/plotter-validator.d.ts +0 -4
  153. package/dist/widgets/sorter/sorter-validator.d.ts +0 -4
package/dist/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.
@@ -2225,31 +2261,6 @@ const KhanAnswerTypes = {
2225
2261
  }
2226
2262
  };
2227
2263
 
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
2264
  /* Content creators input a list of answers which are matched from top to
2254
2265
  * bottom. The intent is that they can include spcific solutions which should
2255
2266
  * be graded as correct or incorrect (or ungraded!) first, then get more
@@ -2268,7 +2279,7 @@ const getDecimalSeparator = locale => {
2268
2279
  * show the user an error. TODO(joel) - what error?
2269
2280
  * - Otherwise, pass through the resulting points and message.
2270
2281
  */
2271
- function expressionValidator(userInput, rubric, strings, locale) {
2282
+ function scoreExpression(userInput, rubric, strings, locale) {
2272
2283
  const options = ___default["default"].clone(rubric);
2273
2284
  ___default["default"].extend(options, {
2274
2285
  decimal_separator: getDecimalSeparator(locale)
@@ -2480,6 +2491,9 @@ class Expression extends React__namespace.Component {
2480
2491
  getUserInput() {
2481
2492
  return Expression.getUserInputFromProps(this.props);
2482
2493
  }
2494
+ getPromptJSON() {
2495
+ return getPromptJSON$t(this.props, this.getUserInput());
2496
+ }
2483
2497
  change = (() => {
2484
2498
  var _this = this;
2485
2499
  return function () {
@@ -2764,7 +2778,7 @@ var Expression$1 = {
2764
2778
  propUpgrades: propUpgrades$2,
2765
2779
  // For use by the editor
2766
2780
  isLintable: true,
2767
- validator: expressionValidator,
2781
+ scorer: scoreExpression,
2768
2782
  getOneCorrectAnswerFromRubric(rubric) {
2769
2783
  const correctAnswers = (rubric.answerForms || []).filter(answerForm => answerForm.considered === "correct");
2770
2784
  if (correctAnswers.length === 0) {
@@ -18502,10 +18516,10 @@ const styles$C = aphrodite.StyleSheet.create({
18502
18516
 
18503
18517
  /* eslint-disable no-useless-escape, no-prototype-builtins */
18504
18518
  const rules$1 = {
18505
- ...pureMarkdown.pureMarkdownRules,
18519
+ ...SimpleMarkdown$1.pureMarkdownRules,
18506
18520
  // NOTE: basically ignored by JIPT. wraps everything at the outer layer
18507
18521
  columns: {
18508
- ...pureMarkdown.pureMarkdownRules.columns,
18522
+ ...SimpleMarkdown$1.pureMarkdownRules.columns,
18509
18523
  react: (node, output, state) => {
18510
18524
  return /*#__PURE__*/React__namespace.createElement("div", {
18511
18525
  className: "perseus-two-columns",
@@ -18533,7 +18547,7 @@ const rules$1 = {
18533
18547
  // for exercises the renderer just renders the crowdin id to the
18534
18548
  // renderer div.
18535
18549
  crowdinId: {
18536
- ...pureMarkdown.pureMarkdownRules.crowdinId,
18550
+ ...SimpleMarkdown$1.pureMarkdownRules.crowdinId,
18537
18551
  react: (node, output, state) => node.id
18538
18552
  },
18539
18553
  // Ensure that the table is focusable in sequential keyboard navigation
@@ -18542,7 +18556,7 @@ const rules$1 = {
18542
18556
  // left/right gesture. Note, setting `tabindex` doesn't appear required for
18543
18557
  // iOS 14.4, in order for VoiceOver to work.
18544
18558
  table: {
18545
- ...pureMarkdown.pureMarkdownRules.table,
18559
+ ...SimpleMarkdown$1.pureMarkdownRules.table,
18546
18560
  react: function (node, output, state) {
18547
18561
  const table = SimpleMarkdown__default["default"].defaultRules.table.react(node, output, state);
18548
18562
  if (!state.isMobile) {
@@ -18560,7 +18574,7 @@ const rules$1 = {
18560
18574
  // our table regex into tableCapture[1..], and we pass tableCapture to
18561
18575
  // our nptable regex
18562
18576
  titledTable: {
18563
- ...pureMarkdown.pureMarkdownRules.titledTable,
18577
+ ...SimpleMarkdown$1.pureMarkdownRules.titledTable,
18564
18578
  react: (node, output, state) => {
18565
18579
  let contents;
18566
18580
  if (!node.table) {
@@ -18620,7 +18634,7 @@ const rules$1 = {
18620
18634
  }
18621
18635
  },
18622
18636
  widget: {
18623
- ...pureMarkdown.pureMarkdownRules.widget,
18637
+ ...SimpleMarkdown$1.pureMarkdownRules.widget,
18624
18638
  react: (node, output, state) => {
18625
18639
  // The actual output is handled in the renderer, where
18626
18640
  // we know the current widget props/state. This is
@@ -18631,7 +18645,7 @@ const rules$1 = {
18631
18645
  }
18632
18646
  },
18633
18647
  blockMath: {
18634
- ...pureMarkdown.pureMarkdownRules.blockMath,
18648
+ ...SimpleMarkdown$1.pureMarkdownRules.blockMath,
18635
18649
  react: (node, output, state) => {
18636
18650
  const {
18637
18651
  TeX
@@ -18645,7 +18659,7 @@ const rules$1 = {
18645
18659
  }
18646
18660
  },
18647
18661
  math: {
18648
- ...pureMarkdown.pureMarkdownRules.math,
18662
+ ...SimpleMarkdown$1.pureMarkdownRules.math,
18649
18663
  react: (node, output, state) => {
18650
18664
  const {
18651
18665
  TeX
@@ -18659,7 +18673,7 @@ const rules$1 = {
18659
18673
  }
18660
18674
  },
18661
18675
  unescapedDollar: {
18662
- ...pureMarkdown.pureMarkdownRules.unescapedDollar,
18676
+ ...SimpleMarkdown$1.pureMarkdownRules.unescapedDollar,
18663
18677
  react: (node, output, state) => {
18664
18678
  // Unescaped dollar signs render correctly, but result in
18665
18679
  // untranslatable text after the i18n python linter flags it
@@ -18669,7 +18683,7 @@ const rules$1 = {
18669
18683
  // Extend the SimpleMarkdown link parser to make the link open in a new
18670
18684
  // window and handle reverse tabnapping phishing attacks
18671
18685
  link: {
18672
- ...pureMarkdown.pureMarkdownRules.link,
18686
+ ...SimpleMarkdown$1.pureMarkdownRules.link,
18673
18687
  react: function (node, output, state) {
18674
18688
  const link = SimpleMarkdown__default["default"].defaultRules.link.react(node, output, state);
18675
18689
  const href = link.props.href;
@@ -18696,7 +18710,7 @@ const rules$1 = {
18696
18710
  }
18697
18711
  },
18698
18712
  codeBlock: {
18699
- ...pureMarkdown.pureMarkdownRules.codeBlock,
18713
+ ...SimpleMarkdown$1.pureMarkdownRules.codeBlock,
18700
18714
  react: (node, output, state) => {
18701
18715
  // ideally this should be a different rule, with only an
18702
18716
  // output function, but right now that breaks the parser.
@@ -18714,7 +18728,7 @@ const rules$1 = {
18714
18728
  // transform the tree to add lint nodes. This rule is here
18715
18729
  // just for the react() function
18716
18730
  lint: {
18717
- ...pureMarkdown.pureMarkdownRules.lint,
18731
+ ...SimpleMarkdown$1.pureMarkdownRules.lint,
18718
18732
  react: (node, output, state) => {
18719
18733
  return /*#__PURE__*/React__namespace.createElement(Lint, {
18720
18734
  message: node.message,
@@ -18964,8 +18978,8 @@ const getWidget = name => {
18964
18978
  const getWidgetExport = name => {
18965
18979
  return widgets[name] ?? null;
18966
18980
  };
18967
- const getWidgetValidator = name => {
18968
- return widgets[name]?.validator ?? null;
18981
+ const getWidgetScorer = name => {
18982
+ return widgets[name]?.scorer ?? null;
18969
18983
  };
18970
18984
  const getEditor = name => {
18971
18985
  return ___default["default"].has(editors, name) ? editors[name] : null;
@@ -19273,7 +19287,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19273
19287
  replaceDeprecatedEditors: replaceDeprecatedEditors,
19274
19288
  getWidget: getWidget,
19275
19289
  getWidgetExport: getWidgetExport,
19276
- getWidgetValidator: getWidgetValidator,
19290
+ getWidgetScorer: getWidgetScorer,
19277
19291
  getEditor: getEditor,
19278
19292
  getTransform: getTransform,
19279
19293
  getVersion: getVersion,
@@ -19305,12 +19319,12 @@ widgetIds, userInputMap, strings, locale) {
19305
19319
  }
19306
19320
  let score = null;
19307
19321
  const userInput = userInputMap[id];
19308
- const validator = getWidgetValidator(widget.type);
19322
+ const scorer = getWidgetScorer(widget.type);
19309
19323
  if (widget.type === "group") {
19310
19324
  const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19311
19325
  score = Util.flattenScores(scores);
19312
- } else if (validator) {
19313
- score = validator(userInput, widget.options, strings, locale);
19326
+ } else if (scorer) {
19327
+ score = scorer(userInput, widget.options, strings, locale);
19314
19328
  }
19315
19329
  if (score) {
19316
19330
  return Util.scoreIsEmpty(score);
@@ -19335,12 +19349,12 @@ widgetIds, userInputMap, strings, locale) {
19335
19349
  return;
19336
19350
  }
19337
19351
  const userInput = userInputMap[id];
19338
- const validator = getWidgetValidator(widget.type);
19352
+ const scorer = getWidgetScorer(widget.type);
19339
19353
  if (widget.type === "group") {
19340
19354
  const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19341
19355
  widgetScores[id] = Util.flattenScores(scores);
19342
- } else if (validator) {
19343
- widgetScores[id] = validator(userInput, widget.options, strings, locale);
19356
+ } else if (scorer) {
19357
+ widgetScores[id] = scorer(userInput, widget.options, strings, locale);
19344
19358
  }
19345
19359
  });
19346
19360
  return widgetScores;
@@ -19824,9 +19838,9 @@ class Renderer extends React__namespace.Component {
19824
19838
  // WidgetContainers don't update their widgets' props when
19825
19839
  // they are re-rendered, so even if they've been
19826
19840
  // re-rendered we need to call these methods on them.
19827
- ___default["default"].each(this.widgetIds, id => {
19841
+ this.widgetIds.forEach(id => {
19828
19842
  const container = this._widgetContainers.get(makeContainerId(id));
19829
- container && container.replaceWidgetProps(this.getWidgetProps(id));
19843
+ container?.replaceWidgetProps(this.getWidgetProps(id));
19830
19844
  });
19831
19845
  if (this.props.serializedState && !___default["default"].isEqual(this.props.serializedState, this.getSerializedState())) {
19832
19846
  this.restoreSerializedState(this.props.serializedState);
@@ -19965,25 +19979,25 @@ class Renderer extends React__namespace.Component {
19965
19979
  }
19966
19980
  return null;
19967
19981
  };
19968
- getWidgetProps = id => {
19982
+ getWidgetProps(widgetId) {
19969
19983
  var _this = this;
19970
19984
  const apiOptions = this.getApiOptions();
19971
- const widgetProps = this.state.widgetProps[id] || {};
19985
+ const widgetProps = this.state.widgetProps[widgetId] || {};
19972
19986
 
19973
19987
  // The widget needs access to its "rubric" at all times when in review
19974
19988
  // mode (which is really just part of its widget info).
19975
- const widgetInfo = this.state.widgetInfo[id];
19989
+ const widgetInfo = this.state.widgetInfo[widgetId];
19976
19990
  const reviewModeRubric = this.props.reviewMode && widgetInfo ? widgetInfo.options : null;
19977
19991
  if (!this._interactionTrackers) {
19978
19992
  this._interactionTrackers = {};
19979
19993
  }
19980
- let interactionTracker = this._interactionTrackers[id];
19994
+ let interactionTracker = this._interactionTrackers[widgetId];
19981
19995
  if (!interactionTracker) {
19982
- interactionTracker = this._interactionTrackers[id] = new InteractionTracker(apiOptions.trackInteraction, widgetInfo && widgetInfo.type, id, getTracking(widgetInfo && widgetInfo.type));
19996
+ interactionTracker = this._interactionTrackers[widgetId] = new InteractionTracker(apiOptions.trackInteraction, widgetInfo && widgetInfo.type, widgetId, getTracking(widgetInfo && widgetInfo.type));
19983
19997
  }
19984
19998
  return {
19985
19999
  ...widgetProps,
19986
- widgetId: id,
20000
+ widgetId: widgetId,
19987
20001
  alignment: widgetInfo && widgetInfo.alignment,
19988
20002
  static: widgetInfo?.static,
19989
20003
  problemNum: this.props.problemNum,
@@ -19991,19 +20005,19 @@ class Renderer extends React__namespace.Component {
19991
20005
  keypadElement: this.props.keypadElement,
19992
20006
  questionCompleted: this.props.questionCompleted,
19993
20007
  showSolutions: this.props.showSolutions,
19994
- onFocus: ___default["default"].partial(this._onWidgetFocus, id),
19995
- onBlur: ___default["default"].partial(this._onWidgetBlur, id),
20008
+ onFocus: ___default["default"].partial(this._onWidgetFocus, widgetId),
20009
+ onBlur: ___default["default"].partial(this._onWidgetBlur, widgetId),
19996
20010
  findWidgets: this.findWidgets,
19997
20011
  reviewModeRubric: reviewModeRubric,
19998
20012
  reviewMode: this.props.reviewMode,
19999
20013
  onChange: function (newProps, cb) {
20000
20014
  let silent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
20001
- _this._setWidgetProps(id, newProps, cb, silent);
20015
+ _this._setWidgetProps(widgetId, newProps, cb, silent);
20002
20016
  },
20003
20017
  trackInteraction: interactionTracker.track,
20004
- isLastUsedWidget: id === this.state.lastUsedWidgetId
20018
+ isLastUsedWidget: widgetId === this.state.lastUsedWidgetId
20005
20019
  };
20006
- };
20020
+ }
20007
20021
 
20008
20022
  /**
20009
20023
  * Serializes the questions state so it can be recovered.
@@ -20892,6 +20906,25 @@ class Renderer extends React__namespace.Component {
20892
20906
  return this.widgetIds;
20893
20907
  };
20894
20908
 
20909
+ /**
20910
+ * Returns a JSON representation of the content and widgets
20911
+ * that can be passed to an LLM for prompt context.
20912
+ */
20913
+ getPromptJSON() {
20914
+ const {
20915
+ content
20916
+ } = this.props;
20917
+ const widgetJSON = {};
20918
+ this.widgetIds.forEach(id => {
20919
+ const widget = this.getWidgetInstance(id);
20920
+ widgetJSON[id] = widget?.getPromptJSON?.() || {};
20921
+ });
20922
+ return {
20923
+ content,
20924
+ widgets: widgetJSON
20925
+ };
20926
+ }
20927
+
20895
20928
  /**
20896
20929
  * Returns an object mapping from widget ID to perseus-style score.
20897
20930
  * The keys of this object are the values of the array returned
@@ -21618,6 +21651,19 @@ SimpleKeypadInput.propTypes = {
21618
21651
  value: PropTypes__default["default"].oneOfType([PropTypes__default["default"].string, PropTypes__default["default"].number])
21619
21652
  };
21620
21653
 
21654
+ const getPromptJSON$s = (renderProps, userInput) => {
21655
+ return {
21656
+ type: "input-number",
21657
+ options: {
21658
+ simplify: renderProps.simplify,
21659
+ answerType: renderProps.answerType
21660
+ },
21661
+ userInput: {
21662
+ value: userInput.currentValue
21663
+ }
21664
+ };
21665
+ };
21666
+
21621
21667
  /*
21622
21668
  * In this file, an `expression` is some portion of valid TeX enclosed in
21623
21669
  * curly brackets.
@@ -21808,7 +21854,7 @@ const answerTypes = {
21808
21854
  forms: "pi"
21809
21855
  }
21810
21856
  };
21811
- function inputNumberValidator(userInput, rubric, strings) {
21857
+ function scoreInputNumber(userInput, rubric, strings) {
21812
21858
  if (rubric.answerType == null) {
21813
21859
  rubric.answerType = "number";
21814
21860
  }
@@ -21939,6 +21985,9 @@ class InputNumber extends React__namespace.Component {
21939
21985
  getUserInput() {
21940
21986
  return InputNumber.getUserInputFromProps(this.props);
21941
21987
  }
21988
+ getPromptJSON() {
21989
+ return getPromptJSON$s(this.props, this.getUserInput());
21990
+ }
21942
21991
  examples = () => {
21943
21992
  const {
21944
21993
  strings
@@ -22038,7 +22087,7 @@ var InputNumber$1 = {
22038
22087
  widget: InputNumber,
22039
22088
  transform: propTransform$3,
22040
22089
  isLintable: true,
22041
- validator: inputNumberValidator,
22090
+ scorer: scoreInputNumber,
22042
22091
  getOneCorrectAnswerFromRubric(rubric) {
22043
22092
  if (rubric.value == null) {
22044
22093
  return;
@@ -22051,6 +22100,16 @@ var InputNumber$1 = {
22051
22100
  }
22052
22101
  };
22053
22102
 
22103
+ const getPromptJSON$r = (renderProps, userInput) => {
22104
+ return {
22105
+ type: "numeric-input",
22106
+ label: renderProps.labelText,
22107
+ userInput: {
22108
+ value: userInput.currentValue
22109
+ }
22110
+ };
22111
+ };
22112
+
22054
22113
  const ParseTex = TexWrangler.parseTex;
22055
22114
  const answerFormButtons = [{
22056
22115
  title: "Integers",
@@ -22109,7 +22168,7 @@ function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
22109
22168
  // Otherwise, we return input valuåe (number) stripped of the "%".
22110
22169
  return value;
22111
22170
  }
22112
- function numericInputValidator(userInput, rubric, strings) {
22171
+ function scoreNumericInput(userInput, rubric, strings) {
22113
22172
  const defaultAnswerForms = answerFormButtons.map(e => e["value"])
22114
22173
  // Don't default to validating the answer as a pi answer
22115
22174
  // if answerForm isn't set on the answer
@@ -22271,6 +22330,9 @@ class NumericInput extends React__namespace.Component {
22271
22330
  getUserInput() {
22272
22331
  return NumericInput.getUserInputFromProps(this.props);
22273
22332
  }
22333
+ getPromptJSON() {
22334
+ return getPromptJSON$r(this.props, this.getUserInput());
22335
+ }
22274
22336
  handleChange = (newValue, cb) => {
22275
22337
  this.props.onChange({
22276
22338
  currentValue: newValue
@@ -22396,7 +22458,7 @@ var NumericInput$1 = {
22396
22458
  widget: NumericInput,
22397
22459
  transform: propsTransform,
22398
22460
  isLintable: true,
22399
- validator: numericInputValidator,
22461
+ scorer: scoreNumericInput,
22400
22462
  getOneCorrectAnswerFromRubric(rubric) {
22401
22463
  const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
22402
22464
  const answerStrings = correctAnswers.map(answer => {
@@ -22427,15 +22489,43 @@ var NumericInput$1 = {
22427
22489
  }
22428
22490
  };
22429
22491
 
22492
+ const getPromptJSON$q = (renderProps, userInput) => {
22493
+ const choices = renderProps.choices || [];
22494
+ const options = choices.map(choice => {
22495
+ return {
22496
+ value: choice.content
22497
+ };
22498
+ });
22499
+ return {
22500
+ type: "radio",
22501
+ hasNoneOfTheAbove: !!renderProps.hasNoneOfTheAbove,
22502
+ options,
22503
+ userInput: {
22504
+ selectedOptions: userInput.choicesSelected.slice()
22505
+ }
22506
+ };
22507
+ };
22508
+
22509
+ const getPromptJSON$p = renderProps => {
22510
+ return {
22511
+ type: "passage-ref",
22512
+ options: {
22513
+ passageNumber: renderProps.passageNumber,
22514
+ referenceNumber: renderProps.referenceNumber,
22515
+ summaryText: renderProps.summaryText
22516
+ }
22517
+ };
22518
+ };
22519
+
22430
22520
  /**
22431
- * Several widgets don't have "right"/"wrong" validation logic,
22521
+ * Several widgets don't have "right"/"wrong" scoring logic,
22432
22522
  * so this just says to move on past those widgets
22433
22523
  *
22434
22524
  * TODO(LEMS-2543) widgets that use this probably shouldn't have any
22435
- * validation logic and the thing validating an exercise
22525
+ * scoring logic and the thing scoring an exercise
22436
22526
  * should just know to skip these
22437
22527
  */
22438
- function noopValidator() {
22528
+ function scoreNoop() {
22439
22529
  let points = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
22440
22530
  return {
22441
22531
  type: "points",
@@ -23829,6 +23919,17 @@ const styles$x = aphrodite.StyleSheet.create({
23829
23919
  }
23830
23920
  });
23831
23921
 
23922
+ const getPromptJSON$o = renderProps => {
23923
+ return {
23924
+ type: "passage",
23925
+ options: {
23926
+ passageTitle: renderProps.passageTitle,
23927
+ passageText: renderProps.passageText,
23928
+ footnotes: renderProps.footnotes
23929
+ }
23930
+ };
23931
+ };
23932
+
23832
23933
  function getInitialParseState() {
23833
23934
  return {
23834
23935
  currentRef: [],
@@ -24461,6 +24562,9 @@ class Passage extends React__namespace.Component {
24461
24562
  content: refContent
24462
24563
  };
24463
24564
  }
24565
+ getPromptJSON() {
24566
+ return getPromptJSON$o(this.props);
24567
+ }
24464
24568
 
24465
24569
  /**
24466
24570
  * Rendering
@@ -24606,7 +24710,7 @@ var Passage$1 = {
24606
24710
  return ___default["default"].pick(editorProps, "passageTitle", "passageText", "footnotes", "showLineNumbers");
24607
24711
  },
24608
24712
  isLintable: true,
24609
- validator: () => noopValidator()
24713
+ scorer: () => scoreNoop()
24610
24714
  };
24611
24715
 
24612
24716
  /**
@@ -24671,6 +24775,9 @@ class PassageRef extends React__namespace.Component {
24671
24775
  return change.apply(_this, args);
24672
24776
  };
24673
24777
  })();
24778
+ getPromptJSON() {
24779
+ return getPromptJSON$p(this.props);
24780
+ }
24674
24781
  _deferredUpdateRange = () => {
24675
24782
  ___default["default"].defer(this._updateRange);
24676
24783
  };
@@ -24740,8 +24847,8 @@ var PassageRef$1 = {
24740
24847
  major: 0,
24741
24848
  minor: 1
24742
24849
  },
24743
- // TODO: things that aren't interactive shouldn't need validators
24744
- validator: () => noopValidator()
24850
+ // TODO: things that aren't interactive shouldn't need scoring functions
24851
+ scorer: () => scoreNoop()
24745
24852
  };
24746
24853
 
24747
24854
  /**
@@ -26109,7 +26216,7 @@ const styles$p = aphrodite.StyleSheet.create({
26109
26216
  }
26110
26217
  });
26111
26218
 
26112
- function radioValidator(userInput, rubric, strings) {
26219
+ function scoreRadio(userInput, rubric, strings) {
26113
26220
  const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
26114
26221
  return sum + (selected ? 1 : 0);
26115
26222
  }, 0);
@@ -26119,10 +26226,10 @@ function radioValidator(userInput, rubric, strings) {
26119
26226
  message: null
26120
26227
  };
26121
26228
  }
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) {
26229
+ const numCorrect = rubric.choices.reduce((sum, currentChoice) => {
26230
+ return currentChoice.correct ? sum + 1 : sum;
26231
+ }, 0);
26232
+ if (numCorrect > 1 && numSelected !== numCorrect) {
26126
26233
  return {
26127
26234
  type: "invalid",
26128
26235
  message: strings.chooseCorrectNum
@@ -26130,9 +26237,8 @@ function radioValidator(userInput, rubric, strings) {
26130
26237
  // If NOTA and some other answer are checked, ...
26131
26238
  }
26132
26239
 
26133
- // TODO(LEMS-2541): should noneOfTheAboveSelected be replaced with a
26134
- // combination of choicesSelected and noneOfTheAboveIndex?
26135
- if (userInput.noneOfTheAboveSelected && numSelected > 1) {
26240
+ const noneOfTheAboveSelected = rubric.choices.some((choice, index) => choice.isNoneOfTheAbove && userInput.choicesSelected[index]);
26241
+ if (noneOfTheAboveSelected && numSelected > 1) {
26136
26242
  return {
26137
26243
  type: "invalid",
26138
26244
  message: strings.notNoneOfTheAbove
@@ -26140,9 +26246,7 @@ function radioValidator(userInput, rubric, strings) {
26140
26246
  }
26141
26247
  const correct = userInput.choicesSelected.every((selected, i) => {
26142
26248
  let isCorrect;
26143
- // TODO(LEMS-2541): should noneOfTheAboveIndex actually be on the rubric
26144
- // instead of the userInput?
26145
- if (userInput.noneOfTheAboveIndex === i) {
26249
+ if (rubric.choices[i].isNoneOfTheAbove) {
26146
26250
  isCorrect = rubric.choices.every((choice, j) => {
26147
26251
  return i === j || !choice.correct;
26148
26252
  });
@@ -26174,31 +26278,19 @@ class Radio$1 extends React__namespace.Component {
26174
26278
  showSolutions: "none"
26175
26279
  };
26176
26280
  static getUserInputFromProps(props) {
26281
+ let unshuffle = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
26177
26282
  // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
26178
26283
  // future timeline implementers: this used to be {value: i} before
26179
26284
  // multiple select was added)
26180
26285
  if (props.choiceStates) {
26181
- let noneOfTheAboveIndex = null;
26182
- let noneOfTheAboveSelected = false;
26183
26286
  const choiceStates = props.choiceStates;
26184
26287
  const choicesSelected = choiceStates.map(() => false);
26185
- const numCorrect = props.numCorrect;
26186
26288
  for (let i = 0; i < choicesSelected.length; i++) {
26187
- const index = props.choices[i].originalIndex;
26289
+ const index = unshuffle ? props.choices[i].originalIndex : i;
26188
26290
  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
26291
  }
26197
26292
  return {
26198
- choicesSelected,
26199
- numCorrect,
26200
- noneOfTheAboveIndex,
26201
- noneOfTheAboveSelected
26293
+ choicesSelected
26202
26294
  };
26203
26295
  // Support legacy choiceState implementation
26204
26296
  }
@@ -26207,27 +26299,14 @@ class Radio$1 extends React__namespace.Component {
26207
26299
  values
26208
26300
  } = props;
26209
26301
  if (values) {
26210
- let noneOfTheAboveIndex = null;
26211
- let noneOfTheAboveSelected = false;
26212
26302
  const choicesSelected = [...values];
26213
- const numCorrect = props.numCorrect;
26214
26303
  const valuesLength = values.length;
26215
26304
  for (let i = 0; i < valuesLength; i++) {
26216
- const index = props.choices[i].originalIndex;
26305
+ const index = unshuffle ? props.choices[i].originalIndex : i;
26217
26306
  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
26307
  }
26226
26308
  return {
26227
- choicesSelected,
26228
- noneOfTheAboveIndex,
26229
- noneOfTheAboveSelected,
26230
- numCorrect
26309
+ choicesSelected
26231
26310
  };
26232
26311
  }
26233
26312
  // Nothing checked
@@ -26355,6 +26434,10 @@ class Radio$1 extends React__namespace.Component {
26355
26434
  getUserInput() {
26356
26435
  return Radio$1.getUserInputFromProps(this.props);
26357
26436
  }
26437
+ getPromptJSON() {
26438
+ const userInput = Radio$1.getUserInputFromProps(this.props, false);
26439
+ return getPromptJSON$q(this.props, userInput);
26440
+ }
26358
26441
 
26359
26442
  /**
26360
26443
  * Turn on rationale display for the currently selected choices. Note that
@@ -26367,7 +26450,7 @@ class Radio$1 extends React__namespace.Component {
26367
26450
  choiceStates
26368
26451
  } = this.props;
26369
26452
  if (choiceStates) {
26370
- const score = radioValidator(this.getUserInput(), rubric, this.context.strings);
26453
+ const score = scoreRadio(this.getUserInput(), rubric, this.context.strings);
26371
26454
  const widgetCorrect = score.type === "points" && score.total === score.earned;
26372
26455
  const newStates = choiceStates.map(state => ({
26373
26456
  ...state,
@@ -26649,13 +26732,26 @@ var Radio = {
26649
26732
  },
26650
26733
  propUpgrades: propUpgrades$1,
26651
26734
  isLintable: true,
26652
- validator: radioValidator
26735
+ scorer: scoreRadio
26653
26736
  };
26654
26737
 
26655
26738
  /* eslint-disable import/no-named-as-default */
26656
26739
  var basicWidgets = [Radio, InputNumber$1, NumericInput$1, Expression$1];
26657
26740
 
26658
- function categorizerValidator(userInput, rubric, strings) {
26741
+ const getPromptJSON$n = (renderProps, userInput) => {
26742
+ return {
26743
+ type: "categorizer",
26744
+ options: {
26745
+ items: renderProps.items,
26746
+ categories: renderProps.categories
26747
+ },
26748
+ userInput: {
26749
+ itemToCategoryMapping: userInput.values
26750
+ }
26751
+ };
26752
+ };
26753
+
26754
+ function scoreCategorizer(userInput, rubric, strings) {
26659
26755
  let completed = true;
26660
26756
  let allCorrect = true;
26661
26757
  rubric.values.forEach((value, i) => {
@@ -26710,6 +26806,9 @@ class Categorizer extends React__namespace.Component {
26710
26806
  getUserInput() {
26711
26807
  return Categorizer.getUserInputFromProps(this.props);
26712
26808
  }
26809
+ getPromptJSON() {
26810
+ return getPromptJSON$n(this.props, this.getUserInput());
26811
+ }
26713
26812
  onChange(itemNum, catNum) {
26714
26813
  const values = [...this.props.values];
26715
26814
  // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'never'.
@@ -26856,7 +26955,7 @@ var Categorizer$1 = {
26856
26955
  return ___default["default"].pick(editorProps, "items", "categories", "values", "randomizeItems");
26857
26956
  },
26858
26957
  isLintable: true,
26859
- validator: categorizerValidator
26958
+ scorer: scoreCategorizer
26860
26959
  };
26861
26960
 
26862
26961
  /**
@@ -26906,7 +27005,20 @@ const parseUrl = url => new URL(url, location().origin);
26906
27005
  */
26907
27006
  const toAbsoluteUrl = url => parseUrl(url).href;
26908
27007
 
26909
- function csProgramValidator(state) {
27008
+ const getUnsupportedPromptJSON = function (widgetType) {
27009
+ let message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "";
27010
+ return {
27011
+ type: widgetType,
27012
+ isSupported: false,
27013
+ message
27014
+ };
27015
+ };
27016
+
27017
+ const getPromptJSON$m = () => {
27018
+ return getUnsupportedPromptJSON("cs-program");
27019
+ };
27020
+
27021
+ function scoreCSProgram(state) {
26910
27022
  // The iframe can tell us whether it's correct or incorrect,
26911
27023
  // and pass an optional message
26912
27024
  if (state.status === "correct") {
@@ -27002,6 +27114,9 @@ class CSProgram extends React__namespace.Component {
27002
27114
  message: this.props.message
27003
27115
  };
27004
27116
  }
27117
+ getPromptJSON() {
27118
+ return getPromptJSON$m();
27119
+ }
27005
27120
  render() {
27006
27121
  if (!this.props.programID) {
27007
27122
  return /*#__PURE__*/React__namespace.createElement("div", null);
@@ -27077,7 +27192,15 @@ var CSProgram$1 = {
27077
27192
  supportedAlignments: ["block", "full-width"],
27078
27193
  widget: CSProgram,
27079
27194
  hidden: true,
27080
- validator: csProgramValidator
27195
+ scorer: scoreCSProgram
27196
+ };
27197
+
27198
+ const getPromptJSON$l = renderProps => {
27199
+ return {
27200
+ type: "definition",
27201
+ definition: renderProps.definition,
27202
+ togglePrompt: renderProps.togglePrompt
27203
+ };
27081
27204
  };
27082
27205
 
27083
27206
  class Definition extends React__namespace.Component {
@@ -27090,6 +27213,9 @@ class Definition extends React__namespace.Component {
27090
27213
  // this just helps with TS weak typing when a Widget
27091
27214
  // doesn't implement any Widget methods
27092
27215
  isWidget = true;
27216
+ getPromptJSON() {
27217
+ return getPromptJSON$l(this.props);
27218
+ }
27093
27219
  render() {
27094
27220
  return /*#__PURE__*/React__namespace.createElement(DefinitionConsumer, null, _ref => {
27095
27221
  let {
@@ -27146,8 +27272,8 @@ var Definition$1 = {
27146
27272
  defaultAlignment: "inline",
27147
27273
  widget: Definition,
27148
27274
  transform: x => x,
27149
- // TODO: things that aren't interactive shouldn't need validators
27150
- validator: () => noopValidator()
27275
+ // TODO: things that aren't interactive shouldn't need scoring functions
27276
+ scorer: () => scoreNoop()
27151
27277
  };
27152
27278
 
27153
27279
  // The props are type `any` on purpose so that this can receive props
@@ -27175,11 +27301,23 @@ var DeprecatedStandin$1 = {
27175
27301
  displayName: "Deprecated Standin",
27176
27302
  widget: DeprecatedStandin,
27177
27303
  hidden: true,
27178
- // TODO: things that aren't interactive shouldn't need validators
27179
- validator: () => noopValidator(1)
27304
+ // TODO: things that aren't interactive shouldn't need scoring functions
27305
+ scorer: () => scoreNoop(1)
27306
+ };
27307
+
27308
+ const getPromptJSON$k = (renderProps, userInput) => {
27309
+ return {
27310
+ type: "dropdown",
27311
+ options: {
27312
+ items: renderProps.choices
27313
+ },
27314
+ userInput: {
27315
+ selectedIndex: userInput.value - 1 // Offset to account for placeholder
27316
+ }
27317
+ };
27180
27318
  };
27181
27319
 
27182
- function dropdownValidator(userInput, rubric) {
27320
+ function scoreDropdown(userInput, rubric) {
27183
27321
  const selected = userInput.value;
27184
27322
  if (selected === 0) {
27185
27323
  return {
@@ -27225,6 +27363,9 @@ class Dropdown extends React__namespace.Component {
27225
27363
  value: this.props.selected
27226
27364
  };
27227
27365
  }
27366
+ getPromptJSON() {
27367
+ return getPromptJSON$k(this.props, this.getUserInput());
27368
+ }
27228
27369
  render() {
27229
27370
  const children = [/*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.OptionItem, {
27230
27371
  key: "placeholder",
@@ -27266,7 +27407,15 @@ var Dropdown$1 = {
27266
27407
  accessible: true,
27267
27408
  widget: Dropdown,
27268
27409
  transform: optionsTransform,
27269
- validator: dropdownValidator
27410
+ scorer: scoreDropdown
27411
+ };
27412
+
27413
+ const getPromptJSON$j = renderProps => {
27414
+ return {
27415
+ type: "explanation",
27416
+ showPrompt: renderProps.showPrompt,
27417
+ explanation: renderProps.explanation
27418
+ };
27270
27419
  };
27271
27420
 
27272
27421
  // transform = _.identity
@@ -27315,6 +27464,9 @@ class Explanation extends React__namespace.Component {
27315
27464
  });
27316
27465
  this.props.trackInteraction();
27317
27466
  };
27467
+ getPromptJSON() {
27468
+ return getPromptJSON$j(this.props);
27469
+ }
27318
27470
  render() {
27319
27471
  const promptText = this.state.expanded ? this.props.hidePrompt : this.props.showPrompt;
27320
27472
  const caretIcon = this.state.expanded ? caretUp__default["default"] : caretDown__default["default"];
@@ -27421,8 +27573,26 @@ var Explanation$1 = {
27421
27573
  widget: Explanation,
27422
27574
  transform: ___default["default"].identity,
27423
27575
  isLintable: true,
27424
- // TODO: things that aren't interactive shouldn't need validators
27425
- validator: () => noopValidator()
27576
+ // TODO: things that aren't interactive shouldn't need scoring functions
27577
+ scorer: () => scoreNoop()
27578
+ };
27579
+
27580
+ const getPromptJSON$i = (title, rendererJSON, hintRendererJSON) => {
27581
+ if (!rendererJSON) {
27582
+ return {
27583
+ type: "graded-group",
27584
+ title,
27585
+ content: "",
27586
+ widgets: {},
27587
+ hint: hintRendererJSON
27588
+ };
27589
+ }
27590
+ return {
27591
+ ...rendererJSON,
27592
+ title,
27593
+ type: "graded-group",
27594
+ hint: hintRendererJSON
27595
+ };
27426
27596
  };
27427
27597
 
27428
27598
  class GradedGroupAnswerBar extends React__namespace.Component {
@@ -27566,6 +27736,8 @@ class GradedGroup extends React__namespace.Component {
27566
27736
  message: "",
27567
27737
  answerBarState: "HIDDEN"
27568
27738
  };
27739
+ rendererRef = /*#__PURE__*/React__namespace.createRef();
27740
+ hintRendererRef = /*#__PURE__*/React__namespace.createRef();
27569
27741
  shouldComponentUpdate(nextProps, nextState) {
27570
27742
  return nextProps !== this.props || nextState !== this.state;
27571
27743
  }
@@ -27591,13 +27763,9 @@ class GradedGroup extends React__namespace.Component {
27591
27763
  status: GRADING_STATUSES.ungraded,
27592
27764
  message: ""
27593
27765
  });
27594
-
27595
- // eslint-disable-next-line react/no-string-refs
27596
- if (this.refs.renderer) {
27766
+ if (this.rendererRef.current) {
27597
27767
  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();
27768
+ const emptyWidgets = this.rendererRef.current.emptyWidgets();
27601
27769
  const answerable = emptyWidgets.length === 0;
27602
27770
  const answerBarState = this.state.answerBarState;
27603
27771
  this.setState({
@@ -27606,12 +27774,10 @@ class GradedGroup extends React__namespace.Component {
27606
27774
  }
27607
27775
  };
27608
27776
  _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();
27777
+ this.rendererRef.current?.showRationalesForCurrentlySelectedChoices();
27778
+ const score = this.rendererRef.current?.score() || {
27779
+ type: "invalid"
27780
+ };
27615
27781
  const {
27616
27782
  INVALID_MESSAGE_PREFIX,
27617
27783
  DEFAULT_INVALID_MESSAGE_1,
@@ -27632,29 +27798,28 @@ class GradedGroup extends React__namespace.Component {
27632
27798
 
27633
27799
  // Mobile API
27634
27800
  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();
27801
+ return this.rendererRef.current?.getInputPaths() || [];
27638
27802
  };
27803
+ getPromptJSON() {
27804
+ // If the hint isn't expanded, we can't get the prompt JSON from the rendered widgets.
27805
+ // We'll just pass in the hint content as a string instead.
27806
+ const hint = this.hintRendererRef.current?.getPromptJSON() || {
27807
+ content: this.props.hint?.content || "",
27808
+ widgets: {}
27809
+ };
27810
+ return getPromptJSON$i(this.props.title, this.rendererRef.current?.getPromptJSON(), hint);
27811
+ }
27639
27812
  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);
27813
+ return this.rendererRef.current?.setInputValue(path, newValue, cb);
27643
27814
  };
27644
27815
  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();
27816
+ return !!this.rendererRef.current?.focus();
27648
27817
  };
27649
27818
  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);
27819
+ this.rendererRef.current?.focusPath(path);
27653
27820
  };
27654
27821
  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);
27822
+ this.rendererRef.current?.blurPath(path);
27658
27823
  };
27659
27824
  render() {
27660
27825
  const apiOptions = ___default["default"].extend({}, ApiOptions.defaults, this.props.apiOptions, {
@@ -27712,8 +27877,7 @@ class GradedGroup extends React__namespace.Component {
27712
27877
  }, !!this.props.title && /*#__PURE__*/React__namespace.createElement("div", {
27713
27878
  className: aphrodite.css(styles$j.title)
27714
27879
  }, this.props.title), /*#__PURE__*/React__namespace.createElement(Renderer, _extends({}, this.props, {
27715
- // eslint-disable-next-line react/no-string-refs
27716
- ref: "renderer",
27880
+ ref: this.rendererRef,
27717
27881
  apiOptions: {
27718
27882
  ...apiOptions,
27719
27883
  readOnly
@@ -27756,8 +27920,7 @@ class GradedGroup extends React__namespace.Component {
27756
27920
  });
27757
27921
  }
27758
27922
  }, 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",
27923
+ ref: this.hintRendererRef,
27761
27924
  apiOptions: apiOptions,
27762
27925
  linterContext: this.props.linterContext,
27763
27926
  strings: this.context.strings
@@ -27849,6 +28012,16 @@ var GradedGroup$1 = {
27849
28012
  isLintable: true
27850
28013
  };
27851
28014
 
28015
+ const getPromptJSON$h = (renderProps, activeGroupJSON) => {
28016
+ return {
28017
+ type: "graded-group-set",
28018
+ options: {
28019
+ groupCount: renderProps.gradedGroups.length,
28020
+ currentGroup: activeGroupJSON
28021
+ }
28022
+ };
28023
+ };
28024
+
27852
28025
  class Indicators extends React__namespace.Component {
27853
28026
  static contextType = PerseusI18nContext;
27854
28027
  handleKeyDown = (e, i) => {
@@ -27927,6 +28100,10 @@ class GradedGroupSet extends React__namespace.Component {
27927
28100
  getInputPaths = () => {
27928
28101
  return this._childGroup.getInputPaths();
27929
28102
  };
28103
+ getPromptJSON() {
28104
+ const activeGroupPromptJSON = this._childGroup.getPromptJSON();
28105
+ return getPromptJSON$h(this.props, activeGroupPromptJSON);
28106
+ }
27930
28107
  setInputValue = (path, newValue, cb) => {
27931
28108
  return this._childGroup.setInputValue(path, newValue, cb);
27932
28109
  };
@@ -28180,6 +28357,33 @@ const styles$h = aphrodite.StyleSheet.create({
28180
28357
  }
28181
28358
  });
28182
28359
 
28360
+ const getPromptJSON$g = (renderProps, userInput) => {
28361
+ const {
28362
+ type,
28363
+ coords
28364
+ } = userInput;
28365
+ const input = {
28366
+ type,
28367
+ coords
28368
+ };
28369
+ if (userInput.type === "logarithm" || userInput.type === "exponential") {
28370
+ input["asymptote"] = userInput.asymptote;
28371
+ }
28372
+ return {
28373
+ type: "grapher",
28374
+ options: {
28375
+ availableTypes: renderProps.availableTypes,
28376
+ range: renderProps.graph.range,
28377
+ labels: renderProps.graph.labels,
28378
+ tickStep: renderProps.graph.step,
28379
+ gridStep: renderProps.graph.gridStep,
28380
+ snapStep: renderProps.graph.snapStep,
28381
+ backgroundImageUrl: renderProps.graph.backgroundImage.url
28382
+ },
28383
+ userInput: input
28384
+ };
28385
+ };
28386
+
28183
28387
  /* eslint-disable one-var */
28184
28388
  // @ts-expect-error - TS2339 - Property 'Plot' does not exist on type 'typeof Graphie'.
28185
28389
  const Plot$1 = Graphie.Plot;
@@ -28703,7 +28907,7 @@ function getCoefficientsByType(data) {
28703
28907
  throw new perseusCore.PerseusError("Invalid grapher type", perseusCore.Errors.InvalidInput);
28704
28908
  }
28705
28909
  }
28706
- function grapherValidator(userInput, rubric) {
28910
+ function scoreGrapher(userInput, rubric) {
28707
28911
  if (userInput.type !== rubric.correct.type) {
28708
28912
  return {
28709
28913
  type: "points",
@@ -29054,6 +29258,9 @@ class Grapher extends React__namespace.Component {
29054
29258
  getUserInput() {
29055
29259
  return Grapher.getUserInputFromProps(this.props);
29056
29260
  }
29261
+ getPromptJSON() {
29262
+ return getPromptJSON$g(this.props, this.getUserInput());
29263
+ }
29057
29264
  render() {
29058
29265
  const type = this.props.plot.type;
29059
29266
  const coords = this.props.plot.coords;
@@ -29143,7 +29350,21 @@ var Grapher$1 = {
29143
29350
  widget: Grapher,
29144
29351
  transform: propTransform$2,
29145
29352
  staticTransform: staticTransform$4,
29146
- validator: grapherValidator
29353
+ scorer: scoreGrapher
29354
+ };
29355
+
29356
+ const getPromptJSON$f = rendererJSON => {
29357
+ if (!rendererJSON) {
29358
+ return {
29359
+ type: "group",
29360
+ content: "",
29361
+ widgets: {}
29362
+ };
29363
+ }
29364
+ return {
29365
+ ...rendererJSON,
29366
+ type: "group"
29367
+ };
29147
29368
  };
29148
29369
 
29149
29370
  // exports has no 'transform'
@@ -29182,6 +29403,9 @@ class Group extends React__namespace.Component {
29182
29403
  getUserInput() {
29183
29404
  return this.rendererRef?.getUserInput();
29184
29405
  }
29406
+ getPromptJSON() {
29407
+ return getPromptJSON$f(this.rendererRef?.getPromptJSON());
29408
+ }
29185
29409
  getSerializedState = () => {
29186
29410
  return this.rendererRef?.getSerializedState();
29187
29411
  };
@@ -29295,7 +29519,11 @@ var Group$1 = {
29295
29519
  isLintable: true
29296
29520
  };
29297
29521
 
29298
- function iframeValidator(userInput) {
29522
+ const getPromptJSON$e = () => {
29523
+ return getUnsupportedPromptJSON("iframe");
29524
+ };
29525
+
29526
+ function scoreIframe(userInput) {
29299
29527
  // The iframe can tell us whether it's correct or incorrect,
29300
29528
  // and pass an optional message
29301
29529
  if (userInput.status === "correct") {
@@ -29352,6 +29580,9 @@ class Iframe extends React__namespace.Component {
29352
29580
  message: this.props.message
29353
29581
  };
29354
29582
  }
29583
+ getPromptJSON() {
29584
+ return getPromptJSON$e();
29585
+ }
29355
29586
  handleMessageEvent = e => {
29356
29587
  // We receive data from the iframe that contains {passed: true/false}
29357
29588
  // and use that to set the status
@@ -29449,7 +29680,19 @@ var Iframe$1 = {
29449
29680
  widget: Iframe,
29450
29681
  // Let's not expose it to all content creators yet
29451
29682
  hidden: true,
29452
- validator: iframeValidator
29683
+ scorer: scoreIframe
29684
+ };
29685
+
29686
+ const getPromptJSON$d = renderProps => {
29687
+ return {
29688
+ type: "image",
29689
+ options: {
29690
+ altText: renderProps.alt,
29691
+ title: renderProps.title,
29692
+ caption: renderProps.caption,
29693
+ imageUrl: renderProps.backgroundImage.url
29694
+ }
29695
+ };
29453
29696
  };
29454
29697
 
29455
29698
  const defaultBoxSize = 400;
@@ -29490,6 +29733,9 @@ class ImageWidget extends React__namespace.Component {
29490
29733
  return change.apply(_this, args);
29491
29734
  };
29492
29735
  })();
29736
+ getPromptJSON() {
29737
+ return getPromptJSON$d(this.props);
29738
+ }
29493
29739
  render() {
29494
29740
  let image;
29495
29741
  const alt = this.props.caption === this.props.alt ? "" : this.props.alt;
@@ -29636,8 +29882,12 @@ var Image$1 = {
29636
29882
  displayName: "Image",
29637
29883
  widget: ImageWidget,
29638
29884
  isLintable: true,
29639
- // TODO: things that aren't interactive shouldn't need validators
29640
- validator: () => noopValidator()
29885
+ // TODO: things that aren't interactive shouldn't need scoring functions
29886
+ scorer: () => scoreNoop()
29887
+ };
29888
+
29889
+ const getPromptJSON$c = () => {
29890
+ return getUnsupportedPromptJSON("interaction");
29641
29891
  };
29642
29892
 
29643
29893
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -29826,6 +30076,9 @@ class Interaction extends React__namespace.Component {
29826
30076
  return change.apply(_this, args);
29827
30077
  };
29828
30078
  })();
30079
+ getPromptJSON() {
30080
+ return getPromptJSON$c();
30081
+ }
29829
30082
  render() {
29830
30083
  const range = this.props.graph.range;
29831
30084
  let labels = this.props.graph.labels;
@@ -30234,8 +30487,8 @@ var Interactive = {
30234
30487
  widget: Interaction,
30235
30488
  transform: ___default["default"].identity,
30236
30489
  hidden: true,
30237
- // TODO: things that aren't interactive shouldn't need validators
30238
- validator: () => noopValidator()
30490
+ // TODO: things that aren't interactive shouldn't need scoring functions
30491
+ scorer: () => scoreNoop()
30239
30492
  };
30240
30493
 
30241
30494
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -30539,6 +30792,137 @@ class Graph extends React__namespace.Component {
30539
30792
  }
30540
30793
  }
30541
30794
 
30795
+ const getPromptJSON$b = (props, userInput) => {
30796
+ return {
30797
+ type: "interactive-graph",
30798
+ options: {
30799
+ graph: getGraphOptionsForProps(props),
30800
+ backgroundImageUrl: props.backgroundImage?.url,
30801
+ range: props.range,
30802
+ labels: props.labels
30803
+ },
30804
+ userInput: getUserInput(userInput)
30805
+ };
30806
+ };
30807
+ const getGraphOptionsForProps = props => {
30808
+ const type = props.graph.type;
30809
+ switch (type) {
30810
+ case "angle":
30811
+ return {
30812
+ type: props.graph.type,
30813
+ angleOffsetDegrees: props.graph.angleOffsetDeg,
30814
+ startCoords: props.graph.startCoords
30815
+ };
30816
+ case "circle":
30817
+ return {
30818
+ type: props.graph.type,
30819
+ startParams: {
30820
+ center: props.graph.startCoords?.center,
30821
+ radius: props.graph.startCoords?.radius
30822
+ }
30823
+ };
30824
+ case "linear":
30825
+ return {
30826
+ type: props.graph.type,
30827
+ startCoords: props.graph.startCoords
30828
+ };
30829
+ case "linear-system":
30830
+ return {
30831
+ type: props.graph.type,
30832
+ startCoords: props.graph.startCoords
30833
+ };
30834
+ case "point":
30835
+ return {
30836
+ type: props.graph.type,
30837
+ numPoints: props.graph.numPoints,
30838
+ startCoords: props.graph.startCoords
30839
+ };
30840
+ case "polygon":
30841
+ return {
30842
+ type: props.graph.type,
30843
+ match: props.graph.match,
30844
+ numSides: props.graph.numSides,
30845
+ startCoords: props.graph.startCoords
30846
+ };
30847
+ case "quadratic":
30848
+ return {
30849
+ type: props.graph.type,
30850
+ startCoords: props.graph.startCoords
30851
+ };
30852
+ case "ray":
30853
+ return {
30854
+ type: props.graph.type,
30855
+ startCoords: props.graph.startCoords
30856
+ };
30857
+ case "segment":
30858
+ return {
30859
+ type: props.graph.type,
30860
+ numSegments: props.graph.numSegments,
30861
+ startCoords: props.graph.startCoords
30862
+ };
30863
+ case "sinusoid":
30864
+ return {
30865
+ type: props.graph.type,
30866
+ startCoords: props.graph.startCoords
30867
+ };
30868
+ case "none":
30869
+ return {};
30870
+ default:
30871
+ throw new wonderStuffCore.UnreachableCaseError(type);
30872
+ }
30873
+ };
30874
+ const getUserInput = userInput => {
30875
+ const type = userInput.type;
30876
+ switch (type) {
30877
+ case "angle":
30878
+ return {
30879
+ coords: userInput.coords,
30880
+ angleOffsetDegrees: userInput.angleOffsetDeg
30881
+ };
30882
+ case "circle":
30883
+ return {
30884
+ center: userInput.center,
30885
+ radius: userInput.radius
30886
+ };
30887
+ case "linear":
30888
+ return {
30889
+ coords: userInput.coords
30890
+ };
30891
+ case "linear-system":
30892
+ return {
30893
+ coords: userInput.coords
30894
+ };
30895
+ case "point":
30896
+ return {
30897
+ coords: userInput.coords
30898
+ };
30899
+ case "polygon":
30900
+ return {
30901
+ coords: userInput.coords
30902
+ };
30903
+ case "quadratic":
30904
+ return {
30905
+ coords: userInput.coords
30906
+ };
30907
+ case "ray":
30908
+ return {
30909
+ coords: userInput.coords
30910
+ };
30911
+ case "segment":
30912
+ return {
30913
+ coords: userInput.coords
30914
+ };
30915
+ case "sinusoid":
30916
+ return {
30917
+ coords: userInput.coords
30918
+ };
30919
+ case "none":
30920
+ return {};
30921
+ default:
30922
+ throw new wonderStuffCore.UnreachableCaseError(type);
30923
+ }
30924
+ };
30925
+
30542
30926
  function pathBuilder() {
30543
30927
  return new PathBuilder();
30544
30928
  }
@@ -31790,6 +32174,10 @@ const LegacyGrid = _ref => {
31790
32174
  * To render more complex Perseus items, see the `Item` type in the multi item
31791
32175
  * area.
31792
32176
  */
32177
+ /**
32178
+ * A "PerseusArticle" is an item that is meant to be rendered as an article.
32179
+ * This item is never scored and is rendered by the `ArticleRenderer`.
32180
+ */
31793
32181
  /**
31794
32182
  * A "MultiItem" is an advanced Perseus item. It is rendered by the
31795
32183
  * `MultiRenderer` and you can control the layout of individual parts of the
@@ -31886,6 +32274,10 @@ const ItemExtras = [
31886
32274
  // A background image applied to various widgets.
31887
32275
 
31888
32276
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
32277
+
32278
+ // 2D range: xMin, xMax, yMin, yMax
32279
+
32280
+ const lockedFigureColorNames = ["blue", "green", "grayH", "purple", "pink", "orange", "red"];
31889
32281
  const lockedFigureColors = {
31890
32282
  blue: "#3D7586",
31891
32283
  green: "#447A53",
@@ -31953,16 +32345,57 @@ function isUnlimitedGraphState(state) {
31953
32345
  * and the text outside of the $ blocks will be non-TeX text.
31954
32346
  */
31955
32347
  function replaceOutsideTeX(mathString) {
31956
- let currentlyTeX = mathString[0] === "$";
32348
+ // All the information we need is in the first section,
32349
+ // whether it's typed as "blockmath" or "paragraph"
32350
+ const firstSection = SimpleMarkdown__namespace.parse(mathString)[0];
32351
+
32352
+ // If it's blockMath, the outer level has the full math content.
32353
+ if (firstSection.type === "blockMath") {
32354
+ return firstSection.content;
32355
+ }
32356
+
32357
+ // If it's a paragraph, we need to iterate through the sections
32358
+ // to look for individual math blocks.
32359
+ const condensedNodes = condenseTextNodes(firstSection.content);
31957
32360
  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;
32361
+ for (const piece of condensedNodes) {
32362
+ piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
31963
32363
  }
31964
32364
  return result;
31965
32365
  }
32366
+ // Helper function for replaceOutsideTeX()
32367
+ // Condense adjacent text nodes into a single text node
32368
+ function condenseTextNodes(nodes) {
32369
+ const result = [];
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
+ });
32390
+ }
32391
+ return result;
32392
+ }
32393
+
32394
+ // Helper function for replaceOutsideTeX()
32395
+ function escapeSpecialChars(str) {
32396
+ // Escape $, \, {, and } characters
32397
+ return str.replace(/([$\\{}])/g, "\\$1");
32398
+ }
31966
32399
 
31967
32400
  function LockedLabel(props) {
31968
32401
  const {
@@ -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
40548
+ };
40549
+
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
+ };
40064
40561
  };
40065
40562
 
40066
- function matrixValidator(userInput, rubric, strings) {
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()
41543
42050
  };
41544
42051
 
41545
- function numberLineValidator(state, rubric) {
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
+ };
42065
+ };
42066
+
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,10 +44509,10 @@ 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) {
44515
+ function scoreTable(state, rubric, strings) {
43942
44516
  const filterNonEmpty = function (table) {
43943
44517
  return ___default["default"].filter(table, function (row) {
43944
44518
  // Check if row has a cell that is nonempty
@@ -44212,7 +44786,11 @@ var Table$1 = {
44212
44786
  transform: propTransform,
44213
44787
  hidden: true,
44214
44788
  isLintable: true,
44215
- validator: tableValidator
44789
+ scorer: scoreTable
44790
+ };
44791
+
44792
+ const getPromptJSON = () => {
44793
+ return getUnsupportedPromptJSON("video");
44216
44794
  };
44217
44795
 
44218
44796
  /**
@@ -44311,6 +44889,9 @@ class Video extends React__namespace.Component {
44311
44889
  return change.apply(_this, args);
44312
44890
  };
44313
44891
  })();
44892
+ getPromptJSON() {
44893
+ return getPromptJSON();
44894
+ }
44314
44895
  render() {
44315
44896
  const {
44316
44897
  InitialRequestUrl
@@ -44370,8 +44951,8 @@ var Video$1 = {
44370
44951
  defaultAlignment: "block",
44371
44952
  supportedAlignments: ["block", "float-left", "float-right", "full-width"],
44372
44953
  widget: Video,
44373
- // TODO: things that aren't interactive shouldn't need validators
44374
- validator: () => noopValidator()
44954
+ // TODO: things that aren't interactive shouldn't need scoring functions
44955
+ scorer: () => scoreNoop()
44375
44956
  };
44376
44957
 
44377
44958
  /* eslint-disable import/no-named-as-default */
@@ -44390,7 +44971,7 @@ const init = function () {
44390
44971
 
44391
44972
  // This file is processed by a Rollup plugin (replace) to inject the production
44392
44973
  const libName = "@khanacademy/perseus";
44393
- const libVersion = "41.2.0";
44974
+ const libVersion = "41.3.0";
44394
44975
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
44395
44976
 
44396
44977
  const apiVersion = {
@@ -45126,6 +45707,9 @@ class ServerItemRenderer extends React__namespace.Component {
45126
45707
  getNumHints() {
45127
45708
  return this.props.item.hints.length;
45128
45709
  }
45710
+ getPromptJSON() {
45711
+ return this.questionRenderer.getPromptJSON();
45712
+ }
45129
45713
 
45130
45714
  /**
45131
45715
  * Grades the item.
@@ -45783,7 +46367,7 @@ function treeToItem(node) {
45783
46367
  };
45784
46368
  }
45785
46369
 
45786
- var isObject = function (obj) {
46370
+ var isObject$1 = function (obj) {
45787
46371
  return obj === Object(obj);
45788
46372
  };
45789
46373
  var merge = function () {
@@ -45799,7 +46383,7 @@ var merge = function () {
45799
46383
  return obj;
45800
46384
  };
45801
46385
  var clone = function (obj) {
45802
- if (!isObject(obj)) {
46386
+ if (!isObject$1(obj)) {
45803
46387
  return obj;
45804
46388
  }
45805
46389
  return Array.isArray(obj) ? obj.slice() : merge(obj);
@@ -45815,7 +46399,7 @@ var clone = function (obj) {
45815
46399
  var dispatch = function (x) {
45816
46400
  if (Array.isArray(x)) {
45817
46401
  return arr;
45818
- } else if (isObject(x)) {
46402
+ } else if (isObject$1(x)) {
45819
46403
  return obj;
45820
46404
  } else if (typeof x === "string") {
45821
46405
  return str;
@@ -47739,6 +48323,697 @@ function buildTestData(testObject) {
47739
48323
  return `{"data":{"assessmentItem":{"__typename":"AssessmentItemOrError","error":null,"item":{"__typename":"AssessmentItem","id":"x890b3c70f3e8f4a6","itemData":"${testObject}","problemType":"Type 1","sha":"c7284a3ad65214b4e62bccce236d92f7f5d35941"}}}}`;
47740
48324
  }
47741
48325
 
48326
+ function success(value) {
48327
+ return {
48328
+ type: "success",
48329
+ value
48330
+ };
48331
+ }
48332
+ function failure(detail) {
48333
+ return {
48334
+ type: "failure",
48335
+ detail
48336
+ };
48337
+ }
48338
+ function isFailure(result) {
48339
+ return result.type === "failure";
48340
+ }
48341
+ function isSuccess(result) {
48342
+ return result.type === "success";
48343
+ }
48344
+
48345
+ // Result's `all` function is similar to Promise.all: given an array of
48346
+ // results, it returns success if all succeeded, and failure if any failed.
48347
+ function all(results) {
48348
+ let combineFailureDetails = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : a => a;
48349
+ const values = [];
48350
+ const failureDetails = [];
48351
+ for (const result of results) {
48352
+ if (result.type === "success") {
48353
+ values.push(result.value);
48354
+ } else {
48355
+ failureDetails.push(result.detail);
48356
+ }
48357
+ }
48358
+ if (failureDetails.length > 0) {
48359
+ return failure(failureDetails.reduce(combineFailureDetails));
48360
+ }
48361
+ return success(values);
48362
+ }
48363
+
48364
+ const any = (rawValue, ctx) => ctx.success(rawValue);
48365
+
48366
+ function array(elementParser) {
48367
+ return (rawValue, ctx) => {
48368
+ if (!Array.isArray(rawValue)) {
48369
+ return ctx.failure("array", rawValue);
48370
+ }
48371
+ const elementResults = rawValue.map((elem, i) => elementParser(elem, ctx.forSubtree(i)));
48372
+ return all(elementResults, concat);
48373
+ };
48374
+ }
48375
+ function concat(a, b) {
48376
+ return [...a, ...b];
48377
+ }
48378
+
48379
+ function boolean(rawValue, ctx) {
48380
+ if (typeof rawValue === "boolean") {
48381
+ return ctx.success(rawValue);
48382
+ }
48383
+ return ctx.failure("boolean", rawValue);
48384
+ }
48385
+
48386
+ function constant(acceptedValue) {
48387
+ return (rawValue, ctx) => {
48388
+ if (rawValue !== acceptedValue) {
48389
+ return ctx.failure(String(JSON.stringify(acceptedValue)), rawValue);
48390
+ }
48391
+ return ctx.success(acceptedValue);
48392
+ };
48393
+ }
48394
+
48395
+ function enumeration() {
48396
+ for (var _len = arguments.length, acceptedValues = new Array(_len), _key = 0; _key < _len; _key++) {
48397
+ acceptedValues[_key] = arguments[_key];
48398
+ }
48399
+ return (rawValue, ctx) => {
48400
+ if (typeof rawValue === "string") {
48401
+ const index = acceptedValues.indexOf(rawValue);
48402
+ if (index > -1) {
48403
+ return ctx.success(acceptedValues[index]);
48404
+ }
48405
+ }
48406
+ const expected = acceptedValues.map(v => JSON.stringify(v));
48407
+ return ctx.failure(expected, rawValue);
48408
+ };
48409
+ }
48410
+
48411
+ function isObject(x) {
48412
+ return x != null && Object.getPrototypeOf(x) === Object.prototype;
48413
+ }
48414
+
48415
+ function nullable(parseValue) {
48416
+ return (rawValue, ctx) => {
48417
+ if (rawValue === null) {
48418
+ return ctx.success(rawValue);
48419
+ }
48420
+ return parseValue(rawValue, ctx);
48421
+ };
48422
+ }
48423
+
48424
+ const number = (rawValue, ctx) => {
48425
+ if (typeof rawValue === "number") {
48426
+ return ctx.success(rawValue);
48427
+ }
48428
+ return ctx.failure("number", rawValue);
48429
+ };
48430
+
48431
+ function object(schema) {
48432
+ return (rawValue, ctx) => {
48433
+ if (!isObject(rawValue)) {
48434
+ return ctx.failure("object", rawValue);
48435
+ }
48436
+ const ret = {
48437
+ ...rawValue
48438
+ };
48439
+ const mismatches = [];
48440
+ for (const [prop, propParser] of Object.entries(schema)) {
48441
+ const result = propParser(rawValue[prop], ctx.forSubtree(prop));
48442
+ if (isSuccess(result)) {
48443
+ ret[prop] = result.value;
48444
+ } else {
48445
+ mismatches.push(...result.detail);
48446
+ }
48447
+ }
48448
+ if (mismatches.length > 0) {
48449
+ return failure(mismatches);
48450
+ }
48451
+ return ctx.success(ret);
48452
+ };
48453
+ }
48454
+
48455
+ function optional(parseValue) {
48456
+ return (rawValue, ctx) => {
48457
+ if (rawValue === undefined) {
48458
+ return ctx.success(rawValue);
48459
+ }
48460
+ return parseValue(rawValue, ctx);
48461
+ };
48462
+ }
48463
+
48464
+ function pair(parseA, parseB) {
48465
+ return (rawValue, ctx) => {
48466
+ if (!Array.isArray(rawValue)) {
48467
+ return ctx.failure("array", rawValue);
48468
+ }
48469
+ if (rawValue.length !== 2) {
48470
+ return ctx.failure("array of length 2", rawValue);
48471
+ }
48472
+ const [rawA, rawB] = rawValue;
48473
+ const resultA = parseA(rawA, ctx.forSubtree(0));
48474
+ if (isFailure(resultA)) {
48475
+ return resultA;
48476
+ }
48477
+ const resultB = parseB(rawB, ctx.forSubtree(1));
48478
+ if (isFailure(resultB)) {
48479
+ return resultB;
48480
+ }
48481
+ return ctx.success([resultA.value, resultB.value]);
48482
+ };
48483
+ }
48484
+
48485
+ function record(parseKey, parseValue) {
48486
+ return (rawValue, ctx) => {
48487
+ if (!isObject(rawValue)) {
48488
+ return ctx.failure("object", rawValue);
48489
+ }
48490
+ const result = {};
48491
+ const mismatches = [];
48492
+ for (const [key, value] of Object.entries(rawValue)) {
48493
+ const entryCtx = ctx.forSubtree(key);
48494
+ const keyResult = parseKey(key, entryCtx);
48495
+ if (isFailure(keyResult)) {
48496
+ mismatches.push(...keyResult.detail);
48497
+ }
48498
+ const valueResult = parseValue(value, entryCtx);
48499
+ if (isFailure(valueResult)) {
48500
+ mismatches.push(...valueResult.detail);
48501
+ }
48502
+ if (isSuccess(keyResult) && isSuccess(valueResult)) {
48503
+ result[keyResult.value] = valueResult.value;
48504
+ }
48505
+ }
48506
+ if (mismatches.length > 0) {
48507
+ return failure(mismatches);
48508
+ }
48509
+ return ctx.success(result);
48510
+ };
48511
+ }
48512
+
48513
+ const string = (rawValue, ctx) => {
48514
+ if (typeof rawValue === "string") {
48515
+ return ctx.success(rawValue);
48516
+ }
48517
+ return ctx.failure("string", rawValue);
48518
+ };
48519
+
48520
+ function trio(parseA, parseB, parseC) {
48521
+ return (rawValue, ctx) => {
48522
+ if (!Array.isArray(rawValue)) {
48523
+ return ctx.failure("array", rawValue);
48524
+ }
48525
+ if (rawValue.length !== 3) {
48526
+ return ctx.failure("array of length 3", rawValue);
48527
+ }
48528
+ const resultA = parseA(rawValue[0], ctx.forSubtree(0));
48529
+ if (isFailure(resultA)) {
48530
+ return resultA;
48531
+ }
48532
+ const resultB = parseB(rawValue[1], ctx.forSubtree(1));
48533
+ if (isFailure(resultB)) {
48534
+ return resultB;
48535
+ }
48536
+ const resultC = parseC(rawValue[2], ctx.forSubtree(2));
48537
+ if (isFailure(resultC)) {
48538
+ return resultC;
48539
+ }
48540
+ return ctx.success([resultA.value, resultB.value, resultC.value]);
48541
+ };
48542
+ }
48543
+
48544
+ function union(parseBranch) {
48545
+ return new UnionBuilder(parseBranch);
48546
+ }
48547
+ class UnionBuilder {
48548
+ constructor(parser) {
48549
+ this.parser = parser;
48550
+ }
48551
+ or(newBranch) {
48552
+ return new UnionBuilder(either(this.parser, newBranch));
48553
+ }
48554
+ }
48555
+ function either(parseA, parseB) {
48556
+ return (rawValue, ctx) => {
48557
+ const resultA = parseA(rawValue, ctx);
48558
+ if (isSuccess(resultA)) {
48559
+ return resultA;
48560
+ }
48561
+ return parseB(rawValue, ctx);
48562
+ };
48563
+ }
48564
+
48565
+ function parseWidget(parseType, parseOptions) {
48566
+ return object({
48567
+ type: parseType,
48568
+ static: optional(boolean),
48569
+ graded: optional(boolean),
48570
+ alignment: optional(string),
48571
+ options: parseOptions,
48572
+ key: optional(number),
48573
+ version: object({
48574
+ major: number,
48575
+ minor: number
48576
+ })
48577
+ });
48578
+ }
48579
+
48580
+ const parseCategorizerWidget = parseWidget(constant("categorizer"), object({
48581
+ items: array(string),
48582
+ categories: array(string),
48583
+ randomizeItems: boolean,
48584
+ static: boolean,
48585
+ values: array(number),
48586
+ highlightLint: optional(boolean),
48587
+ linterContext: optional(object({
48588
+ contentType: string,
48589
+ paths: array(string),
48590
+ stack: array(string)
48591
+ }))
48592
+ }));
48593
+
48594
+ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
48595
+ programID: string,
48596
+ programType: any,
48597
+ settings: array(object({
48598
+ name: string,
48599
+ value: string
48600
+ })),
48601
+ showEditor: boolean,
48602
+ showButtons: boolean,
48603
+ width: number,
48604
+ height: number,
48605
+ static: boolean
48606
+ }));
48607
+
48608
+ const parseDefinitionWidget = parseWidget(constant("definition"), object({
48609
+ togglePrompt: string,
48610
+ definition: string,
48611
+ static: boolean
48612
+ }));
48613
+
48614
+ const parseDropdownWidget = parseWidget(constant("dropdown"), object({
48615
+ placeholder: string,
48616
+ static: boolean,
48617
+ choices: array(object({
48618
+ content: string,
48619
+ correct: boolean
48620
+ }))
48621
+ }));
48622
+
48623
+ const parseExplanationWidget = parseWidget(constant("explanation"), object({
48624
+ showPrompt: string,
48625
+ hidePrompt: string,
48626
+ explanation: string,
48627
+ // We wrap parseWidgetsMap in a function here to make sure it is not
48628
+ // referenced before it is defined. There is an import cycle between
48629
+ // this file and widgets-map.ts that could cause it to be undefined.
48630
+ widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
48631
+ static: boolean
48632
+ }));
48633
+
48634
+ const parseAnswerForm = object({
48635
+ value: string,
48636
+ form: boolean,
48637
+ simplify: boolean,
48638
+ considered: enumeration("correct", "wrong", "ungraded"),
48639
+ key: optional(string)
48640
+ });
48641
+ const parseExpressionWidget = parseWidget(constant("expression"), object({
48642
+ answerForms: array(parseAnswerForm),
48643
+ functions: array(string),
48644
+ times: boolean,
48645
+ visibleLabel: optional(string),
48646
+ ariaLabel: optional(string),
48647
+ buttonSets: array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")),
48648
+ buttonsVisible: optional(enumeration("always", "never", "focused"))
48649
+ }));
48650
+
48651
+ const parsePerseusImageBackground = object({
48652
+ url: optional(nullable(string)),
48653
+ width: optional(number),
48654
+ height: optional(number),
48655
+ top: optional(number),
48656
+ left: optional(number),
48657
+ bottom: optional(number),
48658
+ // TODO(benchristel): convert scale to a number
48659
+ scale: optional(union(number).or(string).parser)
48660
+ });
48661
+
48662
+ // Used to represent 2-D points and ranges
48663
+ const pairOfNumbers = pair(number, number);
48664
+ const parsePerseusGraphTypeAngle = object({
48665
+ type: constant("angle"),
48666
+ showAngles: optional(boolean),
48667
+ allowReflexAngles: optional(boolean),
48668
+ angleOffsetDeg: optional(number),
48669
+ snapDegrees: optional(number),
48670
+ match: optional(constant("congruent")),
48671
+ coords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
48672
+ startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))
48673
+ });
48674
+ const parsePerseusGraphTypeCircle = object({
48675
+ type: constant("circle"),
48676
+ center: optional(pairOfNumbers),
48677
+ radius: optional(number),
48678
+ startCoords: optional(object({
48679
+ center: pairOfNumbers,
48680
+ radius: number
48681
+ })),
48682
+ // TODO: remove coord? it's legacy.
48683
+ coord: optional(pairOfNumbers)
48684
+ });
48685
+ const parsePerseusGraphTypeLinear = object({
48686
+ type: constant("linear"),
48687
+ coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
48688
+ startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
48689
+ // TODO: remove coord? it's legacy.
48690
+ coord: optional(pairOfNumbers)
48691
+ });
48692
+ const parsePerseusGraphTypeLinearSystem = object({
48693
+ type: constant("linear-system"),
48694
+ // TODO(benchristel): default coords to empty array?
48695
+ coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
48696
+ startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
48697
+ // TODO: remove coord? it's legacy.
48698
+ coord: optional(pairOfNumbers)
48699
+ });
48700
+ const parsePerseusGraphTypeNone = object({
48701
+ type: constant("none")
48702
+ });
48703
+ const parsePerseusGraphTypePoint = object({
48704
+ type: constant("point"),
48705
+ numPoints: optional(union(number).or(constant("unlimited")).parser),
48706
+ coords: optional(nullable(array(pairOfNumbers))),
48707
+ startCoords: optional(array(pairOfNumbers)),
48708
+ // TODO: remove coord? it's legacy.
48709
+ coord: optional(pairOfNumbers)
48710
+ });
48711
+ const parsePerseusGraphTypePolygon = object({
48712
+ type: constant("polygon"),
48713
+ numSides: optional(union(number).or(constant("unlimited")).parser),
48714
+ showAngles: optional(boolean),
48715
+ showSides: optional(boolean),
48716
+ snapTo: optional(enumeration("grid", "angles", "sides")),
48717
+ match: optional(enumeration("similar", "congruent", "approx")),
48718
+ startCoords: optional(array(pairOfNumbers)),
48719
+ // TODO: remove coord? it's legacy.
48720
+ coord: optional(pairOfNumbers)
48721
+ });
48722
+ const parsePerseusGraphTypeQuadratic = object({
48723
+ type: constant("quadratic"),
48724
+ coords: optional(nullable(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))),
48725
+ startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
48726
+ // TODO: remove coord? it's legacy.
48727
+ coord: optional(pairOfNumbers)
48728
+ });
48729
+ const parsePerseusGraphTypeRay = object({
48730
+ type: constant("ray"),
48731
+ coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
48732
+ startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
48733
+ // TODO: remove coord? it's legacy.
48734
+ coord: optional(pairOfNumbers)
48735
+ });
48736
+ const parsePerseusGraphTypeSegment = object({
48737
+ type: constant("segment"),
48738
+ // TODO(benchristel): default numSegments?
48739
+ numSegments: optional(number),
48740
+ coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
48741
+ startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
48742
+ // TODO: remove coord? it's legacy.
48743
+ coord: optional(pairOfNumbers)
48744
+ });
48745
+ const parsePerseusGraphTypeSinusoid = object({
48746
+ type: constant("sinusoid"),
48747
+ coords: optional(nullable(array(pairOfNumbers))),
48748
+ startCoords: optional(array(pairOfNumbers)),
48749
+ // TODO: remove coord? it's legacy.
48750
+ coord: optional(pairOfNumbers)
48751
+ });
48752
+ 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;
48753
+ const parseLockedFigureColor = enumeration(...lockedFigureColorNames);
48754
+ const parseLockedFigureFillType = enumeration("none", "white", "translucent", "solid");
48755
+ const parseLockedLineStyle = enumeration("solid", "dashed");
48756
+ const parseLockedLabelType = object({
48757
+ type: constant("label"),
48758
+ coord: pairOfNumbers,
48759
+ text: string,
48760
+ color: parseLockedFigureColor,
48761
+ size: enumeration("small", "medium", "large")
48762
+ });
48763
+ const parseLockedPointType = object({
48764
+ type: constant("point"),
48765
+ coord: pairOfNumbers,
48766
+ color: parseLockedFigureColor,
48767
+ filled: boolean,
48768
+ // TODO(benchristel): default labels to empty array?
48769
+ labels: optional(array(parseLockedLabelType)),
48770
+ ariaLabel: optional(string)
48771
+ });
48772
+ const parseLockedLineType = object({
48773
+ type: constant("line"),
48774
+ kind: enumeration("line", "ray", "segment"),
48775
+ points: pair(parseLockedPointType, parseLockedPointType),
48776
+ color: parseLockedFigureColor,
48777
+ lineStyle: parseLockedLineStyle,
48778
+ showPoint1: boolean,
48779
+ showPoint2: boolean,
48780
+ // TODO(benchristel): default labels to empty array?
48781
+ labels: optional(array(parseLockedLabelType)),
48782
+ ariaLabel: optional(string)
48783
+ });
48784
+ const parseLockedVectorType = object({
48785
+ type: constant("vector"),
48786
+ points: pair(pairOfNumbers, pairOfNumbers),
48787
+ color: parseLockedFigureColor,
48788
+ // TODO(benchristel): default labels to empty array?
48789
+ labels: optional(array(parseLockedLabelType)),
48790
+ ariaLabel: optional(string)
48791
+ });
48792
+ const parseLockedEllipseType = object({
48793
+ type: constant("ellipse"),
48794
+ center: pairOfNumbers,
48795
+ radius: pairOfNumbers,
48796
+ angle: number,
48797
+ color: parseLockedFigureColor,
48798
+ fillStyle: parseLockedFigureFillType,
48799
+ strokeStyle: parseLockedLineStyle,
48800
+ // TODO(benchristel): default labels to empty array?
48801
+ labels: optional(array(parseLockedLabelType)),
48802
+ ariaLabel: optional(string)
48803
+ });
48804
+ const parseLockedPolygonType = object({
48805
+ type: constant("polygon"),
48806
+ points: array(pairOfNumbers),
48807
+ color: parseLockedFigureColor,
48808
+ showVertices: boolean,
48809
+ fillStyle: parseLockedFigureFillType,
48810
+ strokeStyle: parseLockedLineStyle,
48811
+ // TODO(benchristel): default labels to empty array?
48812
+ labels: optional(array(parseLockedLabelType)),
48813
+ ariaLabel: optional(string)
48814
+ });
48815
+ const parseLockedFunctionType = object({
48816
+ type: constant("function"),
48817
+ color: parseLockedFigureColor,
48818
+ strokeStyle: parseLockedLineStyle,
48819
+ equation: string,
48820
+ directionalAxis: enumeration("x", "y"),
48821
+ domain: optional(pairOfNumbers),
48822
+ // TODO(benchristel): default labels to empty array?
48823
+ labels: optional(array(parseLockedLabelType)),
48824
+ ariaLabel: optional(string)
48825
+ });
48826
+ const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
48827
+ const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
48828
+ step: pairOfNumbers,
48829
+ gridStep: pairOfNumbers,
48830
+ snapStep: pairOfNumbers,
48831
+ backgroundImage: optional(parsePerseusImageBackground),
48832
+ markings: enumeration("graph", "grid", "none"),
48833
+ labels: array(string),
48834
+ showProtractor: boolean,
48835
+ showRuler: optional(boolean),
48836
+ showTooltips: optional(boolean),
48837
+ rulerLabel: optional(string),
48838
+ rulerTicks: optional(number),
48839
+ range: pair(pairOfNumbers, pairOfNumbers),
48840
+ graph: parsePerseusGraphType,
48841
+ correct: parsePerseusGraphType,
48842
+ // TODO(benchristel): default lockedFigures to empty array
48843
+ lockedFigures: optional(array(parseLockedFigure)),
48844
+ fullGraphLabel: optional(string),
48845
+ fullGraphAriaDescription: optional(string)
48846
+ }));
48847
+
48848
+ const parseWidgetsMap = (rawValue, ctx) => {
48849
+ if (!isObject(rawValue)) {
48850
+ return ctx.failure("PerseusWidgetsMap", rawValue);
48851
+ }
48852
+ const widgetsMap = {};
48853
+ for (const key of Object.keys(rawValue)) {
48854
+ // parseWidgetsMapEntry modifies the widgetsMap. This is kind of gross,
48855
+ // but it's the only way I could find to make TypeScript check the key
48856
+ // against the widget type.
48857
+ const entryResult = parseWidgetsMapEntry([key, rawValue[key]], widgetsMap, ctx.forSubtree(key));
48858
+ if (isFailure(entryResult)) {
48859
+ return entryResult;
48860
+ }
48861
+ }
48862
+ return ctx.success(widgetsMap);
48863
+ };
48864
+ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
48865
+ let [key, widget] = _ref;
48866
+ const keyComponentsResult = parseWidgetMapKeyComponents(key.split(" "), ctx);
48867
+ if (isFailure(keyComponentsResult)) {
48868
+ return keyComponentsResult;
48869
+ }
48870
+ const [type, id] = keyComponentsResult.value;
48871
+ function parseAndAssign(key, parse) {
48872
+ const widgetResult = parse(widget, ctx);
48873
+ if (isFailure(widgetResult)) {
48874
+ return widgetResult;
48875
+ }
48876
+ widgetMap[key] = widgetResult.value;
48877
+ return ctx.success(undefined);
48878
+ }
48879
+ switch (type) {
48880
+ case "categorizer":
48881
+ return parseAndAssign(`categorizer ${id}`, parseCategorizerWidget);
48882
+ case "cs-program":
48883
+ return parseAndAssign(`cs-program ${id}`, parseCSProgramWidget);
48884
+ case "definition":
48885
+ return parseAndAssign(`definition ${id}`, parseDefinitionWidget);
48886
+ case "dropdown":
48887
+ return parseAndAssign(`dropdown ${id}`, parseDropdownWidget);
48888
+ case "explanation":
48889
+ return parseAndAssign(`explanation ${id}`, parseExplanationWidget);
48890
+ case "expression":
48891
+ return parseAndAssign(`expression ${id}`, parseExpressionWidget);
48892
+ case "grapher":
48893
+ // TODO(LEMS-2585): implement a real parser for this widget
48894
+ return parseAndAssign(`grapher ${id}`, any);
48895
+ case "group":
48896
+ // TODO(LEMS-2585): implement a real parser for this widget
48897
+ return parseAndAssign(`group ${id}`, any);
48898
+ case "graded-group":
48899
+ // TODO(LEMS-2585): implement a real parser for this widget
48900
+ return parseAndAssign(`graded-group ${id}`, any);
48901
+ case "graded-group-set":
48902
+ // TODO(LEMS-2585): implement a real parser for this widget
48903
+ return parseAndAssign(`graded-group-set ${id}`, any);
48904
+ case "iframe":
48905
+ // TODO(LEMS-2585): implement a real parser for this widget
48906
+ return parseAndAssign(`iframe ${id}`, any);
48907
+ case "image":
48908
+ // TODO(LEMS-2585): implement a real parser for this widget
48909
+ return parseAndAssign(`image ${id}`, any);
48910
+ case "input-number":
48911
+ // TODO(LEMS-2585): implement a real parser for this widget
48912
+ return parseAndAssign(`input-number ${id}`, any);
48913
+ case "interaction":
48914
+ // TODO(LEMS-2585): implement a real parser for this widget
48915
+ return parseAndAssign(`interaction ${id}`, any);
48916
+ case "interactive-graph":
48917
+ return parseAndAssign(`interactive-graph ${id}`, parseInteractiveGraphWidget);
48918
+ case "label-image":
48919
+ // TODO(LEMS-2585): implement a real parser for this widget
48920
+ return parseAndAssign(`label-image ${id}`, any);
48921
+ case "matcher":
48922
+ // TODO(LEMS-2585): implement a real parser for this widget
48923
+ return parseAndAssign(`matcher ${id}`, any);
48924
+ case "matrix":
48925
+ // TODO(LEMS-2585): implement a real parser for this widget
48926
+ return parseAndAssign(`matrix ${id}`, any);
48927
+ case "measurer":
48928
+ // TODO(LEMS-2585): implement a real parser for this widget
48929
+ return parseAndAssign(`measurer ${id}`, any);
48930
+ case "molecule-renderer":
48931
+ // TODO(LEMS-2585): implement a real parser for this widget
48932
+ return parseAndAssign(`molecule-renderer ${id}`, any);
48933
+ case "number-line":
48934
+ // TODO(LEMS-2585): implement a real parser for this widget
48935
+ return parseAndAssign(`number-line ${id}`, any);
48936
+ case "numeric-input":
48937
+ // TODO(LEMS-2585): implement a real parser for this widget
48938
+ return parseAndAssign(`numeric-input ${id}`, any);
48939
+ case "orderer":
48940
+ // TODO(LEMS-2585): implement a real parser for this widget
48941
+ return parseAndAssign(`orderer ${id}`, any);
48942
+ case "passage":
48943
+ // TODO(LEMS-2585): implement a real parser for this widget
48944
+ return parseAndAssign(`passage ${id}`, any);
48945
+ case "passage-ref":
48946
+ // TODO(LEMS-2585): implement a real parser for this widget
48947
+ return parseAndAssign(`passage-ref ${id}`, any);
48948
+ case "passage-ref-target":
48949
+ // TODO(LEMS-2585): implement a real parser for this widget
48950
+ return parseAndAssign(`passage-ref-target ${id}`, any);
48951
+ case "phet-simulation":
48952
+ // TODO(LEMS-2585): implement a real parser for this widget
48953
+ return parseAndAssign(`phet-simulation ${id}`, any);
48954
+ case "plotter":
48955
+ // TODO(LEMS-2585): implement a real parser for this widget
48956
+ return parseAndAssign(`plotter ${id}`, any);
48957
+ case "python-program":
48958
+ // TODO(LEMS-2585): implement a real parser for this widget
48959
+ return parseAndAssign(`python-program ${id}`, any);
48960
+ case "radio":
48961
+ // TODO(LEMS-2585): implement a real parser for this widget
48962
+ return parseAndAssign(`radio ${id}`, any);
48963
+ case "sorter":
48964
+ // TODO(LEMS-2585): implement a real parser for this widget
48965
+ return parseAndAssign(`sorter ${id}`, any);
48966
+ case "table":
48967
+ // TODO(LEMS-2585): implement a real parser for this widget
48968
+ return parseAndAssign(`table ${id}`, any);
48969
+ case "video":
48970
+ // TODO(LEMS-2585): implement a real parser for this widget
48971
+ return parseAndAssign(`video ${id}`, any);
48972
+ default:
48973
+ return ctx.failure("a valid widget type", type);
48974
+ }
48975
+ };
48976
+ const parseStringToPositiveInt = (rawValue, ctx) => {
48977
+ if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
48978
+ return ctx.failure("numeric string", rawValue);
48979
+ }
48980
+ return ctx.success(+rawValue);
48981
+ };
48982
+ const parseWidgetMapKeyComponents = pair(string, parseStringToPositiveInt);
48983
+
48984
+ const parseHint = object({
48985
+ replace: optional(boolean),
48986
+ content: string,
48987
+ widgets: parseWidgetsMap,
48988
+ metadata: optional(array(string)),
48989
+ images: record(string, object({
48990
+ width: number,
48991
+ height: number
48992
+ }))
48993
+ });
48994
+
48995
+ const parsePerseusRenderer = object({
48996
+ content: string,
48997
+ widgets: parseWidgetsMap,
48998
+ metadata: optional(array(string)),
48999
+ images: record(string, object({
49000
+ width: number,
49001
+ height: number
49002
+ }))
49003
+ });
49004
+
49005
+ object({
49006
+ question: parsePerseusRenderer,
49007
+ hints: array(parseHint),
49008
+ answerArea: record(enumeration(...ItemExtras), boolean),
49009
+ itemDataVersion: object({
49010
+ major: number,
49011
+ minor: number
49012
+ }),
49013
+ // Deprecated field
49014
+ answer: any
49015
+ });
49016
+
47742
49017
  /**
47743
49018
  * Helper to parse PerseusItem JSON
47744
49019
  * Why not just use JSON.parse? We want: