@khanacademy/perseus 51.0.1 → 52.0.1

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 (130) hide show
  1. package/dist/es/index.js +681 -2195
  2. package/dist/es/index.js.map +1 -1
  3. package/dist/es/strings.js +123 -15
  4. package/dist/es/strings.js.map +1 -1
  5. package/dist/index.d.ts +36 -6
  6. package/dist/index.js +736 -2203
  7. package/dist/index.js.map +1 -1
  8. package/dist/item-version.d.ts +1 -1
  9. package/dist/renderer-util.d.ts +7 -2
  10. package/dist/strings.d.ts +101 -1
  11. package/dist/strings.js +179 -26
  12. package/dist/strings.js.map +1 -1
  13. package/dist/types.d.ts +13 -12
  14. package/dist/widget-ai-utils/mock-widget/prompt-utils.d.ts +1 -1
  15. package/dist/widget-type-utils.d.ts +1 -22
  16. package/dist/widgets/categorizer/categorizer.d.ts +5 -5
  17. package/dist/widgets/cs-program/cs-program.d.ts +3 -2
  18. package/dist/widgets/dropdown/dropdown.d.ts +2 -1
  19. package/dist/widgets/expression/expression.d.ts +8 -10
  20. package/dist/widgets/grapher/grapher.d.ts +2 -1
  21. package/dist/widgets/group/group.d.ts +4 -2
  22. package/dist/widgets/group/score-group.d.ts +2 -2
  23. package/dist/widgets/group/validate-group.d.ts +4 -0
  24. package/dist/widgets/iframe/iframe.d.ts +3 -2
  25. package/dist/widgets/input-number/input-number.d.ts +2 -1
  26. package/dist/widgets/interactive-graph.d.ts +2 -1
  27. package/dist/widgets/interactive-graphs/graphs/linear.d.ts +2 -0
  28. package/dist/widgets/interactive-graphs/graphs/quadratic.d.ts +12 -1
  29. package/dist/widgets/interactive-graphs/graphs/segment.d.ts +2 -0
  30. package/dist/widgets/interactive-graphs/graphs/utils.d.ts +6 -0
  31. package/dist/widgets/label-image/label-image.d.ts +5 -4
  32. package/dist/widgets/matcher/matcher.d.ts +2 -1
  33. package/dist/widgets/matrix/matrix.d.ts +4 -2
  34. package/dist/widgets/measurer/measurer.d.ts +3 -8
  35. package/dist/widgets/mock-widgets/mock-widget-types.d.ts +17 -0
  36. package/dist/widgets/mock-widgets/mock-widget.d.ts +5 -4
  37. package/dist/widgets/mock-widgets/score-mock-widget.d.ts +2 -1
  38. package/dist/widgets/mock-widgets/validate-mock-widget.d.ts +4 -0
  39. package/dist/widgets/number-line/number-line.d.ts +3 -1
  40. package/dist/widgets/numeric-input/numeric-input.d.ts +2 -1
  41. package/dist/widgets/orderer/orderer.d.ts +6 -2
  42. package/dist/widgets/passage-ref/passage-ref.d.ts +2 -5
  43. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +0 -4
  44. package/dist/widgets/plotter/plotter.d.ts +4 -2
  45. package/dist/widgets/radio/radio-component.d.ts +1 -1
  46. package/dist/widgets/radio/radio.d.ts +5 -9
  47. package/dist/widgets/sorter/sorter.d.ts +3 -1
  48. package/dist/widgets/table/table.d.ts +3 -1
  49. package/dist/widgets.d.ts +3 -2
  50. package/package.json +14 -14
  51. package/dist/util/is-real-json-parse.d.ts +0 -1
  52. package/dist/util/parse-perseus-json/error-tracking-parse-context.d.ts +0 -9
  53. package/dist/util/parse-perseus-json/exhaustive-test-tool/index.d.ts +0 -2
  54. package/dist/util/parse-perseus-json/general-purpose-parsers/any.d.ts +0 -2
  55. package/dist/util/parse-perseus-json/general-purpose-parsers/array.d.ts +0 -2
  56. package/dist/util/parse-perseus-json/general-purpose-parsers/array.typetest.d.ts +0 -1
  57. package/dist/util/parse-perseus-json/general-purpose-parsers/boolean.d.ts +0 -2
  58. package/dist/util/parse-perseus-json/general-purpose-parsers/constant.d.ts +0 -4
  59. package/dist/util/parse-perseus-json/general-purpose-parsers/convert.d.ts +0 -2
  60. package/dist/util/parse-perseus-json/general-purpose-parsers/defaulted.d.ts +0 -2
  61. package/dist/util/parse-perseus-json/general-purpose-parsers/discriminated-union.d.ts +0 -21
  62. package/dist/util/parse-perseus-json/general-purpose-parsers/discriminated-union.typetest.d.ts +0 -1
  63. package/dist/util/parse-perseus-json/general-purpose-parsers/enumeration.d.ts +0 -2
  64. package/dist/util/parse-perseus-json/general-purpose-parsers/enumeration.typetest.d.ts +0 -1
  65. package/dist/util/parse-perseus-json/general-purpose-parsers/index.d.ts +0 -17
  66. package/dist/util/parse-perseus-json/general-purpose-parsers/is-object.d.ts +0 -1
  67. package/dist/util/parse-perseus-json/general-purpose-parsers/nullable.d.ts +0 -2
  68. package/dist/util/parse-perseus-json/general-purpose-parsers/number.d.ts +0 -2
  69. package/dist/util/parse-perseus-json/general-purpose-parsers/object.d.ts +0 -6
  70. package/dist/util/parse-perseus-json/general-purpose-parsers/optional.d.ts +0 -2
  71. package/dist/util/parse-perseus-json/general-purpose-parsers/pair.d.ts +0 -2
  72. package/dist/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.d.ts +0 -7
  73. package/dist/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.typetest.d.ts +0 -1
  74. package/dist/util/parse-perseus-json/general-purpose-parsers/record.d.ts +0 -2
  75. package/dist/util/parse-perseus-json/general-purpose-parsers/string-to-number.d.ts +0 -2
  76. package/dist/util/parse-perseus-json/general-purpose-parsers/string.d.ts +0 -2
  77. package/dist/util/parse-perseus-json/general-purpose-parsers/test-helpers.d.ts +0 -8
  78. package/dist/util/parse-perseus-json/general-purpose-parsers/trio.d.ts +0 -2
  79. package/dist/util/parse-perseus-json/general-purpose-parsers/union.d.ts +0 -7
  80. package/dist/util/parse-perseus-json/general-purpose-parsers/union.typetest.d.ts +0 -1
  81. package/dist/util/parse-perseus-json/general-purpose-parsers/unknown.d.ts +0 -2
  82. package/dist/util/parse-perseus-json/index.d.ts +0 -45
  83. package/dist/util/parse-perseus-json/object-path.d.ts +0 -2
  84. package/dist/util/parse-perseus-json/parse-failure-detail.d.ts +0 -2
  85. package/dist/util/parse-perseus-json/parse.d.ts +0 -3
  86. package/dist/util/parse-perseus-json/parser-types.d.ts +0 -30
  87. package/dist/util/parse-perseus-json/perseus-parsers/categorizer-widget.d.ts +0 -3
  88. package/dist/util/parse-perseus-json/perseus-parsers/cs-program-widget.d.ts +0 -3
  89. package/dist/util/parse-perseus-json/perseus-parsers/definition-widget.d.ts +0 -3
  90. package/dist/util/parse-perseus-json/perseus-parsers/dropdown-widget.d.ts +0 -3
  91. package/dist/util/parse-perseus-json/perseus-parsers/explanation-widget.d.ts +0 -3
  92. package/dist/util/parse-perseus-json/perseus-parsers/expression-widget.d.ts +0 -3
  93. package/dist/util/parse-perseus-json/perseus-parsers/graded-group-set-widget.d.ts +0 -3
  94. package/dist/util/parse-perseus-json/perseus-parsers/graded-group-widget.d.ts +0 -16
  95. package/dist/util/parse-perseus-json/perseus-parsers/grapher-widget.d.ts +0 -3
  96. package/dist/util/parse-perseus-json/perseus-parsers/group-widget.d.ts +0 -3
  97. package/dist/util/parse-perseus-json/perseus-parsers/hint.d.ts +0 -3
  98. package/dist/util/parse-perseus-json/perseus-parsers/iframe-widget.d.ts +0 -3
  99. package/dist/util/parse-perseus-json/perseus-parsers/image-widget.d.ts +0 -3
  100. package/dist/util/parse-perseus-json/perseus-parsers/images-map.d.ts +0 -5
  101. package/dist/util/parse-perseus-json/perseus-parsers/input-number-widget.d.ts +0 -3
  102. package/dist/util/parse-perseus-json/perseus-parsers/interaction-widget.d.ts +0 -3
  103. package/dist/util/parse-perseus-json/perseus-parsers/interactive-graph-widget.d.ts +0 -3
  104. package/dist/util/parse-perseus-json/perseus-parsers/label-image-widget.d.ts +0 -3
  105. package/dist/util/parse-perseus-json/perseus-parsers/matcher-widget.d.ts +0 -3
  106. package/dist/util/parse-perseus-json/perseus-parsers/matrix-widget.d.ts +0 -3
  107. package/dist/util/parse-perseus-json/perseus-parsers/measurer-widget.d.ts +0 -3
  108. package/dist/util/parse-perseus-json/perseus-parsers/molecule-renderer-widget.d.ts +0 -3
  109. package/dist/util/parse-perseus-json/perseus-parsers/number-line-widget.d.ts +0 -3
  110. package/dist/util/parse-perseus-json/perseus-parsers/numeric-input-widget.d.ts +0 -3
  111. package/dist/util/parse-perseus-json/perseus-parsers/orderer-widget.d.ts +0 -3
  112. package/dist/util/parse-perseus-json/perseus-parsers/passage-ref-widget.d.ts +0 -3
  113. package/dist/util/parse-perseus-json/perseus-parsers/passage-widget.d.ts +0 -3
  114. package/dist/util/parse-perseus-json/perseus-parsers/perseus-article.d.ts +0 -3
  115. package/dist/util/parse-perseus-json/perseus-parsers/perseus-image-background.d.ts +0 -3
  116. package/dist/util/parse-perseus-json/perseus-parsers/perseus-item.d.ts +0 -3
  117. package/dist/util/parse-perseus-json/perseus-parsers/perseus-renderer.d.ts +0 -3
  118. package/dist/util/parse-perseus-json/perseus-parsers/phet-simulation-widget.d.ts +0 -3
  119. package/dist/util/parse-perseus-json/perseus-parsers/plotter-widget.d.ts +0 -3
  120. package/dist/util/parse-perseus-json/perseus-parsers/python-program-widget.d.ts +0 -3
  121. package/dist/util/parse-perseus-json/perseus-parsers/radio-widget.d.ts +0 -3
  122. package/dist/util/parse-perseus-json/perseus-parsers/sorter-widget.d.ts +0 -3
  123. package/dist/util/parse-perseus-json/perseus-parsers/table-widget.d.ts +0 -3
  124. package/dist/util/parse-perseus-json/perseus-parsers/versioned-widget-options.d.ts +0 -36
  125. package/dist/util/parse-perseus-json/perseus-parsers/video-widget.d.ts +0 -3
  126. package/dist/util/parse-perseus-json/perseus-parsers/widget.d.ts +0 -7
  127. package/dist/util/parse-perseus-json/perseus-parsers/widgets-map.d.ts +0 -3
  128. package/dist/util/parse-perseus-json/result.d.ts +0 -16
  129. package/dist/util/snowman-utils.d.ts +0 -17
  130. package/dist/widgets/categorizer/categorizer.util.d.ts +0 -17
package/dist/index.js CHANGED
@@ -109,191 +109,6 @@ function _extends() {
109
109
  * The translated strings that are used to render Perseus.
110
110
  */
111
111
 
112
- /**
113
- * Untranslated strings used in Perseus. To be used by an external
114
- * translator to produce translated strings, passed in as `PerseusStrings`.
115
- * !! Note: Ensure that all escape sequences are double-escaped. (e.g. `\\text` -> `\\\\text`)
116
- */
117
- const strings = {
118
- closeKeypad: "close math keypad",
119
- openKeypad: "open math keypad",
120
- mathInputBox: "Math input box",
121
- removeHighlight: "Remove highlight",
122
- addHighlight: "Add highlight",
123
- hintPos: "Hint #%(pos)s",
124
- errorRendering: "Error rendering: %(error)s",
125
- APPROXIMATED_PI_ERROR: "Your answer is close, but you may " + "have approximated pi. Enter your " + "answer as a multiple of pi, like " + "12 pi or " + "2/3 pi",
126
- EXTRA_SYMBOLS_ERROR: "We could not understand your " + "answer. Please check your answer for extra " + "text or symbols.",
127
- NEEDS_TO_BE_SIMPLFIED_ERROR: "Your answer is almost correct, " + "but it needs to be simplified.",
128
- MISSING_PERCENT_ERROR: "Your answer is almost correct, " + "but it is missing a " + "<code>\\\\%</code> at the end.",
129
- MULTIPLICATION_SIGN_ERROR: "I'm a computer. I only understand " + "multiplication if you use an asterisk " + "(*) as the multiplication sign.",
130
- WRONG_CASE_ERROR: "Your answer includes use of a variable with the wrong case.",
131
- WRONG_LETTER_ERROR: "Your answer includes a wrong variable letter.",
132
- invalidSelection: "Make sure you select something for every row.",
133
- ERROR_TITLE: "Oops!",
134
- ERROR_MESSAGE: "Sorry, I don't understand that!",
135
- hints: "Hints",
136
- getAnotherHint: "Get another hint",
137
- deprecatedStandin: "Sorry, this part of the question is no longer available. 😅 Don't worry, you won't be graded on this part. Keep going!",
138
- keepTrying: "Keep trying",
139
- tryAgain: "Try again",
140
- check: "Check",
141
- correctExcited: "Correct!",
142
- nextQuestion: "Next question",
143
- skipToTitle: "Skip to %(title)s",
144
- current: "Current",
145
- correct: "Correct",
146
- correctSelected: "Correct (selected)",
147
- correctCrossedOut: "Correct (but you crossed it out)",
148
- incorrect: "Incorrect",
149
- incorrectSelected: "Incorrect (selected)",
150
- hideExplanation: "Hide explanation",
151
- explain: "Explain",
152
- INVALID_MESSAGE_PREFIX: "We couldn't grade your answer.",
153
- DEFAULT_INVALID_MESSAGE_1: "It looks like you left something blank or ",
154
- DEFAULT_INVALID_MESSAGE_2: "entered in an invalid answer.",
155
- integerExample: "an integer, like $6$",
156
- properExample: "a *proper* fraction, like $1/2$ or $6/10$",
157
- simplifiedProperExample: "a *simplified proper* fraction, like $3/5$",
158
- improperExample: "an *improper* fraction, like $10/7$ or $14/8$",
159
- simplifiedImproperExample: "a *simplified improper* fraction, like $7/4$",
160
- mixedExample: "a mixed number, like $1\\\\ 3/4$",
161
- decimalExample: "an *exact* decimal, like $0.75$",
162
- percentExample: "a percent, like $12.34\\\\%$",
163
- piExample: "a multiple of pi, like $12$ pi or $2/3$ pi",
164
- yourAnswer: "**Your answer should be** ",
165
- yourAnswerLabel: "Your answer:",
166
- addPoints: "Click to add points",
167
- addVertices: "Click to add vertices",
168
- tapMultiple: "Tap each dot on the image to select all answers that apply.",
169
- tapSingle: "Tap each dot on the image to select an answer.",
170
- clickMultiple: "Click each dot on the image to select all answers that apply.",
171
- clickSingle: "Click each dot on the image to select an answer.",
172
- choices: "Choices:",
173
- answers: {
174
- one: "%(num)s answer",
175
- other: "%(num)s answers"
176
- },
177
- hideAnswersToggleLabel: "Hide answer choices",
178
- moves: {
179
- one: "Moves: %(num)s",
180
- other: "Moves: %(num)s"
181
- },
182
- clickTiles: "Click on the tiles to change the lights.",
183
- turnOffLights: "You must turn on all of the lights to continue.",
184
- fillAllCells: "Make sure you fill in all cells in the matrix.",
185
- molecularDrawing: "A molecular structure drawing. SMILES notation: %(content)s",
186
- switchDirection: "Switch direction",
187
- circleOpen: "Make circle open",
188
- circleFilled: "Make circle filled",
189
- numDivisions: "Number of divisions:",
190
- divisions: "Please make sure the number of divisions is in the range %(divRangeString)s.",
191
- lineRange: "lines %(lineRange)s",
192
- lineNumber: "line %(lineNumber)s",
193
- symbolPassage: "The symbol %(questionSymbol)s indicates that question %(questionNumber)s references this portion of the passage.",
194
- symbolQuestion: " The symbol %(sentenceSymbol)s indicates that the following sentence is referenced in a question.",
195
- lineLabel: {
196
- context: "a label next to a reading passage to denote the line number",
197
- message: "Line"
198
- },
199
- beginningPassage: "Beginning of reading passage.",
200
- beginningFootnotes: "Beginning of reading passage footnotes.",
201
- endPassage: "End of reading passage.",
202
- questionMarker: "[Marker for question %(number)s]",
203
- circleMarker: "[Circle marker %(number)s]",
204
- sentenceMarker: "[Sentence %(number)s]",
205
- dragHandles: "Drag handles to make graph",
206
- tapAddPoints: "Tap to add points",
207
- false: "False",
208
- true: "True",
209
- no: "No",
210
- yes: "Yes",
211
- chooseCorrectNum: "Please choose the correct number of answers.",
212
- notNoneOfTheAbove: "'None of the above' may not be selected when other answers are selected.",
213
- noneOfTheAbove: "None of the above",
214
- chooseNumAnswers: "Choose %(numCorrect)s answers:",
215
- chooseAllAnswers: "Choose all answers that apply:",
216
- chooseOneAnswer: "Choose 1 answer:",
217
- choiceCheckedCorrect: "(Choice %(letter)s, Checked, Correct)",
218
- choiceCrossedOutCorrect: "(Choice %(letter)s, Crossed out, Correct)",
219
- choiceCorrect: "(Choice %(letter)s, Correct)",
220
- choiceCheckedIncorrect: "(Choice %(letter)s, Checked, Incorrect)",
221
- choiceCrossedOutIncorrect: "(Choice %(letter)s, Crossed out, Incorrect)",
222
- choiceIncorrect: "(Choice %(letter)s, Incorrect)",
223
- choiceChecked: "(Choice %(letter)s, Checked)",
224
- choiceCrossedOut: "(Choice %(letter)s, Crossed out)",
225
- choice: "(Choice %(letter)s)",
226
- crossOut: "Cross out",
227
- crossOutOption: "Cross out option",
228
- crossOutChoice: "Cross out Choice %(letter)s",
229
- bringBack: "Bring back",
230
- openMenuForChoice: "Open menu for Choice %(letter)s",
231
- letters: {
232
- context: "This is a list of single-character labels that will appear in front of multiple-choice options. For instance, a multiple-choice question with three options would display (A) first option (B) second option (C) third option. There must be spaces between each of the different characters. The characters will show up next to options in the order that they are listed here. Most multiple choice questions have 5 or fewer options.",
233
- message: "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z"
234
- },
235
- rightArrow: "Reaction arrow pointing to the right.",
236
- dontUnderstandUnits: "I couldn't understand those units.",
237
- checkSigFigs: "Check your significant figures.",
238
- answerNumericallyIncorrect: "That answer is numerically incorrect.",
239
- checkUnits: "Check your units.",
240
- dontUnderstand: "I don't understand that",
241
- loading: "Loading...",
242
- videoTranscript: "See video transcript",
243
- somethingWrong: "Something went wrong.",
244
- videoWrapper: "Khan Academy video wrapper",
245
- mathInputTitle: "mathematics keyboard",
246
- mathInputDescription: "Use keyboard/mouse to interact with math-based input fields",
247
- sin: "sin",
248
- cos: "cos",
249
- tan: "tan",
250
- simulationLoadFail: "Sorry, this simulation cannot load.",
251
- simulationLocaleWarning: "Sorry, this simulation isn't available in your language.",
252
- selectAnAnswer: "Select an answer",
253
- // The following strings are used for interactive graph SR descriptions.
254
- addPoint: "Add Point",
255
- removePoint: "Remove Point",
256
- graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
257
- closePolygon: "Close shape",
258
- openPolygon: "Re-open shape",
259
- srInteractiveElements: "Interactive elements: %(elements)s",
260
- srNoInteractiveElements: "No interactive elements",
261
- // TODO(LEMS-2660): The following strings are ones that will need
262
- // translation tickets after all interactive graph SR strings have
263
- // been finalized. Remove this comment after the tickets have been
264
- // created.
265
- srPointAtCoordinates: "Point %(num)s at %(x)s comma %(y)s",
266
- srCircleGraph: "A circle on a coordinate plane.",
267
- srCircleShape: "Circle. The center point is at %(centerX)s comma %(centerY)s.",
268
- srCircleRadiusPoint: "Radius point at %(radiusPointX)s comma %(radiusPointY)s.",
269
- srCircleRadius: "Circle radius is %(radius)s.",
270
- srCircleOuterPoints: "Points on the circle at %(point1X)s comma %(point1Y)s, %(point2X)s comma %(point2Y)s, %(point3X)s comma %(point3Y)s, %(point4X)s comma %(point4Y)s.",
271
- srLinearGraph: "A line on a coordinate plane.",
272
- srLinearGraphPoints: "The line has two points, point 1 at %(point1X)s comma %(point1Y)s and point 2 at %(point2X)s comma %(point2Y)s.",
273
- srLinearGraphSlopeIncreasing: "Its slope increases from left to right.",
274
- srLinearGraphSlopeDecreasing: "Its slope decreases from left to right.",
275
- srLinearGraphSlopeHorizontal: "Its slope is zero.",
276
- srLinearGraphSlopeVertical: "Its slope is undefined.",
277
- srLinearGraphXOnlyIntercept: "The line crosses the X-axis at %(xIntercept)s comma 0.",
278
- srLinearGraphYOnlyIntercept: "The line crosses the Y-axis at 0 comma %(yIntercept)s.",
279
- srLinearGraphBothIntercepts: "The line crosses the X-axis at %(xIntercept)s comma 0 and the Y-axis at 0 comma %(yIntercept)s.",
280
- srLinearGraphOriginIntercept: "The line crosses the x and y axes at the graph's origin.",
281
- srLinearGrabHandle: "Line from %(point1X)s comma %(point1Y)s to %(point2X)s comma %(point2Y)s.",
282
- srAngleSideAtCoordinates: "Point %(point)s, %(side)s at %(x)s comma %(y)s",
283
- srAngleVertexAtCoordinatesWithAngleMeasure: "Point 2, vertex at %(x)s comma %(y)s. Angle %(angleMeasure)s degrees",
284
- srAngleGraphAriaLabel: "An angle on a coordinate plane.",
285
- srAngleGraphAriaDescription: "The angle measure is %(angleMeasure)s degrees with a vertex at %(vertexX)s comma %(vertexY)s, a point on the starting side at %(startingSideX)s comma %(startingSideY)s and a point on the ending side at %(endingSideX)s comma %(endingSideY)s",
286
- srLinearSystemGraph: "Two lines on a coordinate plane.",
287
- srLinearSystemPoints: "Line %(lineNumber)s has two points, point 1 at %(point1X)s comma %(point1Y)s and point 2 at %(point2X)s comma %(point2Y)s.",
288
- srLinearSystemPoint: "Point %(pointSequence)s on line %(lineNumber)s at %(x)s comma %(y)s.",
289
- srRayGraph: "A ray on a coordinate plane.",
290
- srRayPoints: "The endpoint is at %(point1X)s comma %(point1Y)s and the ray goes through point %(point2X)s comma %(point2Y)s.",
291
- srRayGrabHandle: "Ray with endpoint %(point1X)s comma %(point1Y)s going through point %(point2X)s comma %(point2Y)s.",
292
- srRayEndpoint: "Endpoint at %(x)s comma %(y)s.",
293
- srRayTerminalPoint: "Through point at %(x)s comma %(y)s."
294
- // The above strings are used for interactive graph SR descriptions.
295
- };
296
-
297
112
  /**
298
113
  * Mock strings for the Perseus package, to be used for tests and Storybook.
299
114
  */
@@ -680,82 +495,213 @@ const mockStrings = {
680
495
  } = _ref40;
681
496
  return `The angle measure is ${angleMeasure} degrees with a vertex at ${vertexX} comma ${vertexY}, a point on the starting side at ${startingSideX} comma ${startingSideY} and a point on the ending side at ${endingSideX} comma ${endingSideY}.`;
682
497
  },
498
+ srSingleSegmentGraphAriaLabel: "A line segment on a coordinate plane.",
499
+ srMultipleSegmentGraphAriaLabel: _ref41 => {
500
+ let {
501
+ countOfSegments
502
+ } = _ref41;
503
+ return `${countOfSegments} segments on a coordinate plane.`;
504
+ },
505
+ srMultipleSegmentIndividualLabel: _ref42 => {
506
+ let {
507
+ point1X,
508
+ point1Y,
509
+ point2X,
510
+ point2Y,
511
+ indexOfSegment
512
+ } = _ref42;
513
+ return `Segment ${indexOfSegment}: Endpoint 1 at ${point1X} comma ${point1Y}. Endpoint 2 at ${point2X} comma ${point2Y}.`;
514
+ },
515
+ srSingleSegmentLabel: _ref43 => {
516
+ let {
517
+ point1X,
518
+ point1Y,
519
+ point2X,
520
+ point2Y
521
+ } = _ref43;
522
+ return `Endpoint 1 at ${point1X} comma ${point1Y}. Endpoint 2 at ${point2X} comma ${point2Y}.`;
523
+ },
524
+ srSegmentLength: _ref44 => {
525
+ let {
526
+ length
527
+ } = _ref44;
528
+ return `Segment length ${length} units.`;
529
+ },
530
+ srSingleSegmentGraphEndpointAriaLabel: _ref45 => {
531
+ let {
532
+ endpointNumber,
533
+ x,
534
+ y
535
+ } = _ref45;
536
+ return `Endpoint ${endpointNumber} at ${x} comma ${y}.`;
537
+ },
538
+ srMultipleSegmentGraphEndpointAriaLabel: _ref46 => {
539
+ let {
540
+ endpointNumber,
541
+ x,
542
+ y,
543
+ indexOfSegment
544
+ } = _ref46;
545
+ return `Endpoint ${endpointNumber} on segment ${indexOfSegment} at ${x} comma ${y}.`;
546
+ },
547
+ srSegmentGrabHandle: _ref47 => {
548
+ let {
549
+ point1X,
550
+ point1Y,
551
+ point2X,
552
+ point2Y
553
+ } = _ref47;
554
+ return `Segment from ${point1X} comma ${point1Y} to ${point2X} comma ${point2Y}.`;
555
+ },
683
556
  srLinearSystemGraph: "Two lines on a coordinate plane.",
684
- srLinearSystemPoints: _ref41 => {
557
+ srLinearSystemPoints: _ref48 => {
685
558
  let {
686
559
  lineNumber,
687
560
  point1X,
688
561
  point1Y,
689
562
  point2X,
690
563
  point2Y
691
- } = _ref41;
564
+ } = _ref48;
692
565
  return `Line ${lineNumber} has two points, point 1 at ${point1X} comma ${point1Y} and point 2 at ${point2X} comma ${point2Y}.`;
693
566
  },
694
- srLinearSystemPoint: _ref42 => {
567
+ srLinearSystemPoint: _ref49 => {
695
568
  let {
696
569
  lineNumber,
697
570
  pointSequence,
698
571
  x,
699
572
  y
700
- } = _ref42;
573
+ } = _ref49;
701
574
  return `Point ${pointSequence} on line ${lineNumber} at ${x} comma ${y}.`;
702
575
  },
703
576
  srRayGraph: "A ray on a coordinate plane.",
704
- srRayPoints: _ref43 => {
577
+ srRayPoints: _ref50 => {
705
578
  let {
706
579
  point1X,
707
580
  point1Y,
708
581
  point2X,
709
582
  point2Y
710
- } = _ref43;
583
+ } = _ref50;
711
584
  return `The endpoint is at ${point1X} comma ${point1Y} and the ray goes through point ${point2X} comma ${point2Y}.`;
712
585
  },
713
- srRayGrabHandle: _ref44 => {
586
+ srRayGrabHandle: _ref51 => {
714
587
  let {
715
588
  point1X,
716
589
  point1Y,
717
590
  point2X,
718
591
  point2Y
719
- } = _ref44;
592
+ } = _ref51;
720
593
  return `Ray with endpoint ${point1X} comma ${point1Y} going through point ${point2X} comma ${point2Y}.`;
721
594
  },
722
- srRayEndpoint: _ref45 => {
595
+ srRayEndpoint: _ref52 => {
723
596
  let {
724
597
  x,
725
598
  y
726
- } = _ref45;
599
+ } = _ref52;
727
600
  return `Endpoint at ${x} comma ${y}.`;
728
601
  },
729
- srRayTerminalPoint: _ref46 => {
602
+ srRayTerminalPoint: _ref53 => {
730
603
  let {
731
604
  x,
732
605
  y
733
- } = _ref46;
606
+ } = _ref53;
734
607
  return `Through point at ${x} comma ${y}.`;
608
+ },
609
+ srQuadraticGraph: "A parabola on a 4-quadrant coordinate plane.",
610
+ srQuadraticFaceUp: "The parabola opens upward.",
611
+ srQuadraticFaceDown: "The parabola opens downward.",
612
+ srQuadraticGraphVertexOrigin: "Vertex is at the origin.",
613
+ srQuadraticGraphVertexXAxis: "Vertex is on the X-axis.",
614
+ srQuadraticGraphVertexYAxis: "Vertex is on the Y-axis.",
615
+ srQuadraticGraphVertexQuadrant: _ref54 => {
616
+ let {
617
+ quadrant
618
+ } = _ref54;
619
+ return `Vertex is in quadrant ${quadrant}.`;
620
+ },
621
+ srQuadraticTwoXIntercepts: _ref55 => {
622
+ let {
623
+ intercept1,
624
+ intercept2
625
+ } = _ref55;
626
+ return `The X-intercepts are at ${intercept1} comma 0 and ${intercept2} comma 0.`;
627
+ },
628
+ srQuadraticOneXIntercept: _ref56 => {
629
+ let {
630
+ intercept
631
+ } = _ref56;
632
+ return `The X-intercept is at ${intercept} comma 0.`;
633
+ },
634
+ srQuadraticYIntercept: _ref57 => {
635
+ let {
636
+ intercept
637
+ } = _ref57;
638
+ return `The Y-intercept is at 0 comma ${intercept}.`;
639
+ },
640
+ srQuadraticPointOrigin: _ref58 => {
641
+ let {
642
+ pointNumber
643
+ } = _ref58;
644
+ return `Point ${pointNumber} on parabola at the origin.`;
645
+ },
646
+ srQuadraticPointAxis: _ref59 => {
647
+ let {
648
+ pointNumber,
649
+ x,
650
+ y
651
+ } = _ref59;
652
+ return `Point ${pointNumber} on parabola at ${x} comma ${y}.`;
653
+ },
654
+ srQuadraticPointQuadrant: _ref60 => {
655
+ let {
656
+ pointNumber,
657
+ x,
658
+ y,
659
+ quadrant
660
+ } = _ref60;
661
+ return `Point ${pointNumber} on parabola in quadrant ${quadrant} at ${x} comma ${y}.`;
662
+ },
663
+ srQuadraticInteractiveElements: _ref61 => {
664
+ let {
665
+ point1X,
666
+ point1Y,
667
+ point2X,
668
+ point2Y,
669
+ point3X,
670
+ point3Y
671
+ } = _ref61;
672
+ return `Parabola with points at ${point1X} comma ${point1Y}, ${point2X} comma ${point2Y}, and ${point3X} comma ${point3Y}.`;
735
673
  }
736
674
  // The above strings are used for interactive graph SR descriptions.
737
675
  };
738
676
 
739
677
  // This type helps us make sure all error codes are mapped to strings
740
678
 
679
+ /**
680
+ * Map an error string to a PerseusStrings key
681
+ * that we can use to get the translated error message
682
+ */
741
683
  const errorToString = {
742
- MISSING_PERCENT_ERROR: strings.MISSING_PERCENT_ERROR,
743
- NEEDS_TO_BE_SIMPLIFIED_ERROR: strings.NEEDS_TO_BE_SIMPLFIED_ERROR,
744
- APPROXIMATED_PI_ERROR: strings.APPROXIMATED_PI_ERROR,
745
- EXTRA_SYMBOLS_ERROR: strings.EXTRA_SYMBOLS_ERROR,
746
- WRONG_CASE_ERROR: strings.WRONG_CASE_ERROR,
747
- WRONG_LETTER_ERROR: strings.WRONG_LETTER_ERROR,
748
- MULTIPLICATION_SIGN_ERROR: strings.MULTIPLICATION_SIGN_ERROR,
749
- INVALID_SELECTION_ERROR: strings.invalidSelection,
750
- CHOOSE_CORRECT_NUM_ERROR: strings.chooseCorrectNum,
751
- NOT_NONE_ABOVE_ERROR: strings.notNoneOfTheAbove,
752
- FILL_ALL_CELLS_ERROR: strings.fillAllCells
753
- };
754
- function mapErrorToString(err) {
684
+ MISSING_PERCENT_ERROR: "MISSING_PERCENT_ERROR",
685
+ NEEDS_TO_BE_SIMPLIFIED_ERROR: "NEEDS_TO_BE_SIMPLFIED_ERROR",
686
+ APPROXIMATED_PI_ERROR: "APPROXIMATED_PI_ERROR",
687
+ EXTRA_SYMBOLS_ERROR: "EXTRA_SYMBOLS_ERROR",
688
+ WRONG_CASE_ERROR: "WRONG_CASE_ERROR",
689
+ WRONG_LETTER_ERROR: "WRONG_LETTER_ERROR",
690
+ MULTIPLICATION_SIGN_ERROR: "MULTIPLICATION_SIGN_ERROR",
691
+ INVALID_SELECTION_ERROR: "invalidSelection",
692
+ CHOOSE_CORRECT_NUM_ERROR: "chooseCorrectNum",
693
+ NOT_NONE_ABOVE_ERROR: "notNoneOfTheAbove",
694
+ FILL_ALL_CELLS_ERROR: "fillAllCells"
695
+ };
696
+ function mapErrorToString(
697
+ // the string representing an error code
698
+ err,
699
+ // the translated Perseus strings
700
+ translatedStrings) {
755
701
  if (!err) {
756
702
  return err;
757
703
  }
758
- return errorToString[err] || err;
704
+ return translatedStrings[errorToString[err]] || err;
759
705
  }
760
706
 
761
707
  /**
@@ -2006,23 +1952,6 @@ const keypadConfigurationForProps = widgetOptions => {
2006
1952
  times: widgetOptions.times
2007
1953
  };
2008
1954
  };
2009
- const propUpgrades$2 = {
2010
- /* c8 ignore next */
2011
- "1": v0props => ({
2012
- times: v0props.times,
2013
- buttonSets: v0props.buttonSets,
2014
- functions: v0props.functions,
2015
- buttonsVisible: v0props.buttonsVisible,
2016
- visibleLabel: v0props.visibleLabel,
2017
- ariaLabel: v0props.ariaLabel,
2018
- answerForms: [{
2019
- considered: "correct",
2020
- form: v0props.form,
2021
- simplify: v0props.simplify,
2022
- value: v0props.value
2023
- }]
2024
- })
2025
- };
2026
1955
  const ExpressionWithDependencies = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
2027
1956
  const deps = useDependencies();
2028
1957
  return /*#__PURE__*/React__namespace.createElement(Expression, _extends({
@@ -2062,17 +1991,18 @@ var Expression$1 = {
2062
1991
  ariaLabel
2063
1992
  };
2064
1993
  },
2065
- version: {
2066
- major: 1,
2067
- minor: 0
2068
- },
2069
- propUpgrades: propUpgrades$2,
1994
+ version: perseusCore.expressionLogic.version,
1995
+ propUpgrades: perseusCore.expressionLogic.widgetOptionsUpgrades,
2070
1996
  // For use by the editor
2071
1997
  isLintable: true,
2072
1998
  // TODO(LEMS-2656): remove TS suppression
2073
1999
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'.
2074
2000
  scorer: perseusScore.scoreExpression,
2075
2001
  // TODO(LEMS-2656): remove TS suppression
2002
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'.
2003
+ validator: perseusScore.validateExpression,
2004
+ getPublicWidgetOptions: perseusCore.getExpressionPublicWidgetOptions,
2005
+ // TODO(LEMS-2656): remove TS suppression
2076
2006
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusExpressionRubric'.
2077
2007
  getOneCorrectAnswerFromRubric(rubric) {
2078
2008
  const correctAnswers = (rubric.answerForms || []).filter(answerForm => answerForm.considered === "correct");
@@ -17580,7 +17510,7 @@ const inlineNodeTypes = {
17580
17510
 
17581
17511
  // @ts-expect-error - TS2345 - Argument of type '{ readonly columns: { readonly react: (node: any, output: any, state: any) => Element; readonly order: -2; readonly match: any; readonly parse: (capture: any, parse: any, state: any) => any; }; ... 35 more ...; readonly text: TextInOutRule; }' is not assignable to parameter of type 'ParserRules'.
17582
17512
  const builtParser$1 = SimpleMarkdown__default["default"].parserFor(rules$1);
17583
- const parse$4 = (source, state) => {
17513
+ const parse$3 = (source, state) => {
17584
17514
  const paragraphedSource = source + "\n\n";
17585
17515
  return builtParser$1(paragraphedSource, {
17586
17516
  ...state,
@@ -17665,14 +17595,14 @@ const getContent = ast => {
17665
17595
  */
17666
17596
  const characterCount = source => {
17667
17597
  // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
17668
- const ast = parse$4(source);
17598
+ const ast = parse$3(source);
17669
17599
  const content = getContent(ast).join("");
17670
17600
  return content.length;
17671
17601
  };
17672
17602
  var PerseusMarkdown = {
17673
17603
  characterCount: characterCount,
17674
17604
  traverseContent: traverseContent,
17675
- parse: parse$4,
17605
+ parse: parse$3,
17676
17606
  parseInline: inlineParser,
17677
17607
  reactFor: SimpleMarkdown__default["default"].reactFor,
17678
17608
  ruleOutput: SimpleMarkdown__default["default"].ruleOutput(rules$1, "react"),
@@ -17813,136 +17743,6 @@ guess, state) {
17813
17743
  });
17814
17744
  }
17815
17745
 
17816
- // These are all the current widgets that require user-input and are supported for automatic scoring
17817
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
17818
-
17819
- /**
17820
- * Regex for widget placeholders in a string.
17821
- *
17822
- * First capture group is the widget ID (ex. 'radio 1')
17823
- * Second capture group is the widget type (ex. "radio)
17824
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
17825
- */
17826
- function getWidgetRegex() {
17827
- return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
17828
- }
17829
-
17830
- /**
17831
- * Add a widget placeholder using the widget ID.
17832
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
17833
- *
17834
- * @param {string} id
17835
- * @returns {string}
17836
- */
17837
- function addWidget(id) {
17838
- return `[[☃ ${id}]]`;
17839
- }
17840
-
17841
- /**
17842
- * Get a widget type by a widget's ID
17843
- * (please don't derive type from ID)
17844
- *
17845
- * @param {string} widgetId the ID of the widget
17846
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
17847
- * @returns {string} the widget type (ie "radio")
17848
- */
17849
- function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
17850
- const widget = widgetMap[WidgetId];
17851
- return widget?.type ?? null;
17852
- }
17853
-
17854
- /**
17855
- * Does the content have a specific type of widget?
17856
- *
17857
- * @param {string} type the type of the widget in question (ie "radio")
17858
- * @param {string} content the string to search through
17859
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
17860
- * @returns {boolean} if the content includes the widget type
17861
- */
17862
- function contentHasWidgetType(type, content, widgetMap) {
17863
- return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
17864
- }
17865
-
17866
- /**
17867
- * Extract all widget IDs, which includes the widget type and instance number.
17868
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
17869
- *
17870
- * Content should contain Perseus widget placeholders,
17871
- * which look like: '[[☃ radio 1]]'.
17872
- *
17873
- * @param {string} content
17874
- * @returns {ReadonlyArray<string>} widgetIds
17875
- */
17876
- function getWidgetIdsFromContent(content) {
17877
- const widgets = [];
17878
- const localWidgetRegex = getWidgetRegex();
17879
- let match = localWidgetRegex.exec(content);
17880
- while (match !== null) {
17881
- widgets.push(match[1]);
17882
- match = localWidgetRegex.exec(content);
17883
- }
17884
- return widgets;
17885
- }
17886
-
17887
- /**
17888
- * Get a list of widget IDs from content,
17889
- * but only for specific widget types
17890
- *
17891
- * @param {string} type the type of widget (ie "radio")
17892
- * @param {string} content the string to parse
17893
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
17894
- * @returns {ReadonlyArray<string>} the widget type (ie "radio")
17895
- */
17896
- function getWidgetIdsFromContentByType(type, content, widgetMap) {
17897
- const rv = [];
17898
- const widgetIdsInContent = getWidgetIdsFromContent(content);
17899
- widgetIdsInContent.forEach(widgetId => {
17900
- const widget = widgetMap[widgetId];
17901
- if (widget?.type === type) {
17902
- rv.push(widgetId);
17903
- }
17904
- });
17905
- return rv;
17906
- }
17907
-
17908
- /**
17909
- * Pull the widget map out of ItemData
17910
- *
17911
- * @param {PerseusItem} PerseusItem containing a widgetMap
17912
- * @returns {WidgetMap} the widget map in the PerseusItem
17913
- */
17914
- function getWidgetsMapFromItemData(itemData) {
17915
- return itemData.question.widgets;
17916
- }
17917
-
17918
- /**
17919
- * Get the widget information from a WidgetMap
17920
- *
17921
- * @param {string} widgetId the ID of the widget
17922
- * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
17923
- */
17924
- function getWidgetFromWidgetMap(widgetId, widgetMap) {
17925
- return widgetMap[widgetId] ?? null;
17926
- }
17927
-
17928
- /**
17929
- * Get select widgets from a widget map.
17930
- *
17931
- * @param {ReadonlyArray<string>} widgetIds to extract
17932
- * @param {PerseusWidgetsMap} widgetMap to extract from
17933
- * @return {PerseusWidgetsMap} a new widget map with requested widgets
17934
- */
17935
- function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
17936
- const widgets = {};
17937
- widgetIds.forEach(widgetId => {
17938
- const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
17939
- if (widget) {
17940
- widgets[widgetId] = widget;
17941
- }
17942
- });
17943
- return widgets;
17944
- }
17945
-
17946
17746
  const DEFAULT_ALIGNMENT$1 = "block";
17947
17747
  // NOTE(kevinb): "default" is not one in `validAlignments`.
17948
17748
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -18044,6 +17844,9 @@ const getWidget = name => {
18044
17844
  const getWidgetExport = name => {
18045
17845
  return widgets[name] ?? null;
18046
17846
  };
17847
+ const getWidgetValidator = name => {
17848
+ return widgets[name]?.validator ?? null;
17849
+ };
18047
17850
  const getWidgetScorer = name => {
18048
17851
  return widgets[name]?.scorer ?? null;
18049
17852
  };
@@ -18356,6 +18159,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
18356
18159
  replaceDeprecatedEditors: replaceDeprecatedEditors,
18357
18160
  getWidget: getWidget,
18358
18161
  getWidgetExport: getWidgetExport,
18162
+ getWidgetValidator: getWidgetValidator,
18359
18163
  getWidgetScorer: getWidgetScorer,
18360
18164
  getPublicWidgetOptionsFunction: getPublicWidgetOptionsFunction,
18361
18165
  getEditor: getEditor,
@@ -18378,7 +18182,6 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
18378
18182
  });
18379
18183
 
18380
18184
  function getUpgradedWidgetOptions(oldWidgetOptions) {
18381
- // @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
18382
18185
  return perseusCore.mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
18383
18186
  if (!widgetInfo.type || !widgetInfo.alignment) {
18384
18187
  const newValues = {};
@@ -18396,22 +18199,30 @@ function getUpgradedWidgetOptions(oldWidgetOptions) {
18396
18199
  ...newValues
18397
18200
  };
18398
18201
  }
18202
+ // TODO(LEMS-2656): remove TS suppression
18399
18203
  return upgradeWidgetInfoToLatestVersion(widgetInfo);
18400
18204
  });
18401
18205
  }
18206
+
18207
+ /**
18208
+ * Checks the given user input to see if any answerable widgets have not been
18209
+ * "filled in" (ie. if they're empty). Another way to think about this
18210
+ * function is that its a check to see if we can score the provided input.
18211
+ */
18402
18212
  function emptyWidgetsFunctional(widgets,
18403
18213
  // This is a port of old code, I'm not sure why
18404
18214
  // we need widgetIds vs the keys of the widgets object
18405
18215
  widgetIds, userInputMap, strings, locale) {
18406
- const upgradedWidgets = getUpgradedWidgetOptions(widgets);
18407
18216
  return widgetIds.filter(id => {
18408
- const widget = upgradedWidgets[id];
18409
- if (!widget || widget.static) {
18217
+ const widget = widgets[id];
18218
+ if (!widget || widget.static === true) {
18410
18219
  // Static widgets shouldn't count as empty
18411
18220
  return false;
18412
18221
  }
18413
- const scorer = getWidgetScorer(widget.type);
18414
- const score = scorer?.(userInputMap[id], widget.options, strings, locale);
18222
+ const validator = getWidgetValidator(widget.type);
18223
+ const userInput = userInputMap[id];
18224
+ const validationData = widget.options;
18225
+ const score = validator?.(userInput, validationData, strings, locale);
18415
18226
  if (score) {
18416
18227
  return scoreIsEmpty(score);
18417
18228
  }
@@ -18427,7 +18238,7 @@ strings, locale) {
18427
18238
  // There seems to be a chance that PerseusRenderer.widgets might include
18428
18239
  // widget data for widgets that are not in PerseusRenderer.content,
18429
18240
  // so this checks that the widgets are being used before scoring them
18430
- const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
18241
+ const usedWidgetIds = perseusCore.getWidgetIdsFromContent(perseusRenderData.content);
18431
18242
  const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
18432
18243
  return flattenScores(scores);
18433
18244
  }
@@ -18450,8 +18261,12 @@ widgetIds, userInputMap, strings, locale) {
18450
18261
  return;
18451
18262
  }
18452
18263
  const userInput = userInputMap[id];
18264
+ const validator = getWidgetValidator(widget.type);
18453
18265
  const scorer = getWidgetScorer(widget.type);
18454
- const score = scorer?.(userInput, widget.options, strings, locale);
18266
+
18267
+ // We do validation (empty checks) first and then scoring. If
18268
+ // validation fails, it's result is itself a PerseusScore.
18269
+ const score = validator?.(userInput, widget.options, strings, locale) ?? scorer?.(userInput, widget.options, strings, locale);
18455
18270
  if (score != null) {
18456
18271
  widgetScores[id] = score;
18457
18272
  }
@@ -19060,7 +18875,7 @@ class Renderer extends React__namespace.Component {
19060
18875
  const apiOptions = this.getApiOptions();
19061
18876
  const widgetProps = this.state.widgetProps[widgetId] || {};
19062
18877
 
19063
- // The widget needs access to its "rubric" at all times when in review
18878
+ // The widget needs access to its "scoring data" at all times when in review
19064
18879
  // mode (which is really just part of its widget info).
19065
18880
  const widgetInfo = this.state.widgetInfo[widgetId];
19066
18881
  const reviewModeRubric = this.props.reviewMode && widgetInfo ? widgetInfo.options : null;
@@ -23020,7 +22835,7 @@ const CIRCLE_LABEL_STYLE = {
23020
22835
  // TODO(LEMS-2656): remove TS suppression
23021
22836
  // @ts-expect-error: State is not assignable to ParseState
23022
22837
  const builtParser = SimpleMarkdown__default["default"].parserFor(rules);
23023
- const parse$3 = (source, state) => {
22838
+ const parse$2 = (source, state) => {
23024
22839
  // @ts-expect-error - TS2322 - Type 'ParseState | {}' is not assignable to type 'ParseState | null | undefined'.
23025
22840
  state = state || {};
23026
22841
  const paragraphedSource = source + "\n\n";
@@ -23030,7 +22845,7 @@ const parse$3 = (source, state) => {
23030
22845
  // @ts-expect-error - TS2322 - Type 'ReactOutput' is not assignable to type '(arg1: SingleASTNode[]) => ReactElement<any, string | JSXElementConstructor<any>>'.
23031
22846
  const output = SimpleMarkdown__default["default"].reactFor(SimpleMarkdown__default["default"].ruleOutput(rules, "react"));
23032
22847
  var PassageMarkdown = {
23033
- parse: parse$3,
22848
+ parse: parse$2,
23034
22849
  output,
23035
22850
  START_REF_PREFIX,
23036
22851
  END_REF_PREFIX,
@@ -23585,10 +23400,7 @@ var PassageRef$1 = {
23585
23400
  referenceNumber: widgetOptions.referenceNumber,
23586
23401
  summaryText: widgetOptions.summaryText
23587
23402
  }),
23588
- version: {
23589
- major: 0,
23590
- minor: 1
23591
- },
23403
+ version: perseusCore.passageRefLogic.version,
23592
23404
  // TODO: things that aren't interactive shouldn't need scoring functions
23593
23405
  scorer: () => scoreNoop()
23594
23406
  };
@@ -25274,7 +25086,7 @@ class Radio$1 extends React__namespace.Component {
25274
25086
  // Current versions of the radio widget always pass in the
25275
25087
  // "correct" value through the choices. Old serialized state
25276
25088
  // for radio widgets doesn't have this though, so we have to
25277
- // pull the correctness out of the review mode rubric. This
25089
+ // pull the correctness out of the review mode scoring data. This
25278
25090
  // only works because all of the places we use
25279
25091
  // `restoreSerializedState()` also turn on reviewMode, but is
25280
25092
  // fine for now.
@@ -25398,21 +25210,6 @@ const transform = (widgetOptions, strings, problemNum) => {
25398
25210
  selectedChoices: ___default["default"].pluck(choices, "correct")
25399
25211
  };
25400
25212
  };
25401
- const propUpgrades$1 = {
25402
- "1": v0props => {
25403
- const {
25404
- noneOfTheAbove,
25405
- ...rest
25406
- } = v0props;
25407
- if (noneOfTheAbove) {
25408
- throw new Error("radio widget v0 no longer supports auto noneOfTheAbove");
25409
- }
25410
- return {
25411
- ...rest,
25412
- hasNoneOfTheAbove: false
25413
- };
25414
- }
25415
- };
25416
25213
  var Radio = {
25417
25214
  name: "radio",
25418
25215
  displayName: "Radio / Multiple choice",
@@ -25420,15 +25217,15 @@ var Radio = {
25420
25217
  widget: Radio$1,
25421
25218
  transform: transform,
25422
25219
  staticTransform: transform,
25423
- version: {
25424
- major: 1,
25425
- minor: 0
25426
- },
25427
- propUpgrades: propUpgrades$1,
25220
+ version: perseusCore.radioLogic.version,
25221
+ propUpgrades: perseusCore.radioLogic.widgetOptionsUpgrades,
25428
25222
  isLintable: true,
25429
25223
  // TODO(LEMS-2656): remove TS suppression
25430
25224
  // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput
25431
- scorer: perseusScore.scoreRadio
25225
+ scorer: perseusScore.scoreRadio,
25226
+ // TODO(LEMS-2656): remove TS suppression
25227
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput
25228
+ validator: perseusScore.validateRadio
25432
25229
  };
25433
25230
 
25434
25231
  /* eslint-disable import/no-named-as-default */
@@ -25447,24 +25244,6 @@ const getPromptJSON$n = (renderProps, userInput) => {
25447
25244
  };
25448
25245
  };
25449
25246
 
25450
- /**
25451
- * For details on the individual options, see the
25452
- * PerseusCategorizerWidgetOptions type
25453
- */
25454
-
25455
- /**
25456
- * Given a PerseusCategorizerWidgetOptions object, return a new object with only
25457
- * the public options that should be exposed to the client.
25458
- */
25459
- function getCategorizerPublicWidgetOptions(options) {
25460
- return {
25461
- items: options.items,
25462
- categories: options.categories,
25463
- randomizeItems: options.randomizeItems,
25464
- static: options.static
25465
- };
25466
- }
25467
-
25468
25247
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
25469
25248
  class Categorizer extends React__namespace.Component {
25470
25249
  static contextType = PerseusI18nContext;
@@ -25647,7 +25426,10 @@ var Categorizer$1 = {
25647
25426
  // TODO(LEMS-2656): remove TS suppression
25648
25427
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
25649
25428
  scorer: perseusScore.scoreCategorizer,
25650
- getPublicWidgetOptions: getCategorizerPublicWidgetOptions
25429
+ // TODO(LEMS-2656): remove TS suppression
25430
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
25431
+ validator: perseusScore.validateCategorizer,
25432
+ getPublicWidgetOptions: perseusCore.getCategorizerPublicWidgetOptions
25651
25433
  };
25652
25434
 
25653
25435
  /**
@@ -26086,7 +25868,10 @@ var Dropdown$1 = {
26086
25868
  transform: optionsTransform,
26087
25869
  // TODO(LEMS-2656): remove TS suppression
26088
25870
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'.
26089
- scorer: perseusScore.scoreDropdown
25871
+ scorer: perseusScore.scoreDropdown,
25872
+ // TODO(LEMS-2656): remove TS suppression
25873
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'.
25874
+ validator: perseusScore.validateDropdown
26090
25875
  };
26091
25876
 
26092
25877
  const getPromptJSON$j = renderProps => {
@@ -26458,7 +26243,7 @@ class GradedGroup extends React__namespace.Component {
26458
26243
  DEFAULT_INVALID_MESSAGE_2
26459
26244
  } = this.context.strings;
26460
26245
  const status = score.type === "points" ? score.total === score.earned ? GRADING_STATUSES.correct : GRADING_STATUSES.incorrect : GRADING_STATUSES.invalid;
26461
- const message = score.type === "points" ? score.message || "" : score.message ? `${INVALID_MESSAGE_PREFIX} ${mapErrorToString(score.message)}` : `${INVALID_MESSAGE_PREFIX} ${DEFAULT_INVALID_MESSAGE_1}${DEFAULT_INVALID_MESSAGE_2}`;
26246
+ const message = score.type === "points" ? score.message || "" : score.message ? `${INVALID_MESSAGE_PREFIX} ${mapErrorToString(score.message, this.context.strings)}` : `${INVALID_MESSAGE_PREFIX} ${DEFAULT_INVALID_MESSAGE_1}${DEFAULT_INVALID_MESSAGE_2}`;
26462
26247
  this.setState({
26463
26248
  status: status,
26464
26249
  message: message,
@@ -27604,11 +27389,22 @@ const getPromptJSON$f = rendererJSON => {
27604
27389
 
27605
27390
  // The `group` widget is basically a widget hosting a full Perseus system in
27606
27391
  // it. As such, scoring a group means scoring all widgets it contains.
27607
- function scoreGroup(userInput, options, strings, locale) {
27608
- const scores = scoreWidgetsFunctional(options.widgets, Object.keys(options.widgets), userInput, strings, locale);
27392
+ function scoreGroup(userInput, rubric, strings, locale) {
27393
+ const scores = scoreWidgetsFunctional(rubric.widgets, Object.keys(rubric.widgets), userInput, strings, locale);
27609
27394
  return flattenScores(scores);
27610
27395
  }
27611
27396
 
27397
+ function validateGroup(userInput, validationData, strings, locale) {
27398
+ const emptyWidgets = emptyWidgetsFunctional(validationData.widgets, Object.keys(validationData.widgets), userInput, strings, locale);
27399
+ if (emptyWidgets.length === 0) {
27400
+ return null;
27401
+ }
27402
+ return {
27403
+ type: "invalid",
27404
+ message: null
27405
+ };
27406
+ }
27407
+
27612
27408
  // exports has no 'transform'
27613
27409
 
27614
27410
  class Group extends React__namespace.Component {
@@ -27761,8 +27557,11 @@ var Group$1 = {
27761
27557
  widget: Group,
27762
27558
  traverseChildWidgets: traverseChildWidgets,
27763
27559
  // TODO(LEMS-2656): remove TS suppression
27764
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
27560
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGroupUserInput'.
27765
27561
  scorer: scoreGroup,
27562
+ // TODO(LEMS-2656): remove TS suppression
27563
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGroupUserInput'.
27564
+ validator: validateGroup,
27766
27565
  hidden: true,
27767
27566
  isLintable: true
27768
27567
  };
@@ -30682,6 +30481,96 @@ function getInterceptStringForLine(line, strings, locale) {
30682
30481
  yIntercept: srFormatNumber(yIntercept, locale)
30683
30482
  });
30684
30483
  }
30484
+ function getCoordQuadrant(coord) {
30485
+ const [unroundedX, unroundedY] = coord;
30486
+ const x = Number(unroundedX.toFixed(3));
30487
+ const y = Number(unroundedY.toFixed(3));
30488
+ if (x === 0 && y === 0) {
30489
+ return "origin";
30490
+ }
30491
+ if (y === 0) {
30492
+ return "x-axis";
30493
+ }
30494
+ if (x === 0) {
30495
+ return "y-axis";
30496
+ }
30497
+ if (x > 0 && y > 0) {
30498
+ return 1;
30499
+ }
30500
+ if (x < 0 && y > 0) {
30501
+ return 2;
30502
+ }
30503
+ if (x < 0 && y < 0) {
30504
+ return 3;
30505
+ }
30506
+ return 4;
30507
+ }
30508
+ function getQuadraticVertexString(vertex, strings) {
30509
+ const location = getCoordQuadrant(vertex);
30510
+ switch (location) {
30511
+ case "origin":
30512
+ return strings.srQuadraticGraphVertexOrigin;
30513
+ case "x-axis":
30514
+ return strings.srQuadraticGraphVertexXAxis;
30515
+ case "y-axis":
30516
+ return strings.srQuadraticGraphVertexYAxis;
30517
+ default:
30518
+ return strings.srQuadraticGraphVertexQuadrant({
30519
+ quadrant: location
30520
+ });
30521
+ }
30522
+ }
30523
+ function getQuadraticPointString(pointNumber, coord, strings, locale) {
30524
+ const location = getCoordQuadrant(coord);
30525
+ const [x, y] = coord;
30526
+ switch (location) {
30527
+ case "origin":
30528
+ return strings.srQuadraticPointOrigin({
30529
+ pointNumber: pointNumber
30530
+ });
30531
+ case "x-axis":
30532
+ case "y-axis":
30533
+ return strings.srQuadraticPointAxis({
30534
+ pointNumber: pointNumber,
30535
+ x: srFormatNumber(x, locale),
30536
+ y: srFormatNumber(y, locale)
30537
+ });
30538
+ default:
30539
+ // return `Point ${pointNumber} on parabola in quadrant ${location} at ${srFormatNumber(x, locale)} comma ${srFormatNumber(y, locale)}.`;
30540
+ return strings.srQuadraticPointQuadrant({
30541
+ pointNumber: pointNumber,
30542
+ quadrant: location,
30543
+ x: srFormatNumber(x, locale),
30544
+ y: srFormatNumber(y, locale)
30545
+ });
30546
+ }
30547
+ }
30548
+ function getQuadraticXIntercepts(
30549
+ // Coefficients of the quadratic equation
30550
+ a, b, c) {
30551
+ // If a is 0, the equation is a line and has maybe only one x-intercept.
30552
+ if (a === 0) {
30553
+ // If b is 0, the equation a horizontal line and has no x-intercepts.
30554
+ if (b === 0) {
30555
+ return [];
30556
+ }
30557
+ return [-c / b];
30558
+ }
30559
+
30560
+ // Find the x-intercepts by plugging in y = 0 into the quadratic equation.
30561
+ // 0 = ax^2 + bx + c
30562
+ // x = (-b ± sqrt(b^2 - 4ac)) / 2a
30563
+ const discriminant = b * b - 4 * a * c;
30564
+ if (discriminant < 0) {
30565
+ return [];
30566
+ }
30567
+ const x1 = (-b + Math.sqrt(discriminant)) / (2 * a);
30568
+ const x2 = (-b - Math.sqrt(discriminant)) / (2 * a);
30569
+ if (x1 === x2) {
30570
+ return [x1];
30571
+ }
30572
+ return [x1, x2];
30573
+ }
30685
30574
 
30686
30575
  const {
30687
30576
  calculateAngleInDegrees: calculateAngleInDegrees$2
@@ -32519,7 +32408,9 @@ function renderLinearGraph(state, dispatch) {
32519
32408
  graphState: state,
32520
32409
  dispatch: dispatch
32521
32410
  }),
32522
- interactiveElementsDescription: null
32411
+ interactiveElementsDescription: /*#__PURE__*/React__namespace.createElement(LinearGraphDescription, {
32412
+ state: state
32413
+ })
32523
32414
  };
32524
32415
  }
32525
32416
  const LinearGraph = (props, key) => {
@@ -32538,33 +32429,29 @@ const LinearGraph = (props, key) => {
32538
32429
  const interceptDescriptionId = id + "-intercept";
32539
32430
  const slopeDescriptionId = id + "-slope";
32540
32431
 
32541
- // Aria label strings
32542
- const linearGraphPointsDescription = strings.srLinearGraphPoints({
32543
- point1X: srFormatNumber(line[0][0], locale),
32544
- point1Y: srFormatNumber(line[0][1], locale),
32545
- point2X: srFormatNumber(line[1][0], locale),
32546
- point2Y: srFormatNumber(line[1][1], locale)
32547
- });
32548
- const grabHandleAriaLabel = strings.srLinearGrabHandle({
32549
- point1X: srFormatNumber(line[0][0], locale),
32550
- point1Y: srFormatNumber(line[0][1], locale),
32551
- point2X: srFormatNumber(line[1][0], locale),
32552
- point2Y: srFormatNumber(line[1][1], locale)
32432
+ // Aria strings
32433
+ const {
32434
+ srLinearGraph,
32435
+ srLinearGraphPoints,
32436
+ srLinearGrabHandle,
32437
+ slopeString,
32438
+ interceptString
32439
+ } = describeLinearGraph(props.graphState, {
32440
+ strings,
32441
+ locale
32553
32442
  });
32554
- const slopeString = getSlopeStringForLine(line, strings);
32555
- const interceptString = getInterceptStringForLine(line, strings, locale);
32556
32443
 
32557
32444
  // Linear graphs only have one line
32558
32445
  // (LEMS-2050): Update the reducer so that we have a separate action for moving one line
32559
32446
  // and another action for moving multiple lines
32560
32447
  return /*#__PURE__*/React__namespace.createElement("g", {
32561
32448
  // Outer line minimal description
32562
- "aria-label": strings.srLinearGraph,
32449
+ "aria-label": srLinearGraph,
32563
32450
  "aria-describedby": `${pointsDescriptionId} ${interceptDescriptionId} ${slopeDescriptionId}`
32564
32451
  }, /*#__PURE__*/React__namespace.createElement(MovableLine, {
32565
32452
  key: 0,
32566
32453
  ariaLabels: {
32567
- grabHandleAriaLabel: grabHandleAriaLabel
32454
+ grabHandleAriaLabel: srLinearGrabHandle
32568
32455
  },
32569
32456
  ariaDescribedBy: `${interceptDescriptionId} ${slopeDescriptionId}`,
32570
32457
  points: line,
@@ -32580,7 +32467,7 @@ const LinearGraph = (props, key) => {
32580
32467
  }), /*#__PURE__*/React__namespace.createElement("g", {
32581
32468
  id: pointsDescriptionId,
32582
32469
  style: a11y.srOnly
32583
- }, linearGraphPointsDescription), /*#__PURE__*/React__namespace.createElement("g", {
32470
+ }, srLinearGraphPoints), /*#__PURE__*/React__namespace.createElement("g", {
32584
32471
  id: interceptDescriptionId,
32585
32472
  style: a11y.srOnly
32586
32473
  }, interceptString), /*#__PURE__*/React__namespace.createElement("g", {
@@ -32588,6 +32475,56 @@ const LinearGraph = (props, key) => {
32588
32475
  style: a11y.srOnly
32589
32476
  }, slopeString));
32590
32477
  };
32478
+ function LinearGraphDescription(_ref) {
32479
+ let {
32480
+ state
32481
+ } = _ref;
32482
+ // The reason that LinearGraphDescription is a component (rather than a
32483
+ // function that returns a string) is because it needs to use a
32484
+ // hook: `usePerseusI18n`.
32485
+ const i18n = usePerseusI18n();
32486
+ const strings = describeLinearGraph(state, i18n);
32487
+ return strings.srLinearInteractiveElement;
32488
+ }
32489
+
32490
+ // Exported for testing
32491
+ function describeLinearGraph(state, i18n) {
32492
+ const {
32493
+ coords: line
32494
+ } = state;
32495
+ const {
32496
+ strings,
32497
+ locale
32498
+ } = i18n;
32499
+
32500
+ // Aria label strings
32501
+ const srLinearGraph = strings.srLinearGraph;
32502
+ const srLinearGraphPoints = strings.srLinearGraphPoints({
32503
+ point1X: srFormatNumber(line[0][0], locale),
32504
+ point1Y: srFormatNumber(line[0][1], locale),
32505
+ point2X: srFormatNumber(line[1][0], locale),
32506
+ point2Y: srFormatNumber(line[1][1], locale)
32507
+ });
32508
+ const srLinearGrabHandle = strings.srLinearGrabHandle({
32509
+ point1X: srFormatNumber(line[0][0], locale),
32510
+ point1Y: srFormatNumber(line[0][1], locale),
32511
+ point2X: srFormatNumber(line[1][0], locale),
32512
+ point2Y: srFormatNumber(line[1][1], locale)
32513
+ });
32514
+ const slopeString = getSlopeStringForLine(line, strings);
32515
+ const interceptString = getInterceptStringForLine(line, strings, locale);
32516
+ const srLinearInteractiveElement = strings.srInteractiveElements({
32517
+ elements: [srLinearGraph, srLinearGraphPoints].join(" ")
32518
+ });
32519
+ return {
32520
+ srLinearGraph,
32521
+ srLinearGraphPoints,
32522
+ srLinearGrabHandle,
32523
+ slopeString,
32524
+ interceptString,
32525
+ srLinearInteractiveElement
32526
+ };
32527
+ }
32591
32528
 
32592
32529
  function renderLinearSystemGraph(state, dispatch) {
32593
32530
  return {
@@ -33146,13 +33083,13 @@ const hasFocusVisible = element => {
33146
33083
  }
33147
33084
  };
33148
33085
 
33149
- function renderQuadraticGraph(state, dispatch) {
33086
+ function renderQuadraticGraph(state, dispatch, i18n) {
33150
33087
  return {
33151
33088
  graph: /*#__PURE__*/React__namespace.createElement(QuadraticGraph, {
33152
33089
  graphState: state,
33153
33090
  dispatch: dispatch
33154
33091
  }),
33155
- interactiveElementsDescription: null
33092
+ interactiveElementsDescription: getQuadraticGraphDescription(state, i18n)
33156
33093
  };
33157
33094
  }
33158
33095
  function QuadraticGraph(props) {
@@ -33163,6 +33100,14 @@ function QuadraticGraph(props) {
33163
33100
  const {
33164
33101
  coords
33165
33102
  } = graphState;
33103
+ const {
33104
+ strings,
33105
+ locale
33106
+ } = usePerseusI18n();
33107
+ const id = React__namespace.useId();
33108
+ const quadraticDirectionId = id + "-direction";
33109
+ const quadraticVertexId = id + "-vertex";
33110
+ const quadraticInterceptsId = id + "-intercepts";
33166
33111
 
33167
33112
  // The coefficients are used to calculate the quadratic equation, plot the graph, and to indicate
33168
33113
  // to content creators the currently selected "correct answer". ex: y = 0.200x^2 + 0.000x + 0.000
@@ -33178,15 +33123,46 @@ function QuadraticGraph(props) {
33178
33123
 
33179
33124
  // Calculate the y value based on the current x value
33180
33125
  const y = x => (a * x + b) * x + c;
33181
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Plot.OfX, {
33126
+
33127
+ // Aria strings
33128
+ const {
33129
+ srQuadraticGraph,
33130
+ srQuadraticDirection,
33131
+ srQuadraticVertex,
33132
+ srQuadraticXIntercepts,
33133
+ srQuadraticYIntercept
33134
+ } = describeQuadraticGraph(graphState, {
33135
+ strings,
33136
+ locale
33137
+ });
33138
+ return /*#__PURE__*/React__namespace.createElement("g", {
33139
+ // Outer graph minimal description
33140
+ "aria-label": srQuadraticGraph,
33141
+ "aria-describedby": `${quadraticDirectionId} ${quadraticVertexId} ${quadraticInterceptsId}`
33142
+ }, /*#__PURE__*/React__namespace.createElement(mafs.Plot.OfX, {
33182
33143
  y: y,
33183
33144
  color: wonderBlocksTokens.color.blue
33184
- }), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33185
- key: "point-" + i,
33186
- point: coord,
33187
- sequenceNumber: i + 1,
33188
- onMove: destination => dispatch(actions.quadratic.movePoint(i, destination))
33189
- })));
33145
+ }), coords.map((coord, i) => {
33146
+ const srQuadraticPoint = getQuadraticPointString(i + 1, coord, strings, locale);
33147
+ // Vertex might not exist if the quadratic graph is a line
33148
+ const srVertex = srQuadraticVertex ? ` ${srQuadraticVertex}` : "";
33149
+ return /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33150
+ key: "point-" + i,
33151
+ ariaLabel: `${srQuadraticPoint}${srVertex}`,
33152
+ point: coord,
33153
+ sequenceNumber: i + 1,
33154
+ onMove: destination => dispatch(actions.quadratic.movePoint(i, destination))
33155
+ });
33156
+ }), srQuadraticDirection && /*#__PURE__*/React__namespace.createElement("g", {
33157
+ id: quadraticDirectionId,
33158
+ style: a11y.srOnly
33159
+ }, srQuadraticDirection), srQuadraticVertex && /*#__PURE__*/React__namespace.createElement("g", {
33160
+ id: quadraticVertexId,
33161
+ style: a11y.srOnly
33162
+ }, srQuadraticVertex), /*#__PURE__*/React__namespace.createElement("g", {
33163
+ id: quadraticInterceptsId,
33164
+ style: a11y.srOnly
33165
+ }, srQuadraticXIntercepts ? `${srQuadraticXIntercepts} ${srQuadraticYIntercept}` : `${srQuadraticYIntercept}`));
33190
33166
  }
33191
33167
 
33192
33168
  // Get the quadratic coefficients from the 3 control points
@@ -33209,6 +33185,60 @@ const getQuadraticCoefficients$2 = coords => {
33209
33185
  const c = (p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] + p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] + p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) / denom;
33210
33186
  return [a, b, c];
33211
33187
  };
33188
+ function getQuadraticGraphDescription(state, i18n) {
33189
+ const strings = describeQuadraticGraph(state, i18n);
33190
+ return strings.srQuadraticInteractiveElements;
33191
+ }
33192
+ // Exported for testing
33193
+ function describeQuadraticGraph(state, i18n) {
33194
+ const {
33195
+ strings,
33196
+ locale
33197
+ } = i18n;
33198
+ const coeffs = getQuadraticCoefficients$2(state.coords);
33199
+ const [a, b, c] = coeffs ?? [0, 0, 0];
33200
+ const vertex = [-b / (2 * a), c - b * b / (4 * a)];
33201
+ const xIntercepts = getQuadraticXIntercepts(a, b, c);
33202
+
33203
+ // Aria label strings
33204
+ const srQuadraticGraph = strings.srQuadraticGraph;
33205
+ const srQuadraticFaceUp = strings.srQuadraticFaceUp;
33206
+ const srQuadraticFaceDown = strings.srQuadraticFaceDown;
33207
+ // The graph only has a direction if it is not a line.
33208
+ const srQuadraticDirection = a === 0 ? undefined : a > 0 ? srQuadraticFaceUp : srQuadraticFaceDown;
33209
+ // Only describe vertex if the quadratic graph is not a line.
33210
+ // (Undefined means the quadratic graph is a line and has no vertex.)
33211
+ const srQuadraticVertex = a !== 0 ? getQuadraticVertexString(vertex, strings) : undefined;
33212
+ // Undefined means the quadratic graph has no x-intercepts,
33213
+ // such as when the graph is a horizontal line.
33214
+ const srQuadraticXIntercepts = xIntercepts.length === 2 ? strings.srQuadraticTwoXIntercepts({
33215
+ intercept1: srFormatNumber(xIntercepts[0], locale),
33216
+ intercept2: srFormatNumber(xIntercepts[1], locale)
33217
+ }) : xIntercepts.length === 1 ? strings.srQuadraticOneXIntercept({
33218
+ intercept: srFormatNumber(xIntercepts[0], locale)
33219
+ }) : undefined;
33220
+ const srQuadraticYIntercept = strings.srQuadraticYIntercept({
33221
+ intercept: srFormatNumber(c, locale)
33222
+ });
33223
+ const srQuadraticInteractiveElements = strings.srInteractiveElements({
33224
+ elements: strings.srQuadraticInteractiveElements({
33225
+ point1X: srFormatNumber(state.coords[0][0], locale),
33226
+ point1Y: srFormatNumber(state.coords[0][1], locale),
33227
+ point2X: srFormatNumber(state.coords[1][0], locale),
33228
+ point2Y: srFormatNumber(state.coords[1][1], locale),
33229
+ point3X: srFormatNumber(state.coords[2][0], locale),
33230
+ point3Y: srFormatNumber(state.coords[2][1], locale)
33231
+ })
33232
+ });
33233
+ return {
33234
+ srQuadraticGraph,
33235
+ srQuadraticDirection,
33236
+ srQuadraticVertex,
33237
+ srQuadraticXIntercepts,
33238
+ srQuadraticYIntercept,
33239
+ srQuadraticInteractiveElements
33240
+ };
33241
+ }
33212
33242
 
33213
33243
  function renderRayGraph(state, dispatch) {
33214
33244
  return {
@@ -33335,17 +33365,71 @@ function renderSegmentGraph(state, dispatch) {
33335
33365
  graphState: state,
33336
33366
  dispatch: dispatch
33337
33367
  }),
33338
- interactiveElementsDescription: null
33368
+ interactiveElementsDescription: /*#__PURE__*/React__namespace.createElement(SegmentGraphDescription, {
33369
+ state: state
33370
+ })
33339
33371
  };
33340
33372
  }
33341
- const SegmentGraph = props => {
33342
- const {
33343
- dispatch
33344
- } = props;
33373
+ const SegmentGraph = _ref => {
33374
+ let {
33375
+ dispatch,
33376
+ graphState
33377
+ } = _ref;
33345
33378
  const {
33346
33379
  coords: segments
33347
- } = props.graphState;
33348
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, segments?.map((segment, i) => /*#__PURE__*/React__namespace.createElement(MovableLine, {
33380
+ } = graphState;
33381
+ const {
33382
+ strings,
33383
+ locale
33384
+ } = usePerseusI18n();
33385
+ const segmentUniqueId = React__namespace.useId();
33386
+ const lengthDescriptionId = segmentUniqueId + "-length";
33387
+ const wholeGraphDescriptionId = segmentUniqueId + "-whole-graph";
33388
+ function getWholeSegmentGraphAriaLabel() {
33389
+ return segments?.length > 1 ? strings.srMultipleSegmentGraphAriaLabel({
33390
+ countOfSegments: segments.length
33391
+ }) : strings.srSingleSegmentGraphAriaLabel;
33392
+ }
33393
+ const wholeSegmentGraphAriaLabel = getWholeSegmentGraphAriaLabel();
33394
+ function getIndividualSegmentAriaLabel(segment, index) {
33395
+ if (segments.length === 1) {
33396
+ return strings.srSingleSegmentLabel({
33397
+ point1X: srFormatNumber(segments[0][0][X], locale),
33398
+ point1Y: srFormatNumber(segments[0][0][Y], locale),
33399
+ point2X: srFormatNumber(segments[0][1][X], locale),
33400
+ point2Y: srFormatNumber(segments[0][1][Y], locale)
33401
+ });
33402
+ }
33403
+ return strings.srMultipleSegmentIndividualLabel({
33404
+ point1X: srFormatNumber(segment[0][X], locale),
33405
+ point1Y: srFormatNumber(segment[0][Y], locale),
33406
+ point2X: srFormatNumber(segment[1][X], locale),
33407
+ point2Y: srFormatNumber(segment[1][Y], locale),
33408
+ indexOfSegment: index + 1
33409
+ });
33410
+ }
33411
+ function getWholeSegmentGraphAriaDescription() {
33412
+ return segments.map((segment, index) => getIndividualSegmentAriaLabel(segment, index)).join(" ");
33413
+ }
33414
+ function formatSegment(endpointNumber, x, y, index) {
33415
+ const segObj = {
33416
+ endpointNumber: endpointNumber,
33417
+ x: srFormatNumber(x, locale),
33418
+ y: srFormatNumber(y, locale)
33419
+ };
33420
+ return segments.length > 1 ? strings.srMultipleSegmentGraphEndpointAriaLabel({
33421
+ ...segObj,
33422
+ indexOfSegment: index
33423
+ }) : strings.srSingleSegmentGraphEndpointAriaLabel(segObj);
33424
+ }
33425
+ return /*#__PURE__*/React__namespace.createElement("g", {
33426
+ "aria-label": wholeSegmentGraphAriaLabel,
33427
+ "aria-describedby": wholeGraphDescriptionId
33428
+ }, segments?.map((segment, i) => /*#__PURE__*/React__namespace.createElement("g", {
33429
+ "aria-label": getIndividualSegmentAriaLabel(segment, i),
33430
+ "aria-describedby": lengthDescriptionId,
33431
+ key: `${segmentUniqueId}-${i}`
33432
+ }, /*#__PURE__*/React__namespace.createElement(MovableLine, {
33349
33433
  key: i,
33350
33434
  points: segment,
33351
33435
  onMoveLine: delta => {
@@ -33353,9 +33437,61 @@ const SegmentGraph = props => {
33353
33437
  },
33354
33438
  onMovePoint: (endpointIndex, destination) => {
33355
33439
  dispatch(actions.segment.movePointInFigure(i, endpointIndex, destination));
33440
+ },
33441
+ ariaLabels: {
33442
+ point1AriaLabel: formatSegment(1, segment[0][X], segment[0][Y], i + 1),
33443
+ point2AriaLabel: formatSegment(2, segment[1][X], segment[1][Y], i + 1),
33444
+ grabHandleAriaLabel: strings.srSegmentGrabHandle({
33445
+ point1X: srFormatNumber(segment[0][X], locale),
33446
+ point1Y: srFormatNumber(segment[0][Y], locale),
33447
+ point2X: srFormatNumber(segment[1][X], locale),
33448
+ point2Y: srFormatNumber(segment[1][Y], locale)
33449
+ })
33356
33450
  }
33357
- })));
33451
+ }), /*#__PURE__*/React__namespace.createElement("g", {
33452
+ id: lengthDescriptionId,
33453
+ style: a11y.srOnly
33454
+ }, strings.srSegmentLength({
33455
+ length: srFormatNumber(getLengthOfSegment(segment), locale)
33456
+ })))), /*#__PURE__*/React__namespace.createElement("g", {
33457
+ style: a11y.srOnly,
33458
+ id: wholeGraphDescriptionId
33459
+ }, getWholeSegmentGraphAriaDescription()));
33358
33460
  };
33461
+ function getLengthOfSegment(segment) {
33462
+ return kmath.point.distanceToPoint(...segment);
33463
+ }
33464
+ function SegmentGraphDescription(_ref2) {
33465
+ let {
33466
+ state
33467
+ } = _ref2;
33468
+ // The reason that SegmentGraphDescription is a component (rather than a
33469
+ // function that returns a string) is because it needs to use a
33470
+ // hook: `usePerseusI18n`.
33471
+ const i18n = usePerseusI18n();
33472
+ return describeSegmentGraph(state, i18n);
33473
+ }
33474
+
33475
+ // Exported for testing
33476
+ function describeSegmentGraph(state, i18n) {
33477
+ const {
33478
+ strings,
33479
+ locale
33480
+ } = i18n;
33481
+ const segmentDescriptions = state.coords.map((_ref3, index) => {
33482
+ let [point1, point2] = _ref3;
33483
+ return strings.srMultipleSegmentIndividualLabel({
33484
+ point1X: srFormatNumber(point1[X], locale),
33485
+ point1Y: srFormatNumber(point1[Y], locale),
33486
+ point2X: srFormatNumber(point2[X], locale),
33487
+ point2Y: srFormatNumber(point2[Y], locale),
33488
+ indexOfSegment: index + 1
33489
+ });
33490
+ });
33491
+ return strings.srInteractiveElements({
33492
+ elements: segmentDescriptions.join(" ")
33493
+ });
33494
+ }
33359
33495
 
33360
33496
  function renderSinusoidGraph(state, dispatch) {
33361
33497
  return {
@@ -33631,9 +33767,10 @@ const MafsGraph = props => {
33631
33767
  x: viewboxX,
33632
33768
  y: viewboxY
33633
33769
  };
33770
+ const i18n = usePerseusI18n();
33634
33771
  const {
33635
33772
  strings
33636
- } = usePerseusI18n();
33773
+ } = i18n;
33637
33774
  const interactionPrompt = isUnlimitedGraphState(state) && state.showKeyboardInteractionInvitation;
33638
33775
  wonderBlocksCore.useOnMountEffect(() => {
33639
33776
  analytics.onAnalyticsEvent({
@@ -33650,7 +33787,8 @@ const MafsGraph = props => {
33650
33787
  interactiveElementsDescription
33651
33788
  } = renderGraphElements({
33652
33789
  state,
33653
- dispatch
33790
+ dispatch,
33791
+ i18n
33654
33792
  });
33655
33793
  return /*#__PURE__*/React__namespace.createElement(GraphConfigContext.Provider, {
33656
33794
  value: {
@@ -34027,7 +34165,8 @@ const calculateNestedSVGCoords = (range, width, height) => {
34027
34165
  const renderGraphElements = props => {
34028
34166
  const {
34029
34167
  state,
34030
- dispatch
34168
+ dispatch,
34169
+ i18n
34031
34170
  } = props;
34032
34171
  const {
34033
34172
  type
@@ -34050,7 +34189,7 @@ const renderGraphElements = props => {
34050
34189
  case "circle":
34051
34190
  return renderCircleGraph(state, dispatch);
34052
34191
  case "quadratic":
34053
- return renderQuadraticGraph(state, dispatch);
34192
+ return renderQuadraticGraph(state, dispatch, i18n);
34054
34193
  case "sinusoid":
34055
34194
  return renderSinusoidGraph(state, dispatch);
34056
34195
  case "none":
@@ -37670,19 +37809,21 @@ class LabelImage extends React__namespace.Component {
37670
37809
  this._mounted = false;
37671
37810
  }
37672
37811
  getUserInput() {
37673
- const {
37674
- markers
37675
- } = this.props;
37676
37812
  return {
37677
- markers
37813
+ markers: this.props.markers.map(marker => ({
37814
+ selected: marker.selected,
37815
+ label: marker.label
37816
+ }))
37678
37817
  };
37679
37818
  }
37680
37819
  getPromptJSON() {
37681
37820
  return getPromptJSON$a(this.props, this.getUserInput());
37682
37821
  }
37683
37822
 
37684
- // TODO(LEMS-2544): Investigate impact on scoring; possibly pull out &/or remove rubric parameter.
37685
- showRationalesForCurrentlySelectedChoices(rubric) {
37823
+ // TODO(LEMS-2544): Investigate impact on scoring
37824
+ // Also consider how scoreMarker is being called as it seems to require the marker.answers property.
37825
+ // Removed rubric parameter, but it gets a full widget options object from the renderer
37826
+ showRationalesForCurrentlySelectedChoices() {
37686
37827
  const {
37687
37828
  markers
37688
37829
  } = this.props;
@@ -37690,7 +37831,7 @@ class LabelImage extends React__namespace.Component {
37690
37831
  onChange
37691
37832
  } = this.props;
37692
37833
  const updatedMarkers = markers.map(marker => {
37693
- const score = perseusScore.scoreLabelImageMarker(marker);
37834
+ const score = perseusScore.scoreLabelImageMarker(marker.selected, marker.answers);
37694
37835
  return {
37695
37836
  ...marker,
37696
37837
  // Reveal correctness state for markers with answers.
@@ -37798,6 +37939,7 @@ class LabelImage extends React__namespace.Component {
37798
37939
  selected: selected.length ? selected : undefined
37799
37940
  });
37800
37941
  }
37942
+ // TODO(LEMS-2723): Investigate if possible to change this to not require answers
37801
37943
  renderMarkers() {
37802
37944
  const {
37803
37945
  markers,
@@ -37839,7 +37981,7 @@ class LabelImage extends React__namespace.Component {
37839
37981
  bottom: "top"
37840
37982
  }[markerPosition];
37841
37983
  }
37842
- const score = perseusScore.scoreLabelImageMarker(marker);
37984
+ const score = perseusScore.scoreLabelImageMarker(marker.selected, marker.answers);
37843
37985
  // Once the question is answered, show markers
37844
37986
  // with correct answers, otherwise passthrough
37845
37987
  // the correctness state.
@@ -39384,7 +39526,10 @@ var Matrix$1 = {
39384
39526
  isLintable: true,
39385
39527
  // TODO(LEMS-2656): remove TS suppression
39386
39528
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
39387
- scorer: perseusScore.scoreMatrix
39529
+ scorer: perseusScore.scoreMatrix,
39530
+ // TODO(LEMS-2656): remove TS suppression
39531
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
39532
+ validator: perseusScore.validateMatrix
39388
39533
  };
39389
39534
 
39390
39535
  const getPromptJSON$7 = () => {
@@ -39493,34 +39638,13 @@ class Measurer extends React__namespace.Component {
39493
39638
  }));
39494
39639
  }
39495
39640
  }
39496
- const propUpgrades = {
39497
- "1": v0props => {
39498
- const {
39499
- imageUrl,
39500
- imageTop,
39501
- imageLeft,
39502
- ...rest
39503
- } = v0props;
39504
- return {
39505
- ...rest,
39506
- image: {
39507
- url: imageUrl,
39508
- top: imageTop,
39509
- left: imageLeft
39510
- }
39511
- };
39512
- }
39513
- };
39514
39641
  var Measurer$1 = {
39515
39642
  name: "measurer",
39516
39643
  displayName: "Measurer",
39517
39644
  hidden: true,
39518
39645
  widget: Measurer,
39519
- version: {
39520
- major: 1,
39521
- minor: 0
39522
- },
39523
- propUpgrades: propUpgrades,
39646
+ version: perseusCore.measurerLogic.version,
39647
+ propUpgrades: perseusCore.measurerLogic.widgetOptionsUpgrades,
39524
39648
  // TODO: things that aren't interactive shouldn't need scoring functions
39525
39649
  scorer: () => scoreNoop(1)
39526
39650
  };
@@ -40095,10 +40219,10 @@ function parseBondModifier(smiles, ctx) {
40095
40219
  const firstChar = smiles[0];
40096
40220
  const rest = smiles.slice(1);
40097
40221
  if (firstChar === "=") {
40098
- return parse$2(rest, _mset(ctx, ["bond", "bondType"], "double"));
40222
+ return parse$1(rest, _mset(ctx, ["bond", "bondType"], "double"));
40099
40223
  }
40100
40224
  if (firstChar === "#") {
40101
- return parse$2(rest, _mset(ctx, ["bond", "bondType"], "triple"));
40225
+ return parse$1(rest, _mset(ctx, ["bond", "bondType"], "triple"));
40102
40226
  }
40103
40227
  throw new ParseError$1("Invalid character: " + firstChar);
40104
40228
  }
@@ -40159,8 +40283,8 @@ function parseParenthesizedExpression(smiles, ctx) {
40159
40283
  idx: newCtx.idx.concat([[inBranchIdx, 0]]),
40160
40284
  parens: newCtx.parens.concat("(")
40161
40285
  };
40162
- const parenExpr = parse$2(rest, parenCtx);
40163
- const remainder = parse$2(sliceFromMatchingCloseParen(rest, ["("]), newCtx);
40286
+ const parenExpr = parse$1(rest, parenCtx);
40287
+ const remainder = parse$1(sliceFromMatchingCloseParen(rest, ["("]), newCtx);
40164
40288
  return [parenExpr].concat(remainder);
40165
40289
  }
40166
40290
  if (firstChar === ")") {
@@ -40224,7 +40348,7 @@ function parseAtom(smiles, ctx) {
40224
40348
 
40225
40349
  // increment the atom counter and reset the branch counter
40226
40350
  const newCtx = _mset(ctx, ["idx", ctx.idx.length - 1], [1 + ctx.idx[ctx.idx.length - 1][0], 0]);
40227
- let restOfMolecule = parse$2(rest, _mset(newCtx, ["bond", "bondType"], "single"));
40351
+ let restOfMolecule = parse$1(rest, _mset(newCtx, ["bond", "bondType"], "single"));
40228
40352
  if (!Array.isArray(restOfMolecule) && !!restOfMolecule) {
40229
40353
  //TODO(colin): fix this awkwardness.
40230
40354
  restOfMolecule = [restOfMolecule];
@@ -40263,7 +40387,7 @@ function isModifierChar(s) {
40263
40387
  * ParseError: if the input is not valid SMILES or contains features not
40264
40388
  * yet implemented.
40265
40389
  */
40266
- function parse$2(smiles, ctx) {
40390
+ function parse$1(smiles, ctx) {
40267
40391
  if (!validate(smiles)) {
40268
40392
  throw new ParseError$1("Invalid input.");
40269
40393
  }
@@ -40288,7 +40412,7 @@ function parse$2(smiles, ctx) {
40288
40412
  return parseParenthesizedExpression(smiles, ctx);
40289
40413
  }
40290
40414
  var SmilesParser = {
40291
- parse: parse$2,
40415
+ parse: parse$1,
40292
40416
  ParseError: ParseError$1
40293
40417
  };
40294
40418
 
@@ -40296,7 +40420,7 @@ var SmilesParser = {
40296
40420
  const {
40297
40421
  layout
40298
40422
  } = MoleculeLayout;
40299
- const parse$1 = SmilesParser.parse;
40423
+ const parse = SmilesParser.parse;
40300
40424
  const ParseError = SmilesParser.ParseError;
40301
40425
  const borderSize = 30;
40302
40426
  class Molecule extends React__namespace.Component {
@@ -40323,7 +40447,7 @@ class Molecule extends React__namespace.Component {
40323
40447
  try {
40324
40448
  this.setState({
40325
40449
  // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
40326
- parsedSmiles: parse$1(smiles),
40450
+ parsedSmiles: parse(smiles),
40327
40451
  error: null
40328
40452
  });
40329
40453
  } catch (e) {
@@ -40988,7 +41112,10 @@ var NumberLine$1 = {
40988
41112
  staticTransform: staticTransform$1,
40989
41113
  // TODO(LEMS-2656): remove TS suppression
40990
41114
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
40991
- scorer: perseusScore.scoreNumberLine
41115
+ scorer: perseusScore.scoreNumberLine,
41116
+ // TODO(LEMS-2656): remove TS suppression
41117
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
41118
+ validator: perseusScore.validateNumberLine
40992
41119
  };
40993
41120
 
40994
41121
  const getPromptJSON$5 = (renderProps, userInput) => {
@@ -41542,7 +41669,11 @@ var Orderer$1 = {
41542
41669
  isLintable: true,
41543
41670
  // TODO(LEMS-2656): remove TS suppression
41544
41671
  // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
41545
- scorer: perseusScore.scoreOrderer
41672
+ scorer: perseusScore.scoreOrderer,
41673
+ // TODO(LEMS-2656): remove TS suppression
41674
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
41675
+ validator: perseusScore.validateOrderer,
41676
+ getPublicWidgetOptions: perseusCore.getOrdererPublicWidgetOptions
41546
41677
  };
41547
41678
 
41548
41679
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -41591,10 +41722,6 @@ var PassageRefTarget$1 = {
41591
41722
  transform: editorProps => {
41592
41723
  return ___default["default"].pick(editorProps, "content");
41593
41724
  },
41594
- version: {
41595
- major: 0,
41596
- minor: 0
41597
- },
41598
41725
  isLintable: true,
41599
41726
  // TODO: things that aren't interactive shouldn't need scoring functions
41600
41727
  scorer: () => scoreNoop()
@@ -42618,7 +42745,10 @@ var Plotter$1 = {
42618
42745
  staticTransform: staticTransform,
42619
42746
  // TODO(LEMS-2656): remove TS suppression
42620
42747
  // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
42621
- scorer: perseusScore.scorePlotter
42748
+ scorer: perseusScore.scorePlotter,
42749
+ // TODO(LEMS-2656): remove TS suppression
42750
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
42751
+ validator: perseusScore.validatePlotter
42622
42752
  };
42623
42753
 
42624
42754
  const getPromptJSON$2 = () => {
@@ -42781,7 +42911,10 @@ var Sorter$1 = {
42781
42911
  isLintable: true,
42782
42912
  // TODO(LEMS-2656): remove TS suppression
42783
42913
  // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
42784
- scorer: perseusScore.scoreSorter
42914
+ scorer: perseusScore.scoreSorter,
42915
+ // TODO(LEMS-2656): remove TS suppression
42916
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
42917
+ validator: perseusScore.validateSorter
42785
42918
  };
42786
42919
 
42787
42920
  const {
@@ -43000,7 +43133,10 @@ var Table$1 = {
43000
43133
  isLintable: true,
43001
43134
  // TODO(LEMS-2656): remove TS suppression
43002
43135
  // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
43003
- scorer: perseusScore.scoreTable
43136
+ scorer: perseusScore.scoreTable,
43137
+ // TODO(LEMS-2656): remove TS suppression
43138
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
43139
+ validator: perseusScore.validateTable
43004
43140
  };
43005
43141
 
43006
43142
  const getPromptJSON = () => {
@@ -43185,7 +43321,7 @@ const init = function () {
43185
43321
 
43186
43322
  // This file is processed by a Rollup plugin (replace) to inject the production
43187
43323
  const libName = "@khanacademy/perseus";
43188
- const libVersion = "51.0.1";
43324
+ const libVersion = "52.0.1";
43189
43325
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
43190
43326
 
43191
43327
  const apiVersion = {
@@ -44631,6 +44767,69 @@ const registerAllWidgetsForTesting = () => {
44631
44767
  replaceDeprecatedWidgets();
44632
44768
  };
44633
44769
 
44770
+ /**
44771
+ * Get a widget type by a widget's ID
44772
+ * (please don't derive type from ID)
44773
+ *
44774
+ * @param {string} widgetId the ID of the widget
44775
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44776
+ * @returns {string} the widget type (ie "radio")
44777
+ */
44778
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
44779
+ const widget = widgetMap[WidgetId];
44780
+ return widget?.type ?? null;
44781
+ }
44782
+
44783
+ /**
44784
+ * Does the content have a specific type of widget?
44785
+ *
44786
+ * @param {string} type the type of the widget in question (ie "radio")
44787
+ * @param {string} content the string to search through
44788
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44789
+ * @returns {boolean} if the content includes the widget type
44790
+ */
44791
+ function contentHasWidgetType(type, content, widgetMap) {
44792
+ return perseusCore.getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
44793
+ }
44794
+
44795
+ /**
44796
+ * Pull the widget map out of ItemData
44797
+ *
44798
+ * @param {PerseusItem} PerseusItem containing a widgetMap
44799
+ * @returns {WidgetMap} the widget map in the PerseusItem
44800
+ */
44801
+ function getWidgetsMapFromItemData(itemData) {
44802
+ return itemData.question.widgets;
44803
+ }
44804
+
44805
+ /**
44806
+ * Get the widget information from a WidgetMap
44807
+ *
44808
+ * @param {string} widgetId the ID of the widget
44809
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
44810
+ */
44811
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
44812
+ return widgetMap[widgetId] ?? null;
44813
+ }
44814
+
44815
+ /**
44816
+ * Get select widgets from a widget map.
44817
+ *
44818
+ * @param {ReadonlyArray<string>} widgetIds to extract
44819
+ * @param {PerseusWidgetsMap} widgetMap to extract from
44820
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
44821
+ */
44822
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
44823
+ const widgets = {};
44824
+ widgetIds.forEach(widgetId => {
44825
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
44826
+ if (widget) {
44827
+ widgets[widgetId] = widget;
44828
+ }
44829
+ });
44830
+ return widgets;
44831
+ }
44832
+
44634
44833
  /**
44635
44834
  * This function extracts the answers from the widgets.
44636
44835
  *
@@ -45154,1683 +45353,6 @@ const getValidWidgetIds = perseusItem => {
45154
45353
  return wonderStuffCore.keys(widgets).filter(id => isWidgetIdInContent(perseusItem, id));
45155
45354
  };
45156
45355
 
45157
- function isRealJSONParse(jsonParse) {
45158
- const randomPhrase = buildRandomPhrase();
45159
- const randomHintPhrase = buildRandomPhrase();
45160
- const randomString = buildRandomString();
45161
- const testingObject = JSON.stringify({
45162
- answerArea: {
45163
- calculator: false,
45164
- chi2Table: false,
45165
- financialCalculatorMonthlyPayment: false,
45166
- financialCalculatorTimeToPayOff: false,
45167
- financialCalculatorTotalAmount: false,
45168
- periodicTable: false,
45169
- periodicTableWithKey: false,
45170
- tTable: false,
45171
- zTable: false
45172
- },
45173
- hints: [randomHintPhrase, `=${Math.floor(Math.random() * 50) + 1}`],
45174
- itemDataVersion: {
45175
- major: 0,
45176
- minor: 1
45177
- },
45178
- question: {
45179
- content: `${randomPhrase}`,
45180
- images: {},
45181
- widgets: {
45182
- expression1: {
45183
- alignment: "default",
45184
- graded: false,
45185
- options: {
45186
- answerForms: [{
45187
- considered: "wrong",
45188
- form: false,
45189
- key: 0,
45190
- simplify: false,
45191
- value: `${randomString}`
45192
- }],
45193
- ariaLabel: "Answer",
45194
- buttonSets: ["basic"],
45195
- functions: ["f", "g", "h"],
45196
- static: true,
45197
- times: false,
45198
- visibleLabel: "Answer"
45199
- },
45200
- static: true,
45201
- type: "expression",
45202
- version: {
45203
- major: 1,
45204
- minor: 0
45205
- }
45206
- }
45207
- }
45208
- }
45209
- });
45210
- const testJSON = buildTestData(testingObject.replace(/"/g, '\\"'));
45211
- const parsedTestJSON = jsonParse(testJSON);
45212
- const parsedTestItemData = parsedTestJSON.data.assessmentItem.item.itemData;
45213
- return perseusCore.approximateDeepEqual(parsedTestItemData, testingObject);
45214
- }
45215
- function buildRandomString() {
45216
- let capitalize = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
45217
- let randomString = "";
45218
- const randomLength = Math.floor(Math.random() * 8) + 3;
45219
- for (let i = 0; i < randomLength; i++) {
45220
- const randomLetter = String.fromCharCode(97 + Math.floor(Math.random() * 26));
45221
- randomString += capitalize && i === 0 ? randomLetter.toUpperCase() : randomLetter;
45222
- }
45223
- return randomString;
45224
- }
45225
- function buildRandomPhrase() {
45226
- const phrases = [];
45227
- const randomLength = Math.floor(Math.random() * 10) + 5;
45228
- for (let i = 0; i < randomLength; i++) {
45229
- phrases.push(buildRandomString(i === 0));
45230
- }
45231
- const modifierStart = ["**", "$"];
45232
- const modifierEnd = ["**", "$"];
45233
- const modifierIndex = Math.floor(Math.random() * modifierStart.length);
45234
- return `${modifierStart[modifierIndex]}${phrases.join(" ")}${modifierEnd[modifierIndex]}`;
45235
- }
45236
- function buildTestData(testObject) {
45237
- return `{"data":{"assessmentItem":{"__typename":"AssessmentItemOrError","error":null,"item":{"__typename":"AssessmentItem","id":"x890b3c70f3e8f4a6","itemData":"${testObject}","problemType":"Type 1","sha":"c7284a3ad65214b4e62bccce236d92f7f5d35941"}}}}`;
45238
- }
45239
-
45240
- function success(value) {
45241
- return {
45242
- type: "success",
45243
- value
45244
- };
45245
- }
45246
- function failure(detail) {
45247
- return {
45248
- type: "failure",
45249
- detail
45250
- };
45251
- }
45252
- function isFailure(result) {
45253
- return result.type === "failure";
45254
- }
45255
- function isSuccess(result) {
45256
- return result.type === "success";
45257
- }
45258
-
45259
- // Result's `all` function is similar to Promise.all: given an array of
45260
- // results, it returns success if all succeeded, and failure if any failed.
45261
- function all(results) {
45262
- let combineFailureDetails = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : a => a;
45263
- const values = [];
45264
- const failureDetails = [];
45265
- for (const result of results) {
45266
- if (result.type === "success") {
45267
- values.push(result.value);
45268
- } else {
45269
- failureDetails.push(result.detail);
45270
- }
45271
- }
45272
- if (failureDetails.length > 0) {
45273
- return failure(failureDetails.reduce(combineFailureDetails));
45274
- }
45275
- return success(values);
45276
- }
45277
-
45278
- class ErrorTrackingParseContext {
45279
- constructor(path) {
45280
- this.path = path;
45281
- }
45282
- failure(expected, badValue) {
45283
- return failure([{
45284
- expected: wrapInArray(expected),
45285
- badValue,
45286
- path: this.path
45287
- }]);
45288
- }
45289
- forSubtree(key) {
45290
- return new ErrorTrackingParseContext([...this.path, key]);
45291
- }
45292
- success(value) {
45293
- return success(value);
45294
- }
45295
- }
45296
- function wrapInArray(a) {
45297
- return Array.isArray(a) ? a : [a];
45298
- }
45299
-
45300
- function formatPath(path) {
45301
- return "(root)" + path.map(formatPathSegment).join("");
45302
- }
45303
- function formatPathSegment(segment) {
45304
- if (typeof segment === "string") {
45305
- return validIdentifier.test(segment) ? "." + segment : `[${JSON.stringify(segment)}]`;
45306
- }
45307
- return `[${segment.toString()}]`;
45308
- }
45309
- const validIdentifier = /^[A-Za-z$_][A-Za-z$_0-9]*$/;
45310
-
45311
- function message(failure) {
45312
- const expected = conjoin(failure.expected);
45313
- const path = formatPath(failure.path);
45314
- const badValue = JSON.stringify(failure.badValue);
45315
- return `At ${path} -- expected ${expected}, but got ${badValue}`;
45316
- }
45317
- function conjoin(items) {
45318
- switch (items.length) {
45319
- // TODO(benchristel): handle 0 if this is reused elsewhere.
45320
- case 1:
45321
- return items[0];
45322
- case 2:
45323
- return items.join(" or ");
45324
- default:
45325
- {
45326
- const allButLast = items.slice(0, items.length - 1);
45327
- const last = items[items.length - 1];
45328
- return allButLast.join(", ") + ", or " + last;
45329
- }
45330
- }
45331
- }
45332
-
45333
- function parse(value, parser) {
45334
- const result = parser(value, new ErrorTrackingParseContext([]));
45335
- if (isFailure(result)) {
45336
- return failure(result.detail.map(message).join("; "));
45337
- }
45338
- return result;
45339
- }
45340
-
45341
- const any = (rawValue, ctx) => ctx.success(rawValue);
45342
-
45343
- function array(elementParser) {
45344
- return (rawValue, ctx) => {
45345
- if (!Array.isArray(rawValue)) {
45346
- return ctx.failure("array", rawValue);
45347
- }
45348
- const elementResults = rawValue.map((elem, i) => elementParser(elem, ctx.forSubtree(i)));
45349
- return all(elementResults, concat);
45350
- };
45351
- }
45352
- function concat(a, b) {
45353
- return [...a, ...b];
45354
- }
45355
-
45356
- function boolean(rawValue, ctx) {
45357
- if (typeof rawValue === "boolean") {
45358
- return ctx.success(rawValue);
45359
- }
45360
- return ctx.failure("boolean", rawValue);
45361
- }
45362
-
45363
- function constant(acceptedValue) {
45364
- return (rawValue, ctx) => {
45365
- if (rawValue !== acceptedValue) {
45366
- return ctx.failure(String(JSON.stringify(acceptedValue)), rawValue);
45367
- }
45368
- return ctx.success(acceptedValue);
45369
- };
45370
- }
45371
-
45372
- function enumeration() {
45373
- for (var _len = arguments.length, acceptedValues = new Array(_len), _key = 0; _key < _len; _key++) {
45374
- acceptedValues[_key] = arguments[_key];
45375
- }
45376
- return (rawValue, ctx) => {
45377
- if (typeof rawValue === "string") {
45378
- const index = acceptedValues.indexOf(rawValue);
45379
- if (index > -1) {
45380
- return ctx.success(acceptedValues[index]);
45381
- }
45382
- }
45383
- const expected = acceptedValues.map(v => JSON.stringify(v));
45384
- return ctx.failure(expected, rawValue);
45385
- };
45386
- }
45387
-
45388
- function isObject(x) {
45389
- return x != null && Object.getPrototypeOf(x) === Object.prototype;
45390
- }
45391
-
45392
- function nullable(parseValue) {
45393
- return (rawValue, ctx) => {
45394
- if (rawValue === null) {
45395
- return ctx.success(rawValue);
45396
- }
45397
- return parseValue(rawValue, ctx);
45398
- };
45399
- }
45400
-
45401
- const number = (rawValue, ctx) => {
45402
- if (typeof rawValue === "number") {
45403
- return ctx.success(rawValue);
45404
- }
45405
- return ctx.failure("number", rawValue);
45406
- };
45407
-
45408
- function object(schema) {
45409
- return (rawValue, ctx) => {
45410
- if (!isObject(rawValue)) {
45411
- return ctx.failure("object", rawValue);
45412
- }
45413
- const ret = {
45414
- ...rawValue
45415
- };
45416
- const mismatches = [];
45417
- for (const [prop, propParser] of Object.entries(schema)) {
45418
- const result = propParser(rawValue[prop], ctx.forSubtree(prop));
45419
- if (isSuccess(result)) {
45420
- if (result.value !== undefined || prop in rawValue) {
45421
- ret[prop] = result.value;
45422
- }
45423
- } else {
45424
- mismatches.push(...result.detail);
45425
- }
45426
- }
45427
- if (mismatches.length > 0) {
45428
- return failure(mismatches);
45429
- }
45430
- return ctx.success(ret);
45431
- };
45432
- }
45433
-
45434
- function optional(parseValue) {
45435
- return (rawValue, ctx) => {
45436
- if (rawValue === undefined) {
45437
- return ctx.success(rawValue);
45438
- }
45439
- return parseValue(rawValue, ctx);
45440
- };
45441
- }
45442
-
45443
- function pair(parseA, parseB) {
45444
- return (rawValue, ctx) => {
45445
- if (!Array.isArray(rawValue)) {
45446
- return ctx.failure("array", rawValue);
45447
- }
45448
- if (rawValue.length !== 2) {
45449
- return ctx.failure("array of length 2", rawValue);
45450
- }
45451
- const [rawA, rawB] = rawValue;
45452
- const resultA = parseA(rawA, ctx.forSubtree(0));
45453
- if (isFailure(resultA)) {
45454
- return resultA;
45455
- }
45456
- const resultB = parseB(rawB, ctx.forSubtree(1));
45457
- if (isFailure(resultB)) {
45458
- return resultB;
45459
- }
45460
- return ctx.success([resultA.value, resultB.value]);
45461
- };
45462
- }
45463
-
45464
- function pipeParsers(p) {
45465
- return new ParserPipeline(p);
45466
- }
45467
- class ParserPipeline {
45468
- constructor(parser) {
45469
- this.parser = parser;
45470
- }
45471
- then(nextParser) {
45472
- return new ParserPipeline(composeParsers(this.parser, nextParser));
45473
- }
45474
- }
45475
- function composeParsers(parserA, parserB) {
45476
- return (rawValue, ctx) => {
45477
- const partialResult = parserA(rawValue, ctx);
45478
- if (isFailure(partialResult)) {
45479
- return partialResult;
45480
- }
45481
- return parserB(partialResult.value, ctx);
45482
- };
45483
- }
45484
-
45485
- function record(parseKey, parseValue) {
45486
- return (rawValue, ctx) => {
45487
- if (!isObject(rawValue)) {
45488
- return ctx.failure("object", rawValue);
45489
- }
45490
- const result = {};
45491
- const mismatches = [];
45492
- for (const [key, value] of Object.entries(rawValue)) {
45493
- const entryCtx = ctx.forSubtree(key);
45494
- const keyResult = parseKey(key, entryCtx);
45495
- if (isFailure(keyResult)) {
45496
- mismatches.push(...keyResult.detail);
45497
- }
45498
- const valueResult = parseValue(value, entryCtx);
45499
- if (isFailure(valueResult)) {
45500
- mismatches.push(...valueResult.detail);
45501
- }
45502
- if (isSuccess(keyResult) && isSuccess(valueResult)) {
45503
- result[keyResult.value] = valueResult.value;
45504
- }
45505
- }
45506
- if (mismatches.length > 0) {
45507
- return failure(mismatches);
45508
- }
45509
- return ctx.success(result);
45510
- };
45511
- }
45512
-
45513
- const string = (rawValue, ctx) => {
45514
- if (typeof rawValue === "string") {
45515
- return ctx.success(rawValue);
45516
- }
45517
- return ctx.failure("string", rawValue);
45518
- };
45519
-
45520
- function trio(parseA, parseB, parseC) {
45521
- return (rawValue, ctx) => {
45522
- if (!Array.isArray(rawValue)) {
45523
- return ctx.failure("array", rawValue);
45524
- }
45525
- if (rawValue.length !== 3) {
45526
- return ctx.failure("array of length 3", rawValue);
45527
- }
45528
- const resultA = parseA(rawValue[0], ctx.forSubtree(0));
45529
- if (isFailure(resultA)) {
45530
- return resultA;
45531
- }
45532
- const resultB = parseB(rawValue[1], ctx.forSubtree(1));
45533
- if (isFailure(resultB)) {
45534
- return resultB;
45535
- }
45536
- const resultC = parseC(rawValue[2], ctx.forSubtree(2));
45537
- if (isFailure(resultC)) {
45538
- return resultC;
45539
- }
45540
- return ctx.success([resultA.value, resultB.value, resultC.value]);
45541
- };
45542
- }
45543
-
45544
- function union(parseBranch) {
45545
- return new UnionBuilder(parseBranch);
45546
- }
45547
- class UnionBuilder {
45548
- constructor(parser) {
45549
- this.parser = parser;
45550
- }
45551
- or(newBranch) {
45552
- return new UnionBuilder(either(this.parser, newBranch));
45553
- }
45554
- }
45555
- function either(parseA, parseB) {
45556
- return (rawValue, ctx) => {
45557
- const resultA = parseA(rawValue, ctx);
45558
- if (isSuccess(resultA)) {
45559
- return resultA;
45560
- }
45561
- return parseB(rawValue, ctx);
45562
- };
45563
- }
45564
-
45565
- function defaulted(parser, fallback) {
45566
- return (rawValue, ctx) => {
45567
- if (rawValue == null) {
45568
- return success(fallback(rawValue));
45569
- }
45570
- return parser(rawValue, ctx);
45571
- };
45572
- }
45573
-
45574
- const parseImages = defaulted(record(string, object({
45575
- width: number,
45576
- height: number
45577
- })), () => ({}));
45578
-
45579
- function parseWidget(parseType, parseOptions) {
45580
- return object({
45581
- type: parseType,
45582
- static: optional(boolean),
45583
- graded: optional(boolean),
45584
- alignment: optional(string),
45585
- options: parseOptions,
45586
- key: optional(number),
45587
- version: optional(object({
45588
- major: number,
45589
- minor: number
45590
- }))
45591
- });
45592
- }
45593
- function parseWidgetWithVersion(parseVersion, parseType, parseOptions) {
45594
- return object({
45595
- type: parseType,
45596
- static: optional(boolean),
45597
- graded: optional(boolean),
45598
- alignment: optional(string),
45599
- options: parseOptions,
45600
- key: optional(number),
45601
- version: parseVersion
45602
- });
45603
- }
45604
-
45605
- const parseCategorizerWidget = parseWidget(constant("categorizer"), object({
45606
- items: array(string),
45607
- categories: array(string),
45608
- randomizeItems: defaulted(boolean, () => false),
45609
- static: defaulted(boolean, () => false),
45610
- values: array(defaulted(number, () => 0)),
45611
- highlightLint: optional(boolean),
45612
- linterContext: optional(object({
45613
- contentType: string,
45614
- paths: array(string),
45615
- stack: array(string)
45616
- }))
45617
- }));
45618
-
45619
- const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
45620
- programID: string,
45621
- programType: any,
45622
- settings: array(object({
45623
- name: string,
45624
- value: string
45625
- })),
45626
- showEditor: boolean,
45627
- showButtons: boolean,
45628
- height: number,
45629
- static: defaulted(boolean, () => false)
45630
- }));
45631
-
45632
- const parseDefinitionWidget = parseWidget(constant("definition"), object({
45633
- togglePrompt: string,
45634
- definition: string,
45635
- static: defaulted(boolean, () => false)
45636
- }));
45637
-
45638
- const parseDropdownWidget = parseWidget(constant("dropdown"), object({
45639
- placeholder: defaulted(string, () => ""),
45640
- ariaLabel: optional(string),
45641
- visibleLabel: optional(string),
45642
- static: defaulted(boolean, () => false),
45643
- choices: array(object({
45644
- content: string,
45645
- correct: boolean
45646
- }))
45647
- }));
45648
-
45649
- const parseExplanationWidget = parseWidget(constant("explanation"), object({
45650
- showPrompt: string,
45651
- hidePrompt: string,
45652
- explanation: string,
45653
- // We wrap parseWidgetsMap in a function here to make sure it is not
45654
- // referenced before it is defined. There is an import cycle between
45655
- // this file and widgets-map.ts that could cause it to be undefined.
45656
- widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
45657
- static: defaulted(boolean, () => false)
45658
- }));
45659
-
45660
- // Given a function, creates a PartialParser that converts one type to another
45661
- // using that function. The returned parser never fails.
45662
- function convert(f) {
45663
- return (rawValue, ctx) => ctx.success(f(rawValue));
45664
- }
45665
-
45666
- /**
45667
- * Creates a parser for a widget options type with multiple major versions. Old
45668
- * versions are migrated to the latest version. The parse fails if the input
45669
- * data does not match any of the versions.
45670
- *
45671
- * @example
45672
- * const parseOptions = versionedWidgetOptions(3, parseOptionsV3)
45673
- * .withMigrationFrom(2, parseOptionsV2, migrateV2ToV3)
45674
- * .withMigrationFrom(1, parseOptionsV1, migrateV1ToV2)
45675
- * .withMigrationFrom(0, parseOptionsV0, migrateV0ToV1)
45676
- * .parser;
45677
- *
45678
- * @param latestMajorVersion the latest major version of the widget options.
45679
- * @param parseLatest a {@link Parser} for the latest version of the widget
45680
- * options.
45681
- * @returns a builder object, to which migrations from earlier versions can be
45682
- * added. Migrations must be added in "reverse chronological" order as in the
45683
- * example above.
45684
- */
45685
- function versionedWidgetOptions(latestMajorVersion, parseLatest) {
45686
- return new VersionedWidgetOptionsParserBuilder(latestMajorVersion, parseLatest, latest => latest, (raw, ctx) => ctx.failure("widget options with a known version number", raw));
45687
- }
45688
- class VersionedWidgetOptionsParserBuilder {
45689
- parser;
45690
- constructor(majorVersion, parseThisVersion, migrateToLatest, parseOtherVersions) {
45691
- this.migrateToLatest = migrateToLatest;
45692
- this.parseOtherVersions = parseOtherVersions;
45693
- const parseThisVersionAndMigrateToLatest = pipeParsers(parseThisVersion).then(convert(this.migrateToLatest)).parser;
45694
- this.parser = (raw, ctx) => {
45695
- if (!isObject(raw)) {
45696
- return ctx.failure("object", raw);
45697
- }
45698
- const versionParseResult = parseVersionedObject(raw, ctx);
45699
- if (isFailure(versionParseResult)) {
45700
- return versionParseResult;
45701
- }
45702
- if (versionParseResult.value.version.major !== majorVersion) {
45703
- return this.parseOtherVersions(raw, ctx);
45704
- }
45705
- return parseThisVersionAndMigrateToLatest(raw, ctx);
45706
- };
45707
- }
45708
-
45709
- /**
45710
- * Add a migration from an old version of the widget options.
45711
- */
45712
- withMigrationFrom(majorVersion, parseOldVersion, migrateToNextVersion) {
45713
- const parseOtherVersions = this.parser;
45714
- const migrateToLatest = old => this.migrateToLatest(migrateToNextVersion(old));
45715
- return new VersionedWidgetOptionsParserBuilder(majorVersion, parseOldVersion, migrateToLatest, parseOtherVersions);
45716
- }
45717
- }
45718
- const parseVersionedObject = object({
45719
- version: defaulted(object({
45720
- major: number,
45721
- minor: number
45722
- }), () => ({
45723
- major: 0,
45724
- minor: 0
45725
- }))
45726
- });
45727
-
45728
- const stringOrNumberOrNullOrUndefined = union(string).or(number).or(constant(null)).or(constant(undefined)).parser;
45729
- const parsePossiblyInvalidAnswerForm = object({
45730
- // `value` is the possibly invalid part of this. It should always be a
45731
- // string, but some answer forms don't have it. The Expression widget
45732
- // ignores invalid values, so we can safely filter them out during parsing.
45733
- value: optional(string),
45734
- form: defaulted(boolean, () => false),
45735
- simplify: defaulted(boolean, () => false),
45736
- considered: enumeration("correct", "wrong", "ungraded"),
45737
- key: pipeParsers(stringOrNumberOrNullOrUndefined).then(convert(String)).parser
45738
- });
45739
- function removeInvalidAnswerForms(possiblyInvalid) {
45740
- const valid = [];
45741
- for (const answerForm of possiblyInvalid) {
45742
- const {
45743
- value
45744
- } = answerForm;
45745
- if (value != null) {
45746
- // Copying the object seems to be needed to make TypeScript happy
45747
- valid.push({
45748
- ...answerForm,
45749
- value
45750
- });
45751
- }
45752
- }
45753
- return valid;
45754
- }
45755
-
45756
- // NOTE(benchristel): I copied the default buttonSets from
45757
- // expression.tsx. See the parse-perseus-json/README.md for
45758
- // an explanation of why we want to duplicate the default here.
45759
- const parseButtonSets = defaulted(array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")), () => ["basic", "trig", "prealgebra", "logarithms"]);
45760
- const version1 = object({
45761
- major: constant(1),
45762
- minor: number
45763
- });
45764
- const parseExpressionWidgetV1 = parseWidgetWithVersion(version1, constant("expression"), object({
45765
- answerForms: pipeParsers(array(parsePossiblyInvalidAnswerForm)).then(convert(removeInvalidAnswerForms)).parser,
45766
- functions: array(string),
45767
- times: boolean,
45768
- visibleLabel: optional(string),
45769
- ariaLabel: optional(string),
45770
- buttonSets: parseButtonSets,
45771
- buttonsVisible: optional(enumeration("always", "never", "focused"))
45772
- }));
45773
- const version0 = optional(object({
45774
- major: constant(0),
45775
- minor: number
45776
- }));
45777
- const parseExpressionWidgetV0 = parseWidgetWithVersion(version0, constant("expression"), object({
45778
- functions: array(string),
45779
- times: boolean,
45780
- visibleLabel: optional(string),
45781
- ariaLabel: optional(string),
45782
- form: boolean,
45783
- simplify: boolean,
45784
- value: string,
45785
- buttonSets: parseButtonSets,
45786
- buttonsVisible: optional(enumeration("always", "never", "focused"))
45787
- }));
45788
- function migrateV0ToV1(widget) {
45789
- const {
45790
- options
45791
- } = widget;
45792
- return {
45793
- ...widget,
45794
- version: {
45795
- major: 1,
45796
- minor: 0
45797
- },
45798
- options: {
45799
- times: options.times,
45800
- buttonSets: options.buttonSets,
45801
- functions: options.functions,
45802
- buttonsVisible: options.buttonsVisible,
45803
- visibleLabel: options.visibleLabel,
45804
- ariaLabel: options.ariaLabel,
45805
- answerForms: [{
45806
- considered: "correct",
45807
- form: options.form,
45808
- simplify: options.simplify,
45809
- value: options.value
45810
- }]
45811
- }
45812
- };
45813
- }
45814
- const parseExpressionWidget = versionedWidgetOptions(1, parseExpressionWidgetV1).withMigrationFrom(0, parseExpressionWidgetV0, migrateV0ToV1).parser;
45815
-
45816
- const falseToNull = pipeParsers(constant(false)).then(convert(() => null)).parser;
45817
- const parseGradedGroupWidgetOptions = object({
45818
- title: defaulted(string, () => ""),
45819
- hasHint: optional(nullable(boolean)),
45820
- // This module has an import cycle with parsePerseusRenderer.
45821
- // The anonymous function below ensures that we don't try to access
45822
- // parsePerseusRenderer before it's defined.
45823
- hint: union(falseToNull).or(constant(null)).or(constant(undefined)).or((rawVal, ctx) => parsePerseusRenderer(rawVal, ctx)).parser,
45824
- content: string,
45825
- // This module has an import cycle with parseWidgetsMap.
45826
- // The anonymous function below ensures that we don't try to access
45827
- // parseWidgetsMap before it's defined.
45828
- widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
45829
- widgetEnabled: optional(nullable(boolean)),
45830
- immutableWidgets: optional(nullable(boolean)),
45831
- images: record(string, object({
45832
- width: number,
45833
- height: number
45834
- }))
45835
- });
45836
- const parseGradedGroupWidget = parseWidget(constant("graded-group"), parseGradedGroupWidgetOptions);
45837
-
45838
- const parseGradedGroupSetWidget = parseWidget(constant("graded-group-set"), object({
45839
- gradedGroups: array(parseGradedGroupWidgetOptions)
45840
- }));
45841
-
45842
- /**
45843
- * discriminatedUnion() should be preferred over union() when parsing a
45844
- * discriminated union type, because discriminatedUnion() produces more
45845
- * understandable failure messages. It takes the discriminant as the source of
45846
- * truth for which variant is to be parsed, and expects the other data to match
45847
- * that variant.
45848
- */
45849
- function discriminatedUnionOn(discriminantKey) {
45850
- const noMoreBranches = (raw, ctx) => {
45851
- if (!isObject(raw)) {
45852
- return ctx.failure("object", raw);
45853
- }
45854
- return ctx.forSubtree(discriminantKey).failure("a valid value", raw[discriminantKey]);
45855
- };
45856
- return new DiscriminatedUnionBuilder(discriminantKey, noMoreBranches);
45857
- }
45858
- class DiscriminatedUnionBuilder {
45859
- constructor(discriminantKey, parser) {
45860
- this.discriminantKey = discriminantKey;
45861
- this.parser = parser;
45862
- }
45863
- withBranch(discriminantValue, parseNewVariant) {
45864
- const parseNewBranch = discriminatedUnionBranch(this.discriminantKey, discriminantValue, parseNewVariant, this.parser);
45865
- return new DiscriminatedUnionBuilder(this.discriminantKey, parseNewBranch);
45866
- }
45867
- }
45868
- function discriminatedUnionBranch(discriminantKey, discriminantValue, parseVariant, parseOtherBranches) {
45869
- return (raw, ctx) => {
45870
- if (!isObject(raw)) {
45871
- return ctx.failure("object", raw);
45872
- }
45873
- if (raw[discriminantKey] === discriminantValue) {
45874
- return parseVariant(raw, ctx);
45875
- }
45876
- return parseOtherBranches(raw, ctx);
45877
- };
45878
- }
45879
-
45880
- const pairOfNumbers$3 = pair(number, number);
45881
- const pairOfPoints = pair(pairOfNumbers$3, pairOfNumbers$3);
45882
- const parseGrapherWidget = parseWidget(constant("grapher"), object({
45883
- availableTypes: array(enumeration("absolute_value", "exponential", "linear", "logarithm", "quadratic", "sinusoid", "tangent")),
45884
- correct: discriminatedUnionOn("type").withBranch("absolute_value", object({
45885
- type: constant("absolute_value"),
45886
- coords: nullable(pairOfPoints)
45887
- })).withBranch("exponential", object({
45888
- type: constant("exponential"),
45889
- asymptote: pairOfPoints,
45890
- coords: nullable(pairOfPoints)
45891
- })).withBranch("linear", object({
45892
- type: constant("linear"),
45893
- coords: nullable(pairOfPoints)
45894
- })).withBranch("logarithm", object({
45895
- type: constant("logarithm"),
45896
- asymptote: pairOfPoints,
45897
- coords: nullable(pairOfPoints)
45898
- })).withBranch("quadratic", object({
45899
- type: constant("quadratic"),
45900
- coords: nullable(pairOfPoints)
45901
- })).withBranch("sinusoid", object({
45902
- type: constant("sinusoid"),
45903
- coords: nullable(pairOfPoints)
45904
- })).withBranch("tangent", object({
45905
- type: constant("tangent"),
45906
- coords: nullable(pairOfPoints)
45907
- })).parser,
45908
- graph: object({
45909
- backgroundImage: object({
45910
- bottom: optional(number),
45911
- height: optional(number),
45912
- left: optional(number),
45913
- scale: optional(number),
45914
- url: optional(nullable(string)),
45915
- width: optional(number)
45916
- }),
45917
- box: optional(pairOfNumbers$3),
45918
- editableSettings: optional(array(enumeration("graph", "snap", "image", "measure"))),
45919
- gridStep: optional(pairOfNumbers$3),
45920
- labels: pair(string, string),
45921
- markings: enumeration("graph", "none", "grid"),
45922
- range: pair(pairOfNumbers$3, pairOfNumbers$3),
45923
- rulerLabel: constant(""),
45924
- rulerTicks: number,
45925
- showProtractor: optional(boolean),
45926
- showRuler: optional(boolean),
45927
- showTooltips: optional(boolean),
45928
- snapStep: optional(pairOfNumbers$3),
45929
- step: pairOfNumbers$3,
45930
- valid: optional(union(boolean).or(string).parser)
45931
- })
45932
- }));
45933
-
45934
- const parseGroupWidget = parseWidget(constant("group"),
45935
- // This module has an import cycle with parsePerseusRenderer.
45936
- // The anonymous function below ensures that we don't try to access
45937
- // parsePerseusRenderer before it's defined.
45938
- (rawVal, ctx) => parsePerseusRenderer(rawVal, ctx));
45939
-
45940
- const parseIframeWidget = parseWidget(constant("iframe"), object({
45941
- url: string,
45942
- settings: optional(array(object({
45943
- name: string,
45944
- value: string
45945
- }))),
45946
- width: union(number).or(string).parser,
45947
- height: union(number).or(string).parser,
45948
- allowFullScreen: defaulted(boolean, () => false),
45949
- allowTopNavigation: optional(boolean),
45950
- static: defaulted(boolean, () => false)
45951
- }));
45952
-
45953
- const stringToNumber = (rawValue, ctx) => {
45954
- if (typeof rawValue === "number") {
45955
- return ctx.success(rawValue);
45956
- }
45957
- const parsedNumber = +rawValue;
45958
- if (rawValue === "" || isNaN(parsedNumber)) {
45959
- return ctx.failure("a number or numeric string", rawValue);
45960
- }
45961
- return ctx.success(parsedNumber);
45962
- };
45963
-
45964
- function emptyToZero(x) {
45965
- return x === "" ? 0 : x;
45966
- }
45967
- const imageDimensionToNumber = pipeParsers(union(number).or(string).parser)
45968
- // In this specific case, empty string is equivalent to zero. An empty
45969
- // string parses to either NaN (using parseInt) or 0 (using unary +) and
45970
- // CSS will treat NaN as invalid and default to 0 instead.
45971
- .then(convert(emptyToZero)).then(stringToNumber).parser;
45972
- const parsePerseusImageBackground = object({
45973
- url: optional(nullable(string)),
45974
- width: optional(imageDimensionToNumber),
45975
- height: optional(imageDimensionToNumber),
45976
- top: optional(imageDimensionToNumber),
45977
- left: optional(imageDimensionToNumber),
45978
- bottom: optional(imageDimensionToNumber),
45979
- scale: optional(imageDimensionToNumber)
45980
- });
45981
-
45982
- const pairOfNumbers$2 = pair(number, number);
45983
- const parseImageWidget = parseWidget(constant("image"), object({
45984
- title: optional(string),
45985
- caption: optional(string),
45986
- alt: optional(string),
45987
- backgroundImage: parsePerseusImageBackground,
45988
- static: optional(boolean),
45989
- labels: optional(array(object({
45990
- content: string,
45991
- alignment: string,
45992
- coordinates: array(number)
45993
- }))),
45994
- range: optional(pair(pairOfNumbers$2, pairOfNumbers$2)),
45995
- box: optional(pairOfNumbers$2)
45996
- }));
45997
-
45998
- const booleanToString = (rawValue, ctx) => {
45999
- if (typeof rawValue === "boolean") {
46000
- return ctx.success(String(rawValue));
46001
- }
46002
- return ctx.failure("boolean", rawValue);
46003
- };
46004
- const parseInputNumberWidget = parseWidget(constant("input-number"), object({
46005
- answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
46006
- inexact: optional(boolean),
46007
- maxError: optional(union(number).or(string).parser),
46008
- rightAlign: optional(boolean),
46009
- simplify: enumeration("required", "optional", "enforced"),
46010
- size: enumeration("normal", "small"),
46011
- // TODO(benchristel): there are some content items where value is a
46012
- // boolean, even though that makes no sense. We should figure out if
46013
- // those content items are actually published anywhere, and consider
46014
- // updating them.
46015
- value: union(number).or(string).or(booleanToString).parser,
46016
- customKeypad: optional(boolean)
46017
- }));
46018
-
46019
- const pairOfNumbers$1 = pair(number, number);
46020
- const stringOrEmpty = defaulted(string, () => "");
46021
- const parseKey = pipeParsers(optional(string)).then(convert(String)).parser;
46022
- const parseFunctionElement = object({
46023
- type: constant("function"),
46024
- key: parseKey,
46025
- options: object({
46026
- value: string,
46027
- funcName: string,
46028
- rangeMin: string,
46029
- rangeMax: string,
46030
- color: string,
46031
- strokeDasharray: string,
46032
- strokeWidth: number
46033
- })
46034
- });
46035
- const parseLabelElement = object({
46036
- type: constant("label"),
46037
- key: parseKey,
46038
- options: object({
46039
- label: string,
46040
- color: string,
46041
- coordX: string,
46042
- coordY: string
46043
- })
46044
- });
46045
- const parseLineElement = object({
46046
- type: constant("line"),
46047
- key: parseKey,
46048
- options: object({
46049
- color: string,
46050
- startX: string,
46051
- startY: string,
46052
- endX: string,
46053
- endY: string,
46054
- strokeDasharray: string,
46055
- strokeWidth: number,
46056
- arrows: string
46057
- })
46058
- });
46059
- const parseMovableLineElement = object({
46060
- type: constant("movable-line"),
46061
- key: parseKey,
46062
- options: object({
46063
- startX: string,
46064
- startY: string,
46065
- startSubscript: number,
46066
- endX: string,
46067
- endY: string,
46068
- endSubscript: number,
46069
- constraint: string,
46070
- snap: number,
46071
- constraintFn: string,
46072
- constraintXMin: string,
46073
- constraintXMax: string,
46074
- constraintYMin: string,
46075
- constraintYMax: string
46076
- })
46077
- });
46078
- const parseMovablePointElement = object({
46079
- type: constant("movable-point"),
46080
- key: parseKey,
46081
- options: object({
46082
- startX: string,
46083
- startY: string,
46084
- varSubscript: number,
46085
- constraint: string,
46086
- snap: number,
46087
- constraintFn: string,
46088
- constraintXMin: stringOrEmpty,
46089
- constraintXMax: stringOrEmpty,
46090
- constraintYMin: stringOrEmpty,
46091
- constraintYMax: stringOrEmpty
46092
- })
46093
- });
46094
- const parseParametricElement = object({
46095
- type: constant("parametric"),
46096
- key: parseKey,
46097
- options: object({
46098
- x: string,
46099
- y: string,
46100
- rangeMin: string,
46101
- rangeMax: string,
46102
- color: string,
46103
- strokeDasharray: string,
46104
- strokeWidth: number
46105
- })
46106
- });
46107
- const parsePointElement = object({
46108
- type: constant("point"),
46109
- key: parseKey,
46110
- options: object({
46111
- color: string,
46112
- coordX: string,
46113
- coordY: string
46114
- })
46115
- });
46116
- const parseRectangleElement = object({
46117
- type: constant("rectangle"),
46118
- key: parseKey,
46119
- options: object({
46120
- color: string,
46121
- coordX: string,
46122
- coordY: string,
46123
- width: string,
46124
- height: string
46125
- })
46126
- });
46127
- const parseInteractionWidget = parseWidget(constant("interaction"), object({
46128
- static: defaulted(boolean, () => false),
46129
- graph: object({
46130
- editableSettings: optional(array(enumeration("canvas", "graph"))),
46131
- box: pairOfNumbers$1,
46132
- labels: array(string),
46133
- range: pair(pairOfNumbers$1, pairOfNumbers$1),
46134
- gridStep: pairOfNumbers$1,
46135
- markings: enumeration("graph", "grid", "none"),
46136
- snapStep: optional(pairOfNumbers$1),
46137
- valid: optional(union(boolean).or(string).parser),
46138
- backgroundImage: optional(parsePerseusImageBackground),
46139
- showProtractor: optional(boolean),
46140
- showRuler: optional(boolean),
46141
- rulerLabel: optional(string),
46142
- rulerTicks: optional(number),
46143
- tickStep: pairOfNumbers$1
46144
- }),
46145
- elements: array(discriminatedUnionOn("type").withBranch("function", parseFunctionElement).withBranch("label", parseLabelElement).withBranch("line", parseLineElement).withBranch("movable-line", parseMovableLineElement).withBranch("movable-point", parseMovablePointElement).withBranch("parametric", parseParametricElement).withBranch("point", parsePointElement).withBranch("rectangle", parseRectangleElement).parser)
46146
- }));
46147
-
46148
- // Used to represent 2-D points and ranges
46149
- const pairOfNumbers = pair(number, number);
46150
- const parsePerseusGraphTypeAngle = object({
46151
- type: constant("angle"),
46152
- showAngles: optional(boolean),
46153
- allowReflexAngles: optional(boolean),
46154
- angleOffsetDeg: optional(number),
46155
- snapDegrees: optional(number),
46156
- match: optional(constant("congruent")),
46157
- coords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
46158
- startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))
46159
- });
46160
- const parsePerseusGraphTypeCircle = object({
46161
- type: constant("circle"),
46162
- center: optional(pairOfNumbers),
46163
- radius: optional(number),
46164
- startCoords: optional(object({
46165
- center: pairOfNumbers,
46166
- radius: number
46167
- })),
46168
- // TODO: remove coord? it's legacy.
46169
- coord: optional(pairOfNumbers)
46170
- });
46171
- const parsePerseusGraphTypeLinear = object({
46172
- type: constant("linear"),
46173
- coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
46174
- startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
46175
- // TODO: remove coord? it's legacy.
46176
- coord: optional(pairOfNumbers)
46177
- });
46178
- const parsePerseusGraphTypeLinearSystem = object({
46179
- type: constant("linear-system"),
46180
- // TODO(benchristel): default coords to empty array?
46181
- coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
46182
- startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
46183
- // TODO: remove coord? it's legacy.
46184
- coord: optional(pairOfNumbers)
46185
- });
46186
- const parsePerseusGraphTypeNone = object({
46187
- type: constant("none")
46188
- });
46189
- const parsePerseusGraphTypePoint = object({
46190
- type: constant("point"),
46191
- numPoints: optional(union(number).or(constant("unlimited")).parser),
46192
- coords: optional(nullable(array(pairOfNumbers))),
46193
- startCoords: optional(array(pairOfNumbers)),
46194
- // TODO: remove coord? it's legacy.
46195
- coord: optional(pairOfNumbers)
46196
- });
46197
- const parsePerseusGraphTypePolygon = object({
46198
- type: constant("polygon"),
46199
- numSides: optional(union(number).or(constant("unlimited")).parser),
46200
- showAngles: optional(boolean),
46201
- showSides: optional(boolean),
46202
- snapTo: optional(enumeration("grid", "angles", "sides")),
46203
- match: optional(enumeration("similar", "congruent", "approx", "exact")),
46204
- startCoords: optional(array(pairOfNumbers)),
46205
- // TODO: remove coord? it's legacy.
46206
- coord: optional(pairOfNumbers)
46207
- });
46208
- const parsePerseusGraphTypeQuadratic = object({
46209
- type: constant("quadratic"),
46210
- coords: optional(nullable(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))),
46211
- startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
46212
- // TODO: remove coord? it's legacy.
46213
- coord: optional(pairOfNumbers)
46214
- });
46215
- const parsePerseusGraphTypeRay = object({
46216
- type: constant("ray"),
46217
- coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
46218
- startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
46219
- // TODO: remove coord? it's legacy.
46220
- coord: optional(pairOfNumbers)
46221
- });
46222
- const parsePerseusGraphTypeSegment = object({
46223
- type: constant("segment"),
46224
- // TODO(benchristel): default numSegments?
46225
- numSegments: optional(number),
46226
- coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
46227
- startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
46228
- // TODO: remove coord? it's legacy.
46229
- coord: optional(pairOfNumbers)
46230
- });
46231
- const parsePerseusGraphTypeSinusoid = object({
46232
- type: constant("sinusoid"),
46233
- coords: optional(nullable(array(pairOfNumbers))),
46234
- startCoords: optional(array(pairOfNumbers)),
46235
- // TODO: remove coord? it's legacy.
46236
- coord: optional(pairOfNumbers)
46237
- });
46238
- const parsePerseusGraphType = discriminatedUnionOn("type").withBranch("angle", parsePerseusGraphTypeAngle).withBranch("circle", parsePerseusGraphTypeCircle).withBranch("linear", parsePerseusGraphTypeLinear).withBranch("linear-system", parsePerseusGraphTypeLinearSystem).withBranch("none", parsePerseusGraphTypeNone).withBranch("point", parsePerseusGraphTypePoint).withBranch("polygon", parsePerseusGraphTypePolygon).withBranch("quadratic", parsePerseusGraphTypeQuadratic).withBranch("ray", parsePerseusGraphTypeRay).withBranch("segment", parsePerseusGraphTypeSegment).withBranch("sinusoid", parsePerseusGraphTypeSinusoid).parser;
46239
- const parseLockedFigureColor = enumeration(...perseusCore.lockedFigureColorNames);
46240
- const parseLockedFigureFillType = enumeration("none", "white", "translucent", "solid");
46241
- const parseLockedLineStyle = enumeration("solid", "dashed");
46242
- const parseLockedLabelType = object({
46243
- type: constant("label"),
46244
- coord: pairOfNumbers,
46245
- text: string,
46246
- color: parseLockedFigureColor,
46247
- size: enumeration("small", "medium", "large")
46248
- });
46249
- const parseLockedPointType = object({
46250
- type: constant("point"),
46251
- coord: pairOfNumbers,
46252
- color: parseLockedFigureColor,
46253
- filled: boolean,
46254
- // TODO(benchristel): default labels to empty array?
46255
- labels: optional(array(parseLockedLabelType)),
46256
- ariaLabel: optional(string)
46257
- });
46258
- const parseLockedLineType = object({
46259
- type: constant("line"),
46260
- kind: enumeration("line", "ray", "segment"),
46261
- points: pair(parseLockedPointType, parseLockedPointType),
46262
- color: parseLockedFigureColor,
46263
- lineStyle: parseLockedLineStyle,
46264
- showPoint1: defaulted(boolean, () => false),
46265
- showPoint2: defaulted(boolean, () => false),
46266
- // TODO(benchristel): default labels to empty array?
46267
- labels: optional(array(parseLockedLabelType)),
46268
- ariaLabel: optional(string)
46269
- });
46270
- const parseLockedVectorType = object({
46271
- type: constant("vector"),
46272
- points: pair(pairOfNumbers, pairOfNumbers),
46273
- color: parseLockedFigureColor,
46274
- // TODO(benchristel): default labels to empty array?
46275
- labels: optional(array(parseLockedLabelType)),
46276
- ariaLabel: optional(string)
46277
- });
46278
- const parseLockedEllipseType = object({
46279
- type: constant("ellipse"),
46280
- center: pairOfNumbers,
46281
- radius: pairOfNumbers,
46282
- angle: number,
46283
- color: parseLockedFigureColor,
46284
- fillStyle: parseLockedFigureFillType,
46285
- strokeStyle: parseLockedLineStyle,
46286
- // TODO(benchristel): default labels to empty array?
46287
- labels: optional(array(parseLockedLabelType)),
46288
- ariaLabel: optional(string)
46289
- });
46290
- const parseLockedPolygonType = object({
46291
- type: constant("polygon"),
46292
- points: array(pairOfNumbers),
46293
- color: parseLockedFigureColor,
46294
- showVertices: boolean,
46295
- fillStyle: parseLockedFigureFillType,
46296
- strokeStyle: parseLockedLineStyle,
46297
- // TODO(benchristel): default labels to empty array?
46298
- labels: optional(array(parseLockedLabelType)),
46299
- ariaLabel: optional(string)
46300
- });
46301
- const parseLockedFunctionType = object({
46302
- type: constant("function"),
46303
- color: parseLockedFigureColor,
46304
- strokeStyle: parseLockedLineStyle,
46305
- equation: string,
46306
- directionalAxis: enumeration("x", "y"),
46307
- domain: optional(pairOfNumbers),
46308
- // TODO(benchristel): default labels to empty array?
46309
- labels: optional(array(parseLockedLabelType)),
46310
- ariaLabel: optional(string)
46311
- });
46312
- const parseLockedFigure = discriminatedUnionOn("type").withBranch("point", parseLockedPointType).withBranch("line", parseLockedLineType).withBranch("vector", parseLockedVectorType).withBranch("ellipse", parseLockedEllipseType).withBranch("polygon", parseLockedPolygonType).withBranch("function", parseLockedFunctionType).withBranch("label", parseLockedLabelType).parser;
46313
- const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
46314
- step: pairOfNumbers,
46315
- // TODO(benchristel): rather than making gridStep and snapStep
46316
- // optional, we should duplicate the defaulting logic from the
46317
- // InteractiveGraph component. See parse-perseus-json/README.md for
46318
- // why.
46319
- gridStep: optional(pairOfNumbers),
46320
- snapStep: optional(pairOfNumbers),
46321
- backgroundImage: optional(parsePerseusImageBackground),
46322
- markings: enumeration("graph", "grid", "none"),
46323
- labels: optional(array(string)),
46324
- showProtractor: boolean,
46325
- showRuler: optional(boolean),
46326
- showTooltips: optional(boolean),
46327
- rulerLabel: optional(string),
46328
- rulerTicks: optional(number),
46329
- range: pair(pairOfNumbers, pairOfNumbers),
46330
- // NOTE(benchristel): I copied the default graph from
46331
- // interactive-graph.tsx. See the parse-perseus-json/README.md for
46332
- // an explanation of why we want to duplicate the default here.
46333
- graph: defaulted(parsePerseusGraphType, () => ({
46334
- type: "linear"
46335
- })),
46336
- correct: parsePerseusGraphType,
46337
- // TODO(benchristel): default lockedFigures to empty array
46338
- lockedFigures: optional(array(parseLockedFigure)),
46339
- fullGraphLabel: optional(string),
46340
- fullGraphAriaDescription: optional(string)
46341
- }));
46342
-
46343
- const parseLabelImageWidget = parseWidget(constant("label-image"), object({
46344
- choices: array(string),
46345
- imageUrl: string,
46346
- imageAlt: string,
46347
- imageHeight: number,
46348
- imageWidth: number,
46349
- markers: array(object({
46350
- answers: array(string),
46351
- label: string,
46352
- x: number,
46353
- y: number
46354
- })),
46355
- hideChoicesFromInstructions: boolean,
46356
- multipleAnswers: boolean,
46357
- static: defaulted(boolean, () => false)
46358
- }));
46359
-
46360
- const parseMatcherWidget = parseWidget(constant("matcher"), object({
46361
- labels: array(string),
46362
- left: array(string),
46363
- right: array(string),
46364
- orderMatters: boolean,
46365
- padding: boolean
46366
- }));
46367
-
46368
- const numberOrString = union(number).or(string).parser;
46369
- const numeric = pipeParsers(defaulted(numberOrString, () => NaN)).then(stringToNumber).parser;
46370
- const parseMatrixWidget = parseWidget(defaulted(constant("matrix"), () => "matrix"), object({
46371
- prefix: optional(string),
46372
- suffix: optional(string),
46373
- answers: array(array(numeric)),
46374
- cursorPosition: optional(array(number)),
46375
- matrixBoardSize: array(number),
46376
- static: optional(boolean)
46377
- }));
46378
-
46379
- const parseMeasurerWidget = parseWidget(constant("measurer"), object({
46380
- // The default value for image comes from measurer.tsx.
46381
- // See parse-perseus-json/README.md for why we want to duplicate the
46382
- // defaults here.
46383
- image: defaulted(parsePerseusImageBackground, () => ({
46384
- url: null,
46385
- top: 0,
46386
- left: 0
46387
- })),
46388
- showProtractor: boolean,
46389
- showRuler: boolean,
46390
- rulerLabel: string,
46391
- rulerTicks: number,
46392
- rulerPixels: number,
46393
- rulerLength: number,
46394
- box: pair(number, number),
46395
- // TODO(benchristel): static is not used. Remove it?
46396
- static: defaulted(boolean, () => false)
46397
- }));
46398
-
46399
- const parseMoleculeRendererWidget = parseWidget(constant("molecule-renderer"), object({
46400
- widgetId: string,
46401
- rotationAngle: optional(number),
46402
- smiles: optional(string)
46403
- }));
46404
-
46405
- const emptyStringToNull = pipeParsers(constant("")).then(convert(() => null)).parser;
46406
- const parseNumberLineWidget = parseWidget(constant("number-line"), object({
46407
- range: array(number),
46408
- labelRange: array(nullable(union(number).or(emptyStringToNull).parser)),
46409
- labelStyle: string,
46410
- labelTicks: boolean,
46411
- isTickCtrl: optional(nullable(boolean)),
46412
- divisionRange: array(number),
46413
- numDivisions: optional(nullable(number)),
46414
- // NOTE(benchristel): I copied the default snapDivisions from
46415
- // number-line.tsx. See the parse-perseus-json/README.md for
46416
- // an explanation of why we want to duplicate the default here.
46417
- snapDivisions: defaulted(number, () => 2),
46418
- tickStep: optional(nullable(number)),
46419
- correctRel: optional(nullable(string)),
46420
- correctX: nullable(number),
46421
- initialX: optional(nullable(number)),
46422
- showTooltip: optional(boolean),
46423
- static: defaulted(boolean, () => false)
46424
- }));
46425
-
46426
- const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "decimal", "percent", "pi");
46427
- const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
46428
- answers: array(object({
46429
- message: string,
46430
- // TODO(benchristel): value should never be null or undefined,
46431
- // but we have some content where it is anyway. If we backfill
46432
- // the data, simplify this.
46433
- value: optional(nullable(number)),
46434
- status: string,
46435
- answerForms: optional(array(parseMathFormat)),
46436
- strict: boolean,
46437
- maxError: optional(nullable(number)),
46438
- // TODO(benchristel): simplify should never be a boolean, but we
46439
- // have some content where it is anyway. If we ever backfill
46440
- // the data, we should simplify `simplify`.
46441
- simplify: optional(nullable(union(string).or(pipeParsers(boolean).then(convert(String)).parser).parser))
46442
- })),
46443
- labelText: optional(string),
46444
- size: string,
46445
- coefficient: defaulted(boolean, () => false),
46446
- rightAlign: optional(boolean),
46447
- static: defaulted(boolean, () => false),
46448
- answerForms: optional(array(object({
46449
- name: parseMathFormat,
46450
- simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
46451
- })))
46452
- }));
46453
-
46454
- // There is an import cycle between orderer-widget.ts and perseus-renderer.ts.
46455
- // This wrapper ensures that we don't refer to parsePerseusRenderer before
46456
- // it's defined.
46457
- function parseRenderer(rawValue, ctx) {
46458
- return parsePerseusRenderer(rawValue, ctx);
46459
- }
46460
- const largeToAuto = (height, ctx) => {
46461
- if (height === "large") {
46462
- return ctx.success("auto");
46463
- }
46464
- return ctx.success(height);
46465
- };
46466
- const parseOrdererWidget = parseWidget(constant("orderer"), object({
46467
- options: defaulted(array(parseRenderer), () => []),
46468
- correctOptions: array(parseRenderer),
46469
- otherOptions: array(parseRenderer),
46470
- height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
46471
- layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
46472
- }));
46473
-
46474
- const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
46475
- passageNumber: number,
46476
- referenceNumber: number,
46477
- summaryText: optional(string)
46478
- }));
46479
-
46480
- const parsePassageWidget = parseWidget(constant("passage"), object({
46481
- footnotes: defaulted(string, () => ""),
46482
- passageText: string,
46483
- passageTitle: defaulted(string, () => ""),
46484
- showLineNumbers: boolean,
46485
- static: defaulted(boolean, () => false)
46486
- }));
46487
-
46488
- const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
46489
- url: string,
46490
- description: string
46491
- }));
46492
-
46493
- const parsePlotterWidget = parseWidget(constant("plotter"), object({
46494
- labels: array(string),
46495
- categories: array(string),
46496
- type: enumeration(...perseusCore.plotterPlotTypes),
46497
- maxY: number,
46498
- // The default value for scaleY comes from plotter.tsx.
46499
- // See parse-perseus-json/README.md for why we want to duplicate the
46500
- // defaults here.
46501
- scaleY: defaulted(number, () => 1),
46502
- labelInterval: optional(nullable(number)),
46503
- // The default value for snapsPerLine comes from plotter.tsx.
46504
- // See parse-perseus-json/README.md for why we want to duplicate the
46505
- // defaults here.
46506
- snapsPerLine: defaulted(number, () => 2),
46507
- starting: array(number),
46508
- correct: array(number),
46509
- picUrl: optional(nullable(string)),
46510
- picSize: optional(nullable(number)),
46511
- picBoxHeight: optional(nullable(number)),
46512
- // NOTE(benchristel): I copied the default plotDimensions from
46513
- // plotter.tsx. See the parse-perseus-json/README.md for an explanation
46514
- // of why we want to duplicate the defaults here.
46515
- plotDimensions: defaulted(array(number), () => [380, 300])
46516
- }));
46517
-
46518
- const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
46519
- programID: string,
46520
- height: number
46521
- }));
46522
-
46523
- const parseRadioWidget = parseWidget(constant("radio"), object({
46524
- choices: array(object({
46525
- content: defaulted(string, () => ""),
46526
- clue: optional(string),
46527
- correct: optional(boolean),
46528
- isNoneOfTheAbove: optional(boolean),
46529
- // deprecated
46530
- // There is an import cycle between radio-widget.ts and
46531
- // widgets-map.ts. The anonymous function below ensures that we
46532
- // don't refer to parseWidgetsMap before it's defined.
46533
- widgets: optional((rawVal, ctx) => parseWidgetsMap(rawVal, ctx))
46534
- })),
46535
- hasNoneOfTheAbove: optional(boolean),
46536
- countChoices: optional(boolean),
46537
- randomize: optional(boolean),
46538
- multipleSelect: optional(boolean),
46539
- deselectEnabled: optional(boolean),
46540
- // deprecated
46541
- onePerLine: optional(boolean),
46542
- // deprecated
46543
- displayCount: optional(any),
46544
- // v0 props
46545
- // `noneOfTheAbove` is still in use (but only set to `false`).
46546
- noneOfTheAbove: optional(constant(false))
46547
- }));
46548
-
46549
- const parseSorterWidget = parseWidget(constant("sorter"), object({
46550
- correct: array(string),
46551
- padding: boolean,
46552
- layout: enumeration("horizontal", "vertical")
46553
- }));
46554
-
46555
- const parseTableWidget = parseWidget(constant("table"), object({
46556
- headers: array(string),
46557
- rows: number,
46558
- columns: number,
46559
- answers: array(array(string))
46560
- }));
46561
-
46562
- const parseVideoWidget = parseWidget(constant("video"), object({
46563
- location: string,
46564
- static: optional(boolean)
46565
- }));
46566
-
46567
- const parseWidgetsMap = (rawValue, ctx) => {
46568
- if (!isObject(rawValue)) {
46569
- return ctx.failure("PerseusWidgetsMap", rawValue);
46570
- }
46571
- const widgetsMap = {};
46572
- for (const key of Object.keys(rawValue)) {
46573
- // parseWidgetsMapEntry modifies the widgetsMap. This is kind of gross,
46574
- // but it's the only way I could find to make TypeScript check the key
46575
- // against the widget type.
46576
- const entryResult = parseWidgetsMapEntry([key, rawValue[key]], widgetsMap, ctx.forSubtree(key));
46577
- if (isFailure(entryResult)) {
46578
- return entryResult;
46579
- }
46580
- }
46581
- return ctx.success(widgetsMap);
46582
- };
46583
- const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
46584
- let [id, widget] = _ref;
46585
- const idComponentsResult = parseWidgetIdComponents(id.split(" "), ctx.forSubtree("(widget ID)"));
46586
- if (isFailure(idComponentsResult)) {
46587
- return idComponentsResult;
46588
- }
46589
- const [type, n] = idComponentsResult.value;
46590
- function parseAndAssign(key, parse) {
46591
- const widgetResult = parse(widget, ctx);
46592
- if (isFailure(widgetResult)) {
46593
- return widgetResult;
46594
- }
46595
- widgetMap[key] = widgetResult.value;
46596
- return ctx.success(undefined);
46597
- }
46598
- switch (type) {
46599
- case "categorizer":
46600
- return parseAndAssign(`categorizer ${n}`, parseCategorizerWidget);
46601
- case "cs-program":
46602
- return parseAndAssign(`cs-program ${n}`, parseCSProgramWidget);
46603
- case "definition":
46604
- return parseAndAssign(`definition ${n}`, parseDefinitionWidget);
46605
- case "dropdown":
46606
- return parseAndAssign(`dropdown ${n}`, parseDropdownWidget);
46607
- case "explanation":
46608
- return parseAndAssign(`explanation ${n}`, parseExplanationWidget);
46609
- case "expression":
46610
- return parseAndAssign(`expression ${n}`, parseExpressionWidget);
46611
- case "grapher":
46612
- return parseAndAssign(`grapher ${n}`, parseGrapherWidget);
46613
- case "group":
46614
- return parseAndAssign(`group ${n}`, parseGroupWidget);
46615
- case "graded-group":
46616
- return parseAndAssign(`graded-group ${n}`, parseGradedGroupWidget);
46617
- case "graded-group-set":
46618
- return parseAndAssign(`graded-group-set ${n}`, parseGradedGroupSetWidget);
46619
- case "iframe":
46620
- return parseAndAssign(`iframe ${n}`, parseIframeWidget);
46621
- case "image":
46622
- return parseAndAssign(`image ${n}`, parseImageWidget);
46623
- case "input-number":
46624
- return parseAndAssign(`input-number ${n}`, parseInputNumberWidget);
46625
- case "interaction":
46626
- return parseAndAssign(`interaction ${n}`, parseInteractionWidget);
46627
- case "interactive-graph":
46628
- return parseAndAssign(`interactive-graph ${n}`, parseInteractiveGraphWidget);
46629
- case "label-image":
46630
- return parseAndAssign(`label-image ${n}`, parseLabelImageWidget);
46631
- case "matcher":
46632
- return parseAndAssign(`matcher ${n}`, parseMatcherWidget);
46633
- case "matrix":
46634
- return parseAndAssign(`matrix ${n}`, parseMatrixWidget);
46635
- case "measurer":
46636
- return parseAndAssign(`measurer ${n}`, parseMeasurerWidget);
46637
- case "molecule-renderer":
46638
- return parseAndAssign(`molecule-renderer ${n}`, parseMoleculeRendererWidget);
46639
- case "number-line":
46640
- return parseAndAssign(`number-line ${n}`, parseNumberLineWidget);
46641
- case "numeric-input":
46642
- return parseAndAssign(`numeric-input ${n}`, parseNumericInputWidget);
46643
- case "orderer":
46644
- return parseAndAssign(`orderer ${n}`, parseOrdererWidget);
46645
- case "passage":
46646
- return parseAndAssign(`passage ${n}`, parsePassageWidget);
46647
- case "passage-ref":
46648
- return parseAndAssign(`passage-ref ${n}`, parsePassageRefWidget);
46649
- case "passage-ref-target":
46650
- // NOTE(benchristel): as of 2024-11-12, passage-ref-target is only
46651
- // used in test content. See:
46652
- // https://www.khanacademy.org/devadmin/content/search?query=widget:passage-ref-target
46653
- return parseAndAssign(`passage-ref-target ${n}`, any);
46654
- case "phet-simulation":
46655
- return parseAndAssign(`phet-simulation ${n}`, parsePhetSimulationWidget);
46656
- case "plotter":
46657
- return parseAndAssign(`plotter ${n}`, parsePlotterWidget);
46658
- case "python-program":
46659
- return parseAndAssign(`python-program ${n}`, parsePythonProgramWidget);
46660
- case "radio":
46661
- return parseAndAssign(`radio ${n}`, parseRadioWidget);
46662
- case "sorter":
46663
- return parseAndAssign(`sorter ${n}`, parseSorterWidget);
46664
- case "table":
46665
- return parseAndAssign(`table ${n}`, parseTableWidget);
46666
- case "video":
46667
- return parseAndAssign(`video ${n}`, parseVideoWidget);
46668
- case "sequence":
46669
- // sequence is a deprecated widget type, and the corresponding
46670
- // widget component no longer exists.
46671
- return parseAndAssign(`sequence ${n}`, parseDeprecatedWidget);
46672
- case "lights-puzzle":
46673
- return parseAndAssign(`lights-puzzle ${n}`, parseDeprecatedWidget);
46674
- case "simulator":
46675
- return parseAndAssign(`simulator ${n}`, parseDeprecatedWidget);
46676
- case "transformer":
46677
- return parseAndAssign(`transformer ${n}`, parseDeprecatedWidget);
46678
- default:
46679
- if (getWidget(type)) {
46680
- // @ts-expect-error - 'type' is not a valid widget type
46681
- return parseAndAssign(`${type} ${n}`, any);
46682
- }
46683
- return ctx.failure("a valid widget type", type);
46684
- }
46685
- };
46686
- const parseDeprecatedWidget = parseWidget(
46687
- // Ignore the incoming widget type and hardcode "deprecated-standin"
46688
- (_, ctx) => ctx.success("deprecated-standin"),
46689
- // Allow any widget options
46690
- object({}));
46691
- const parseStringToPositiveInt = (rawValue, ctx) => {
46692
- if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
46693
- return ctx.failure("a string representing a positive integer", rawValue);
46694
- }
46695
- return ctx.success(+rawValue);
46696
- };
46697
- const parseWidgetIdComponents = pair(string, parseStringToPositiveInt);
46698
-
46699
- const parsePerseusRenderer = defaulted(object({
46700
- // TODO(benchristel): content is also defaulted to empty string in
46701
- // renderer.tsx. See if we can remove one default or the other.
46702
- content: defaulted(string, () => ""),
46703
- // This module has an import cycle with parseWidgetsMap, because the
46704
- // `group` widget can contain another renderer.
46705
- // The anonymous function below ensures that we don't try to access
46706
- // parseWidgetsMap before it's defined.
46707
- widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
46708
- metadata: optional(array(string)),
46709
- images: parseImages
46710
- }),
46711
- // Default value
46712
- () => ({
46713
- content: "",
46714
- widgets: {},
46715
- images: {}
46716
- }));
46717
-
46718
- const parsePerseusArticle = union(parsePerseusRenderer).or(array(parsePerseusRenderer)).parser;
46719
-
46720
- const parseHint = object({
46721
- replace: optional(boolean),
46722
- content: string,
46723
- widgets: defaulted(parseWidgetsMap, () => ({})),
46724
- metadata: optional(array(string)),
46725
- images: parseImages
46726
- });
46727
-
46728
- const parsePerseusItem$1 = object({
46729
- question: parsePerseusRenderer,
46730
- hints: defaulted(array(parseHint), () => []),
46731
- answerArea: pipeParsers(defaulted(object({}), () => ({}))).then(migrateAnswerArea).then(record(enumeration(...perseusCore.ItemExtras), boolean)).parser,
46732
- itemDataVersion: optional(object({
46733
- major: number,
46734
- minor: number
46735
- })),
46736
- // Deprecated field
46737
- answer: any
46738
- });
46739
-
46740
- // Some answerAreas have extra fields, like:
46741
- //
46742
- // "answerArea": {
46743
- // "type": "multiple",
46744
- // "options": {
46745
- // "content": "",
46746
- // "images": {},
46747
- // "widgets": {}
46748
- // }
46749
- // }
46750
- //
46751
- // The "type" and "options" fields don't seem to be used anywhere. This
46752
- // migration function removes them.
46753
- function migrateAnswerArea(rawValue, ctx) {
46754
- const {
46755
- type: _,
46756
- options: __,
46757
- ...rest
46758
- } = rawValue;
46759
- return ctx.success(rest);
46760
- }
46761
-
46762
- /**
46763
- * Helper to parse PerseusItem JSON
46764
- * Why not just use JSON.parse? We want:
46765
- * - To make sure types are correct
46766
- * - To give us a central place to validate/transform output if needed
46767
- * @deprecated - use parseAndMigratePerseusItem instead
46768
- * @param {string} json - the stringified PerseusItem JSON
46769
- * @returns {PerseusItem} the parsed PerseusItem object
46770
- */
46771
- function parsePerseusItem(json) {
46772
- // Try to block a cheating vector which relies on monkey-patching
46773
- // JSON.parse
46774
- if (isRealJSONParse(JSON.parse)) {
46775
- return JSON.parse(json);
46776
- }
46777
- throw new Error("Something went wrong.");
46778
- }
46779
- /**
46780
- * Parses a PerseusItem from a JSON string, migrates old formats to the latest
46781
- * schema, and runtime-typechecks the result. Use this to parse assessmentItem
46782
- * data.
46783
- *
46784
- * @returns a {@link Result} of the parsed PerseusItem. If the result is a
46785
- * failure, it will contain an error message describing where in the tree
46786
- * parsing failed.
46787
- * @throws SyntaxError if the argument is not well-formed JSON.
46788
- */
46789
- function parseAndMigratePerseusItem(json) {
46790
- throwErrorIfCheatingDetected();
46791
- const object = JSON.parse(json);
46792
- const result = parse(object, parsePerseusItem$1);
46793
- if (isFailure(result)) {
46794
- return failure({
46795
- message: result.detail,
46796
- invalidObject: object
46797
- });
46798
- }
46799
- return result;
46800
- }
46801
-
46802
- /**
46803
- * Parses a PerseusArticle from a JSON string, migrates old formats to the
46804
- * latest schema, and runtime-typechecks the result.
46805
- *
46806
- * @returns a {@link Result} of the parsed PerseusArticle. If the result is a
46807
- * failure, it will contain an error message describing where in the tree
46808
- * parsing failed.
46809
- * @throws SyntaxError if the argument is not well-formed JSON.
46810
- */
46811
- function parseAndMigratePerseusArticle(json) {
46812
- throwErrorIfCheatingDetected();
46813
- const object = JSON.parse(json);
46814
- const result = parse(object, parsePerseusArticle);
46815
- if (isFailure(result)) {
46816
- return failure({
46817
- message: result.detail,
46818
- invalidObject: object
46819
- });
46820
- }
46821
- return result;
46822
- }
46823
-
46824
- /**
46825
- * Tries to block a cheating vector that relies on monkey-patching JSON.parse.
46826
- */
46827
- // TODO(LEMS-2331): delete this function once server-side scoring is done.
46828
- function throwErrorIfCheatingDetected() {
46829
- if (!isRealJSONParse(JSON.parse)) {
46830
- throw new Error("Something went wrong.");
46831
- }
46832
- }
46833
-
46834
45356
  const genericPerseusItemData = {
46835
45357
  question: {
46836
45358
  content: "",
@@ -47052,6 +45574,26 @@ const ScoringUtil = {
47052
45574
  * Types
47053
45575
  */
47054
45576
 
45577
+ Object.defineProperty(exports, 'isFailure', {
45578
+ enumerable: true,
45579
+ get: function () { return perseusCore.isFailure; }
45580
+ });
45581
+ Object.defineProperty(exports, 'isSuccess', {
45582
+ enumerable: true,
45583
+ get: function () { return perseusCore.isSuccess; }
45584
+ });
45585
+ Object.defineProperty(exports, 'parseAndMigratePerseusArticle', {
45586
+ enumerable: true,
45587
+ get: function () { return perseusCore.parseAndMigratePerseusArticle; }
45588
+ });
45589
+ Object.defineProperty(exports, 'parseAndMigratePerseusItem', {
45590
+ enumerable: true,
45591
+ get: function () { return perseusCore.parseAndMigratePerseusItem; }
45592
+ });
45593
+ Object.defineProperty(exports, 'parsePerseusItem', {
45594
+ enumerable: true,
45595
+ get: function () { return perseusCore.parsePerseusItem; }
45596
+ });
47055
45597
  exports.ApiOptions = ApiOptions;
47056
45598
  exports.ArticleRenderer = ArticleRenderer;
47057
45599
  exports.BaseRadio = BaseRadio;
@@ -47077,7 +45619,6 @@ exports.PerseusI18nContext = PerseusI18nContext;
47077
45619
  exports.PerseusI18nContextProvider = PerseusI18nContextProvider;
47078
45620
  exports.PerseusMarkdown = PerseusMarkdown;
47079
45621
  exports.PlotterWidget = Plotter$1;
47080
- exports.QUESTION_WIDGETS = QUESTION_WIDGETS;
47081
45622
  exports.Radio = Radio;
47082
45623
  exports.Renderer = Renderer;
47083
45624
  exports.ScoringUtil = ScoringUtil;
@@ -47086,7 +45627,6 @@ exports.TableWidget = Table$1;
47086
45627
  exports.Util = Util;
47087
45628
  exports.WIDGET_PROP_DENYLIST = denylist;
47088
45629
  exports.Widgets = widgets$1;
47089
- exports.addWidget = addWidget;
47090
45630
  exports.apiVersion = apiVersion;
47091
45631
  exports.bodyXsmallBold = bodyXsmallBold;
47092
45632
  exports.components = components;
@@ -47117,8 +45657,6 @@ exports.getSegmentCoords = getSegmentCoords;
47117
45657
  exports.getSinusoidCoords = getSinusoidCoords;
47118
45658
  exports.getValidWidgetIds = getValidWidgetIds;
47119
45659
  exports.getWidgetFromWidgetMap = getWidgetFromWidgetMap;
47120
- exports.getWidgetIdsFromContent = getWidgetIdsFromContent;
47121
- exports.getWidgetIdsFromContentByType = getWidgetIdsFromContentByType;
47122
45660
  exports.getWidgetTypeByWidgetId = getWidgetTypeByWidgetId;
47123
45661
  exports.getWidgetsFromWidgetMap = getWidgetsFromWidgetMap;
47124
45662
  exports.getWidgetsMapFromItemData = getWidgetsMapFromItemData;
@@ -47127,9 +45665,7 @@ exports.iconTrash = iconTrash;
47127
45665
  exports.init = init;
47128
45666
  exports.injectWidgets = injectWidgets;
47129
45667
  exports.interactiveSizes = interactiveSizes$1;
47130
- exports.isFailure = isFailure;
47131
45668
  exports.isItemRenderableByVersion = isItemRenderableByVersion;
47132
- exports.isSuccess = isSuccess;
47133
45669
  exports.isWidgetIdInContent = isWidgetIdInContent;
47134
45670
  exports.isWrongAnswerSupported = isWrongAnswerSupported;
47135
45671
  exports.itemDataVersion = itemDataVersion;
@@ -47138,10 +45674,7 @@ exports.libVersion = libVersion;
47138
45674
  exports.makeSafeUrl = makeSafeUrl;
47139
45675
  exports.mathOnlyParser = mathOnlyParser;
47140
45676
  exports.mediaQueries = mediaQueries;
47141
- exports.parseAndMigratePerseusArticle = parseAndMigratePerseusArticle;
47142
- exports.parseAndMigratePerseusItem = parseAndMigratePerseusItem;
47143
45677
  exports.parseDataFromJSONP = parseDataFromJSONP;
47144
- exports.parsePerseusItem = parsePerseusItem;
47145
45678
  exports.preprocessTex = preprocessTex;
47146
45679
  exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
47147
45680
  exports.scorePerseusItem = scorePerseusItem;