@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/es/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import * as KAS from '@khanacademy/kas';
2
2
  import { MathInputI18nContext, CursorContext, getKeyTranslator, createMathField, getCursorContext, DesktopKeypad, convertDotToTimesByLocale, mathQuillInstance, KeypadInput, KeypadType, KeyArray, keypadElementPropType } from '@khanacademy/math-input';
3
- import { PerseusError, Errors, getDecimalSeparator, approximateDeepEqual, pluck, mapObject, GrapherUtil as GrapherUtil$1, lockedFigureColors, lockedFigureFillStyles, approximateEqual, getMatrixSize, addLibraryVersionToPerseusDebug, lockedFigureColorNames, plotterPlotTypes, ItemExtras } from '@khanacademy/perseus-core';
3
+ import { PerseusError, Errors, expressionLogic, getExpressionPublicWidgetOptions, getDecimalSeparator, approximateDeepEqual, pluck, getWidgetIdsFromContent, mapObject, passageRefLogic, radioLogic, getCategorizerPublicWidgetOptions, GrapherUtil as GrapherUtil$1, lockedFigureColors, lockedFigureFillStyles, approximateEqual, getMatrixSize, measurerLogic, getOrdererPublicWidgetOptions, addLibraryVersionToPerseusDebug, getWidgetIdsFromContentByType } from '@khanacademy/perseus-core';
4
+ export { isFailure, isSuccess, parseAndMigratePerseusArticle, parseAndMigratePerseusItem, parsePerseusItem } from '@khanacademy/perseus-core';
4
5
  import * as PerseusLinter from '@khanacademy/perseus-linter';
5
6
  import { linterContextDefault, Rule } from '@khanacademy/perseus-linter';
6
- import { scoreExpression, KhanAnswerTypes, scoreInputNumber, inputNumberAnswerTypes, scoreNumericInput, scoreRadio, scoreCategorizer, scoreCSProgram, scoreDropdown, scoreGrapher, scoreIframe, scoreInteractiveGraph, scoreLabelImage, scoreLabelImageMarker, scoreMatcher, scoreMatrix, scoreNumberLine, scoreOrderer, scorePlotter, scoreSorter, scoreTable } from '@khanacademy/perseus-score';
7
+ import { scoreExpression, validateExpression, KhanAnswerTypes, scoreInputNumber, inputNumberAnswerTypes, scoreNumericInput, scoreRadio, validateRadio, scoreCategorizer, validateCategorizer, scoreCSProgram, scoreDropdown, validateDropdown, scoreGrapher, scoreIframe, scoreInteractiveGraph, scoreLabelImage, scoreLabelImageMarker, scoreMatcher, scoreMatrix, validateMatrix, scoreNumberLine, validateNumberLine, scoreOrderer, validateOrderer, scorePlotter, validatePlotter, scoreSorter, validateSorter, scoreTable, validateTable } from '@khanacademy/perseus-score';
7
8
  import { View, RenderStateRoot, Id, useOnMountEffect, useLatestRef, Text as Text$2 } from '@khanacademy/wonder-blocks-core';
8
9
  import Tooltip$1 from '@khanacademy/wonder-blocks-tooltip';
9
10
  import { HeadingMedium, LabelSmall, LabelLarge, Body, LabelMedium } from '@khanacademy/wonder-blocks-typography';
@@ -22,7 +23,7 @@ import { v4 } from 'uuid';
22
23
  import PropTypes from 'prop-types';
23
24
  import { entries, UnreachableCaseError, keys } from '@khanacademy/wonder-stuff-core';
24
25
  import { CircularSpinner } from '@khanacademy/wonder-blocks-progress-spinner';
25
- import { point, KhanMath, number as number$1, vector as vector$3, line, geometry, angles, coefficients } from '@khanacademy/kmath';
26
+ import { point, KhanMath, number, vector as vector$3, line, geometry, angles, coefficients } from '@khanacademy/kmath';
26
27
  import SimpleMarkdown from '@khanacademy/simple-markdown';
27
28
  import { pureMarkdownRules } from '@khanacademy/pure-markdown';
28
29
  import { TextField } from '@khanacademy/wonder-blocks-form';
@@ -58,191 +59,6 @@ function _extends() {
58
59
  * The translated strings that are used to render Perseus.
59
60
  */
60
61
 
61
- /**
62
- * Untranslated strings used in Perseus. To be used by an external
63
- * translator to produce translated strings, passed in as `PerseusStrings`.
64
- * !! Note: Ensure that all escape sequences are double-escaped. (e.g. `\\text` -> `\\\\text`)
65
- */
66
- const strings = {
67
- closeKeypad: "close math keypad",
68
- openKeypad: "open math keypad",
69
- mathInputBox: "Math input box",
70
- removeHighlight: "Remove highlight",
71
- addHighlight: "Add highlight",
72
- hintPos: "Hint #%(pos)s",
73
- errorRendering: "Error rendering: %(error)s",
74
- 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",
75
- EXTRA_SYMBOLS_ERROR: "We could not understand your " + "answer. Please check your answer for extra " + "text or symbols.",
76
- NEEDS_TO_BE_SIMPLFIED_ERROR: "Your answer is almost correct, " + "but it needs to be simplified.",
77
- MISSING_PERCENT_ERROR: "Your answer is almost correct, " + "but it is missing a " + "<code>\\\\%</code> at the end.",
78
- MULTIPLICATION_SIGN_ERROR: "I'm a computer. I only understand " + "multiplication if you use an asterisk " + "(*) as the multiplication sign.",
79
- WRONG_CASE_ERROR: "Your answer includes use of a variable with the wrong case.",
80
- WRONG_LETTER_ERROR: "Your answer includes a wrong variable letter.",
81
- invalidSelection: "Make sure you select something for every row.",
82
- ERROR_TITLE: "Oops!",
83
- ERROR_MESSAGE: "Sorry, I don't understand that!",
84
- hints: "Hints",
85
- getAnotherHint: "Get another hint",
86
- deprecatedStandin: "Sorry, this part of the question is no longer available. 😅 Don't worry, you won't be graded on this part. Keep going!",
87
- keepTrying: "Keep trying",
88
- tryAgain: "Try again",
89
- check: "Check",
90
- correctExcited: "Correct!",
91
- nextQuestion: "Next question",
92
- skipToTitle: "Skip to %(title)s",
93
- current: "Current",
94
- correct: "Correct",
95
- correctSelected: "Correct (selected)",
96
- correctCrossedOut: "Correct (but you crossed it out)",
97
- incorrect: "Incorrect",
98
- incorrectSelected: "Incorrect (selected)",
99
- hideExplanation: "Hide explanation",
100
- explain: "Explain",
101
- INVALID_MESSAGE_PREFIX: "We couldn't grade your answer.",
102
- DEFAULT_INVALID_MESSAGE_1: "It looks like you left something blank or ",
103
- DEFAULT_INVALID_MESSAGE_2: "entered in an invalid answer.",
104
- integerExample: "an integer, like $6$",
105
- properExample: "a *proper* fraction, like $1/2$ or $6/10$",
106
- simplifiedProperExample: "a *simplified proper* fraction, like $3/5$",
107
- improperExample: "an *improper* fraction, like $10/7$ or $14/8$",
108
- simplifiedImproperExample: "a *simplified improper* fraction, like $7/4$",
109
- mixedExample: "a mixed number, like $1\\\\ 3/4$",
110
- decimalExample: "an *exact* decimal, like $0.75$",
111
- percentExample: "a percent, like $12.34\\\\%$",
112
- piExample: "a multiple of pi, like $12$ pi or $2/3$ pi",
113
- yourAnswer: "**Your answer should be** ",
114
- yourAnswerLabel: "Your answer:",
115
- addPoints: "Click to add points",
116
- addVertices: "Click to add vertices",
117
- tapMultiple: "Tap each dot on the image to select all answers that apply.",
118
- tapSingle: "Tap each dot on the image to select an answer.",
119
- clickMultiple: "Click each dot on the image to select all answers that apply.",
120
- clickSingle: "Click each dot on the image to select an answer.",
121
- choices: "Choices:",
122
- answers: {
123
- one: "%(num)s answer",
124
- other: "%(num)s answers"
125
- },
126
- hideAnswersToggleLabel: "Hide answer choices",
127
- moves: {
128
- one: "Moves: %(num)s",
129
- other: "Moves: %(num)s"
130
- },
131
- clickTiles: "Click on the tiles to change the lights.",
132
- turnOffLights: "You must turn on all of the lights to continue.",
133
- fillAllCells: "Make sure you fill in all cells in the matrix.",
134
- molecularDrawing: "A molecular structure drawing. SMILES notation: %(content)s",
135
- switchDirection: "Switch direction",
136
- circleOpen: "Make circle open",
137
- circleFilled: "Make circle filled",
138
- numDivisions: "Number of divisions:",
139
- divisions: "Please make sure the number of divisions is in the range %(divRangeString)s.",
140
- lineRange: "lines %(lineRange)s",
141
- lineNumber: "line %(lineNumber)s",
142
- symbolPassage: "The symbol %(questionSymbol)s indicates that question %(questionNumber)s references this portion of the passage.",
143
- symbolQuestion: " The symbol %(sentenceSymbol)s indicates that the following sentence is referenced in a question.",
144
- lineLabel: {
145
- context: "a label next to a reading passage to denote the line number",
146
- message: "Line"
147
- },
148
- beginningPassage: "Beginning of reading passage.",
149
- beginningFootnotes: "Beginning of reading passage footnotes.",
150
- endPassage: "End of reading passage.",
151
- questionMarker: "[Marker for question %(number)s]",
152
- circleMarker: "[Circle marker %(number)s]",
153
- sentenceMarker: "[Sentence %(number)s]",
154
- dragHandles: "Drag handles to make graph",
155
- tapAddPoints: "Tap to add points",
156
- false: "False",
157
- true: "True",
158
- no: "No",
159
- yes: "Yes",
160
- chooseCorrectNum: "Please choose the correct number of answers.",
161
- notNoneOfTheAbove: "'None of the above' may not be selected when other answers are selected.",
162
- noneOfTheAbove: "None of the above",
163
- chooseNumAnswers: "Choose %(numCorrect)s answers:",
164
- chooseAllAnswers: "Choose all answers that apply:",
165
- chooseOneAnswer: "Choose 1 answer:",
166
- choiceCheckedCorrect: "(Choice %(letter)s, Checked, Correct)",
167
- choiceCrossedOutCorrect: "(Choice %(letter)s, Crossed out, Correct)",
168
- choiceCorrect: "(Choice %(letter)s, Correct)",
169
- choiceCheckedIncorrect: "(Choice %(letter)s, Checked, Incorrect)",
170
- choiceCrossedOutIncorrect: "(Choice %(letter)s, Crossed out, Incorrect)",
171
- choiceIncorrect: "(Choice %(letter)s, Incorrect)",
172
- choiceChecked: "(Choice %(letter)s, Checked)",
173
- choiceCrossedOut: "(Choice %(letter)s, Crossed out)",
174
- choice: "(Choice %(letter)s)",
175
- crossOut: "Cross out",
176
- crossOutOption: "Cross out option",
177
- crossOutChoice: "Cross out Choice %(letter)s",
178
- bringBack: "Bring back",
179
- openMenuForChoice: "Open menu for Choice %(letter)s",
180
- letters: {
181
- 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.",
182
- 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"
183
- },
184
- rightArrow: "Reaction arrow pointing to the right.",
185
- dontUnderstandUnits: "I couldn't understand those units.",
186
- checkSigFigs: "Check your significant figures.",
187
- answerNumericallyIncorrect: "That answer is numerically incorrect.",
188
- checkUnits: "Check your units.",
189
- dontUnderstand: "I don't understand that",
190
- loading: "Loading...",
191
- videoTranscript: "See video transcript",
192
- somethingWrong: "Something went wrong.",
193
- videoWrapper: "Khan Academy video wrapper",
194
- mathInputTitle: "mathematics keyboard",
195
- mathInputDescription: "Use keyboard/mouse to interact with math-based input fields",
196
- sin: "sin",
197
- cos: "cos",
198
- tan: "tan",
199
- simulationLoadFail: "Sorry, this simulation cannot load.",
200
- simulationLocaleWarning: "Sorry, this simulation isn't available in your language.",
201
- selectAnAnswer: "Select an answer",
202
- // The following strings are used for interactive graph SR descriptions.
203
- addPoint: "Add Point",
204
- removePoint: "Remove Point",
205
- graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
206
- closePolygon: "Close shape",
207
- openPolygon: "Re-open shape",
208
- srInteractiveElements: "Interactive elements: %(elements)s",
209
- srNoInteractiveElements: "No interactive elements",
210
- // TODO(LEMS-2660): The following strings are ones that will need
211
- // translation tickets after all interactive graph SR strings have
212
- // been finalized. Remove this comment after the tickets have been
213
- // created.
214
- srPointAtCoordinates: "Point %(num)s at %(x)s comma %(y)s",
215
- srCircleGraph: "A circle on a coordinate plane.",
216
- srCircleShape: "Circle. The center point is at %(centerX)s comma %(centerY)s.",
217
- srCircleRadiusPoint: "Radius point at %(radiusPointX)s comma %(radiusPointY)s.",
218
- srCircleRadius: "Circle radius is %(radius)s.",
219
- 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.",
220
- srLinearGraph: "A line on a coordinate plane.",
221
- srLinearGraphPoints: "The line has two points, point 1 at %(point1X)s comma %(point1Y)s and point 2 at %(point2X)s comma %(point2Y)s.",
222
- srLinearGraphSlopeIncreasing: "Its slope increases from left to right.",
223
- srLinearGraphSlopeDecreasing: "Its slope decreases from left to right.",
224
- srLinearGraphSlopeHorizontal: "Its slope is zero.",
225
- srLinearGraphSlopeVertical: "Its slope is undefined.",
226
- srLinearGraphXOnlyIntercept: "The line crosses the X-axis at %(xIntercept)s comma 0.",
227
- srLinearGraphYOnlyIntercept: "The line crosses the Y-axis at 0 comma %(yIntercept)s.",
228
- srLinearGraphBothIntercepts: "The line crosses the X-axis at %(xIntercept)s comma 0 and the Y-axis at 0 comma %(yIntercept)s.",
229
- srLinearGraphOriginIntercept: "The line crosses the x and y axes at the graph's origin.",
230
- srLinearGrabHandle: "Line from %(point1X)s comma %(point1Y)s to %(point2X)s comma %(point2Y)s.",
231
- srAngleSideAtCoordinates: "Point %(point)s, %(side)s at %(x)s comma %(y)s",
232
- srAngleVertexAtCoordinatesWithAngleMeasure: "Point 2, vertex at %(x)s comma %(y)s. Angle %(angleMeasure)s degrees",
233
- srAngleGraphAriaLabel: "An angle on a coordinate plane.",
234
- 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",
235
- srLinearSystemGraph: "Two lines on a coordinate plane.",
236
- srLinearSystemPoints: "Line %(lineNumber)s has two points, point 1 at %(point1X)s comma %(point1Y)s and point 2 at %(point2X)s comma %(point2Y)s.",
237
- srLinearSystemPoint: "Point %(pointSequence)s on line %(lineNumber)s at %(x)s comma %(y)s.",
238
- srRayGraph: "A ray on a coordinate plane.",
239
- srRayPoints: "The endpoint is at %(point1X)s comma %(point1Y)s and the ray goes through point %(point2X)s comma %(point2Y)s.",
240
- srRayGrabHandle: "Ray with endpoint %(point1X)s comma %(point1Y)s going through point %(point2X)s comma %(point2Y)s.",
241
- srRayEndpoint: "Endpoint at %(x)s comma %(y)s.",
242
- srRayTerminalPoint: "Through point at %(x)s comma %(y)s."
243
- // The above strings are used for interactive graph SR descriptions.
244
- };
245
-
246
62
  /**
247
63
  * Mock strings for the Perseus package, to be used for tests and Storybook.
248
64
  */
@@ -509,6 +325,43 @@ const mockStrings = {
509
325
  endingSideX,
510
326
  endingSideY
511
327
  }) => `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}.`,
328
+ srSingleSegmentGraphAriaLabel: "A line segment on a coordinate plane.",
329
+ srMultipleSegmentGraphAriaLabel: ({
330
+ countOfSegments
331
+ }) => `${countOfSegments} segments on a coordinate plane.`,
332
+ srMultipleSegmentIndividualLabel: ({
333
+ point1X,
334
+ point1Y,
335
+ point2X,
336
+ point2Y,
337
+ indexOfSegment
338
+ }) => `Segment ${indexOfSegment}: Endpoint 1 at ${point1X} comma ${point1Y}. Endpoint 2 at ${point2X} comma ${point2Y}.`,
339
+ srSingleSegmentLabel: ({
340
+ point1X,
341
+ point1Y,
342
+ point2X,
343
+ point2Y
344
+ }) => `Endpoint 1 at ${point1X} comma ${point1Y}. Endpoint 2 at ${point2X} comma ${point2Y}.`,
345
+ srSegmentLength: ({
346
+ length
347
+ }) => `Segment length ${length} units.`,
348
+ srSingleSegmentGraphEndpointAriaLabel: ({
349
+ endpointNumber,
350
+ x,
351
+ y
352
+ }) => `Endpoint ${endpointNumber} at ${x} comma ${y}.`,
353
+ srMultipleSegmentGraphEndpointAriaLabel: ({
354
+ endpointNumber,
355
+ x,
356
+ y,
357
+ indexOfSegment
358
+ }) => `Endpoint ${endpointNumber} on segment ${indexOfSegment} at ${x} comma ${y}.`,
359
+ srSegmentGrabHandle: ({
360
+ point1X,
361
+ point1Y,
362
+ point2X,
363
+ point2Y
364
+ }) => `Segment from ${point1X} comma ${point1Y} to ${point2X} comma ${point2Y}.`,
512
365
  srLinearSystemGraph: "Two lines on a coordinate plane.",
513
366
  srLinearSystemPoints: ({
514
367
  lineNumber,
@@ -543,30 +396,79 @@ const mockStrings = {
543
396
  srRayTerminalPoint: ({
544
397
  x,
545
398
  y
546
- }) => `Through point at ${x} comma ${y}.`
399
+ }) => `Through point at ${x} comma ${y}.`,
400
+ srQuadraticGraph: "A parabola on a 4-quadrant coordinate plane.",
401
+ srQuadraticFaceUp: "The parabola opens upward.",
402
+ srQuadraticFaceDown: "The parabola opens downward.",
403
+ srQuadraticGraphVertexOrigin: "Vertex is at the origin.",
404
+ srQuadraticGraphVertexXAxis: "Vertex is on the X-axis.",
405
+ srQuadraticGraphVertexYAxis: "Vertex is on the Y-axis.",
406
+ srQuadraticGraphVertexQuadrant: ({
407
+ quadrant
408
+ }) => `Vertex is in quadrant ${quadrant}.`,
409
+ srQuadraticTwoXIntercepts: ({
410
+ intercept1,
411
+ intercept2
412
+ }) => `The X-intercepts are at ${intercept1} comma 0 and ${intercept2} comma 0.`,
413
+ srQuadraticOneXIntercept: ({
414
+ intercept
415
+ }) => `The X-intercept is at ${intercept} comma 0.`,
416
+ srQuadraticYIntercept: ({
417
+ intercept
418
+ }) => `The Y-intercept is at 0 comma ${intercept}.`,
419
+ srQuadraticPointOrigin: ({
420
+ pointNumber
421
+ }) => `Point ${pointNumber} on parabola at the origin.`,
422
+ srQuadraticPointAxis: ({
423
+ pointNumber,
424
+ x,
425
+ y
426
+ }) => `Point ${pointNumber} on parabola at ${x} comma ${y}.`,
427
+ srQuadraticPointQuadrant: ({
428
+ pointNumber,
429
+ x,
430
+ y,
431
+ quadrant
432
+ }) => `Point ${pointNumber} on parabola in quadrant ${quadrant} at ${x} comma ${y}.`,
433
+ srQuadraticInteractiveElements: ({
434
+ point1X,
435
+ point1Y,
436
+ point2X,
437
+ point2Y,
438
+ point3X,
439
+ point3Y
440
+ }) => `Parabola with points at ${point1X} comma ${point1Y}, ${point2X} comma ${point2Y}, and ${point3X} comma ${point3Y}.`
547
441
  // The above strings are used for interactive graph SR descriptions.
548
442
  };
549
443
 
550
444
  // This type helps us make sure all error codes are mapped to strings
551
445
 
446
+ /**
447
+ * Map an error string to a PerseusStrings key
448
+ * that we can use to get the translated error message
449
+ */
552
450
  const errorToString = {
553
- MISSING_PERCENT_ERROR: strings.MISSING_PERCENT_ERROR,
554
- NEEDS_TO_BE_SIMPLIFIED_ERROR: strings.NEEDS_TO_BE_SIMPLFIED_ERROR,
555
- APPROXIMATED_PI_ERROR: strings.APPROXIMATED_PI_ERROR,
556
- EXTRA_SYMBOLS_ERROR: strings.EXTRA_SYMBOLS_ERROR,
557
- WRONG_CASE_ERROR: strings.WRONG_CASE_ERROR,
558
- WRONG_LETTER_ERROR: strings.WRONG_LETTER_ERROR,
559
- MULTIPLICATION_SIGN_ERROR: strings.MULTIPLICATION_SIGN_ERROR,
560
- INVALID_SELECTION_ERROR: strings.invalidSelection,
561
- CHOOSE_CORRECT_NUM_ERROR: strings.chooseCorrectNum,
562
- NOT_NONE_ABOVE_ERROR: strings.notNoneOfTheAbove,
563
- FILL_ALL_CELLS_ERROR: strings.fillAllCells
564
- };
565
- function mapErrorToString(err) {
451
+ MISSING_PERCENT_ERROR: "MISSING_PERCENT_ERROR",
452
+ NEEDS_TO_BE_SIMPLIFIED_ERROR: "NEEDS_TO_BE_SIMPLFIED_ERROR",
453
+ APPROXIMATED_PI_ERROR: "APPROXIMATED_PI_ERROR",
454
+ EXTRA_SYMBOLS_ERROR: "EXTRA_SYMBOLS_ERROR",
455
+ WRONG_CASE_ERROR: "WRONG_CASE_ERROR",
456
+ WRONG_LETTER_ERROR: "WRONG_LETTER_ERROR",
457
+ MULTIPLICATION_SIGN_ERROR: "MULTIPLICATION_SIGN_ERROR",
458
+ INVALID_SELECTION_ERROR: "invalidSelection",
459
+ CHOOSE_CORRECT_NUM_ERROR: "chooseCorrectNum",
460
+ NOT_NONE_ABOVE_ERROR: "notNoneOfTheAbove",
461
+ FILL_ALL_CELLS_ERROR: "fillAllCells"
462
+ };
463
+ function mapErrorToString(
464
+ // the string representing an error code
465
+ err,
466
+ // the translated Perseus strings
467
+ translatedStrings) {
566
468
  if (!err) {
567
469
  return err;
568
470
  }
569
- return errorToString[err] || err;
471
+ return translatedStrings[errorToString[err]] || err;
570
472
  }
571
473
 
572
474
  /**
@@ -1830,23 +1732,6 @@ const keypadConfigurationForProps = widgetOptions => {
1830
1732
  times: widgetOptions.times
1831
1733
  };
1832
1734
  };
1833
- const propUpgrades$2 = {
1834
- /* c8 ignore next */
1835
- "1": v0props => ({
1836
- times: v0props.times,
1837
- buttonSets: v0props.buttonSets,
1838
- functions: v0props.functions,
1839
- buttonsVisible: v0props.buttonsVisible,
1840
- visibleLabel: v0props.visibleLabel,
1841
- ariaLabel: v0props.ariaLabel,
1842
- answerForms: [{
1843
- considered: "correct",
1844
- form: v0props.form,
1845
- simplify: v0props.simplify,
1846
- value: v0props.value
1847
- }]
1848
- })
1849
- };
1850
1735
  const ExpressionWithDependencies = /*#__PURE__*/React.forwardRef((props, ref) => {
1851
1736
  const deps = useDependencies();
1852
1737
  return /*#__PURE__*/React.createElement(Expression, _extends({
@@ -1886,17 +1771,18 @@ var Expression$1 = {
1886
1771
  ariaLabel
1887
1772
  };
1888
1773
  },
1889
- version: {
1890
- major: 1,
1891
- minor: 0
1892
- },
1893
- propUpgrades: propUpgrades$2,
1774
+ version: expressionLogic.version,
1775
+ propUpgrades: expressionLogic.widgetOptionsUpgrades,
1894
1776
  // For use by the editor
1895
1777
  isLintable: true,
1896
1778
  // TODO(LEMS-2656): remove TS suppression
1897
1779
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'.
1898
1780
  scorer: scoreExpression,
1899
1781
  // TODO(LEMS-2656): remove TS suppression
1782
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'.
1783
+ validator: validateExpression,
1784
+ getPublicWidgetOptions: getExpressionPublicWidgetOptions,
1785
+ // TODO(LEMS-2656): remove TS suppression
1900
1786
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusExpressionRubric'.
1901
1787
  getOneCorrectAnswerFromRubric(rubric) {
1902
1788
  const correctAnswers = (rubric.answerForms || []).filter(answerForm => answerForm.considered === "correct");
@@ -8580,7 +8466,7 @@ class Graphie$1 {
8580
8466
  // We round the coordinates to make testing easier, because trig
8581
8467
  // operations return unround numbers.
8582
8468
  // TODO(benchristel): move rounding to scalePoint?
8583
- const round = x => number$1.round(x, 6);
8469
+ const round = x => number.round(x, 6);
8584
8470
  const startPoint = this.scalePoint([round(center[0] + startVector[0]), round(center[1] + startVector[1])]);
8585
8471
  const endPoint = this.scalePoint([round(center[0] + endVector[0]), round(center[1] + endVector[1])]);
8586
8472
  const largeAngle = ((endAngle - startAngle) % 360 + 360) % 360 > 180;
@@ -17336,7 +17222,7 @@ const inlineNodeTypes = {
17336
17222
 
17337
17223
  // @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'.
17338
17224
  const builtParser$1 = SimpleMarkdown.parserFor(rules$1);
17339
- const parse$4 = (source, state) => {
17225
+ const parse$3 = (source, state) => {
17340
17226
  const paragraphedSource = source + "\n\n";
17341
17227
  return builtParser$1(paragraphedSource, _extends({}, state, {
17342
17228
  inline: false
@@ -17419,14 +17305,14 @@ const getContent = ast => {
17419
17305
  */
17420
17306
  const characterCount = source => {
17421
17307
  // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
17422
- const ast = parse$4(source);
17308
+ const ast = parse$3(source);
17423
17309
  const content = getContent(ast).join("");
17424
17310
  return content.length;
17425
17311
  };
17426
17312
  var PerseusMarkdown = {
17427
17313
  characterCount: characterCount,
17428
17314
  traverseContent: traverseContent,
17429
- parse: parse$4,
17315
+ parse: parse$3,
17430
17316
  parseInline: inlineParser,
17431
17317
  reactFor: SimpleMarkdown.reactFor,
17432
17318
  ruleOutput: SimpleMarkdown.ruleOutput(rules$1, "react"),
@@ -17568,138 +17454,6 @@ guess, state) {
17568
17454
  });
17569
17455
  }
17570
17456
 
17571
- // These are all the current widgets that require user-input and are supported for automatic scoring
17572
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
17573
-
17574
- /**
17575
- * Regex for widget placeholders in a string.
17576
- *
17577
- * First capture group is the widget ID (ex. 'radio 1')
17578
- * Second capture group is the widget type (ex. "radio)
17579
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
17580
- */
17581
- function getWidgetRegex() {
17582
- return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
17583
- }
17584
-
17585
- /**
17586
- * Add a widget placeholder using the widget ID.
17587
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
17588
- *
17589
- * @param {string} id
17590
- * @returns {string}
17591
- */
17592
- function addWidget(id) {
17593
- return `[[☃ ${id}]]`;
17594
- }
17595
-
17596
- /**
17597
- * Get a widget type by a widget's ID
17598
- * (please don't derive type from ID)
17599
- *
17600
- * @param {string} widgetId the ID of the widget
17601
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
17602
- * @returns {string} the widget type (ie "radio")
17603
- */
17604
- function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
17605
- var _widget$type;
17606
- const widget = widgetMap[WidgetId];
17607
- return (_widget$type = widget == null ? void 0 : widget.type) != null ? _widget$type : null;
17608
- }
17609
-
17610
- /**
17611
- * Does the content have a specific type of widget?
17612
- *
17613
- * @param {string} type the type of the widget in question (ie "radio")
17614
- * @param {string} content the string to search through
17615
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
17616
- * @returns {boolean} if the content includes the widget type
17617
- */
17618
- function contentHasWidgetType(type, content, widgetMap) {
17619
- return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
17620
- }
17621
-
17622
- /**
17623
- * Extract all widget IDs, which includes the widget type and instance number.
17624
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
17625
- *
17626
- * Content should contain Perseus widget placeholders,
17627
- * which look like: '[[☃ radio 1]]'.
17628
- *
17629
- * @param {string} content
17630
- * @returns {ReadonlyArray<string>} widgetIds
17631
- */
17632
- function getWidgetIdsFromContent(content) {
17633
- const widgets = [];
17634
- const localWidgetRegex = getWidgetRegex();
17635
- let match = localWidgetRegex.exec(content);
17636
- while (match !== null) {
17637
- widgets.push(match[1]);
17638
- match = localWidgetRegex.exec(content);
17639
- }
17640
- return widgets;
17641
- }
17642
-
17643
- /**
17644
- * Get a list of widget IDs from content,
17645
- * but only for specific widget types
17646
- *
17647
- * @param {string} type the type of widget (ie "radio")
17648
- * @param {string} content the string to parse
17649
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
17650
- * @returns {ReadonlyArray<string>} the widget type (ie "radio")
17651
- */
17652
- function getWidgetIdsFromContentByType(type, content, widgetMap) {
17653
- const rv = [];
17654
- const widgetIdsInContent = getWidgetIdsFromContent(content);
17655
- widgetIdsInContent.forEach(widgetId => {
17656
- const widget = widgetMap[widgetId];
17657
- if ((widget == null ? void 0 : widget.type) === type) {
17658
- rv.push(widgetId);
17659
- }
17660
- });
17661
- return rv;
17662
- }
17663
-
17664
- /**
17665
- * Pull the widget map out of ItemData
17666
- *
17667
- * @param {PerseusItem} PerseusItem containing a widgetMap
17668
- * @returns {WidgetMap} the widget map in the PerseusItem
17669
- */
17670
- function getWidgetsMapFromItemData(itemData) {
17671
- return itemData.question.widgets;
17672
- }
17673
-
17674
- /**
17675
- * Get the widget information from a WidgetMap
17676
- *
17677
- * @param {string} widgetId the ID of the widget
17678
- * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
17679
- */
17680
- function getWidgetFromWidgetMap(widgetId, widgetMap) {
17681
- var _widgetMap$widgetId;
17682
- return (_widgetMap$widgetId = widgetMap[widgetId]) != null ? _widgetMap$widgetId : null;
17683
- }
17684
-
17685
- /**
17686
- * Get select widgets from a widget map.
17687
- *
17688
- * @param {ReadonlyArray<string>} widgetIds to extract
17689
- * @param {PerseusWidgetsMap} widgetMap to extract from
17690
- * @return {PerseusWidgetsMap} a new widget map with requested widgets
17691
- */
17692
- function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
17693
- const widgets = {};
17694
- widgetIds.forEach(widgetId => {
17695
- const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
17696
- if (widget) {
17697
- widgets[widgetId] = widget;
17698
- }
17699
- });
17700
- return widgets;
17701
- }
17702
-
17703
17457
  const DEFAULT_ALIGNMENT$1 = "block";
17704
17458
  // NOTE(kevinb): "default" is not one in `validAlignments`.
17705
17459
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -17804,13 +17558,17 @@ const getWidgetExport = name => {
17804
17558
  var _widgets$name3;
17805
17559
  return (_widgets$name3 = widgets[name]) != null ? _widgets$name3 : null;
17806
17560
  };
17561
+ const getWidgetValidator = name => {
17562
+ var _widgets$name$validat, _widgets$name4;
17563
+ return (_widgets$name$validat = (_widgets$name4 = widgets[name]) == null ? void 0 : _widgets$name4.validator) != null ? _widgets$name$validat : null;
17564
+ };
17807
17565
  const getWidgetScorer = name => {
17808
- var _widgets$name$scorer, _widgets$name4;
17809
- return (_widgets$name$scorer = (_widgets$name4 = widgets[name]) == null ? void 0 : _widgets$name4.scorer) != null ? _widgets$name$scorer : null;
17566
+ var _widgets$name$scorer, _widgets$name5;
17567
+ return (_widgets$name$scorer = (_widgets$name5 = widgets[name]) == null ? void 0 : _widgets$name5.scorer) != null ? _widgets$name$scorer : null;
17810
17568
  };
17811
17569
  const getPublicWidgetOptionsFunction = name => {
17812
- var _widgets$name$getPubl, _widgets$name5;
17813
- return (_widgets$name$getPubl = (_widgets$name5 = widgets[name]) == null ? void 0 : _widgets$name5.getPublicWidgetOptions) != null ? _widgets$name$getPubl : i => i;
17570
+ var _widgets$name$getPubl, _widgets$name6;
17571
+ return (_widgets$name$getPubl = (_widgets$name6 = widgets[name]) == null ? void 0 : _widgets$name6.getPublicWidgetOptions) != null ? _widgets$name$getPubl : i => i;
17814
17572
  };
17815
17573
  const getEditor = name => {
17816
17574
  return _.has(editors, name) ? editors[name] : null;
@@ -18115,6 +17873,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
18115
17873
  replaceDeprecatedEditors: replaceDeprecatedEditors,
18116
17874
  getWidget: getWidget,
18117
17875
  getWidgetExport: getWidgetExport,
17876
+ getWidgetValidator: getWidgetValidator,
18118
17877
  getWidgetScorer: getWidgetScorer,
18119
17878
  getPublicWidgetOptionsFunction: getPublicWidgetOptionsFunction,
18120
17879
  getEditor: getEditor,
@@ -18137,7 +17896,6 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
18137
17896
  });
18138
17897
 
18139
17898
  function getUpgradedWidgetOptions(oldWidgetOptions) {
18140
- // @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; }'.
18141
17899
  return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
18142
17900
  if (!widgetInfo.type || !widgetInfo.alignment) {
18143
17901
  const newValues = {};
@@ -18152,22 +17910,30 @@ function getUpgradedWidgetOptions(oldWidgetOptions) {
18152
17910
  }
18153
17911
  widgetInfo = _extends({}, widgetInfo, newValues);
18154
17912
  }
17913
+ // TODO(LEMS-2656): remove TS suppression
18155
17914
  return upgradeWidgetInfoToLatestVersion(widgetInfo);
18156
17915
  });
18157
17916
  }
17917
+
17918
+ /**
17919
+ * Checks the given user input to see if any answerable widgets have not been
17920
+ * "filled in" (ie. if they're empty). Another way to think about this
17921
+ * function is that its a check to see if we can score the provided input.
17922
+ */
18158
17923
  function emptyWidgetsFunctional(widgets,
18159
17924
  // This is a port of old code, I'm not sure why
18160
17925
  // we need widgetIds vs the keys of the widgets object
18161
17926
  widgetIds, userInputMap, strings, locale) {
18162
- const upgradedWidgets = getUpgradedWidgetOptions(widgets);
18163
17927
  return widgetIds.filter(id => {
18164
- const widget = upgradedWidgets[id];
18165
- if (!widget || widget.static) {
17928
+ const widget = widgets[id];
17929
+ if (!widget || widget.static === true) {
18166
17930
  // Static widgets shouldn't count as empty
18167
17931
  return false;
18168
17932
  }
18169
- const scorer = getWidgetScorer(widget.type);
18170
- const score = scorer == null ? void 0 : scorer(userInputMap[id], widget.options, strings, locale);
17933
+ const validator = getWidgetValidator(widget.type);
17934
+ const userInput = userInputMap[id];
17935
+ const validationData = widget.options;
17936
+ const score = validator == null ? void 0 : validator(userInput, validationData, strings, locale);
18171
17937
  if (score) {
18172
17938
  return scoreIsEmpty(score);
18173
17939
  }
@@ -18201,13 +17967,18 @@ widgetIds, userInputMap, strings, locale) {
18201
17967
  });
18202
17968
  const widgetScores = {};
18203
17969
  gradedWidgetIds.forEach(id => {
17970
+ var _validator;
18204
17971
  const widget = upgradedWidgets[id];
18205
17972
  if (!widget) {
18206
17973
  return;
18207
17974
  }
18208
17975
  const userInput = userInputMap[id];
17976
+ const validator = getWidgetValidator(widget.type);
18209
17977
  const scorer = getWidgetScorer(widget.type);
18210
- const score = scorer == null ? void 0 : scorer(userInput, widget.options, strings, locale);
17978
+
17979
+ // We do validation (empty checks) first and then scoring. If
17980
+ // validation fails, it's result is itself a PerseusScore.
17981
+ const score = (_validator = validator == null ? void 0 : validator(userInput, widget.options, strings, locale)) != null ? _validator : scorer == null ? void 0 : scorer(userInput, widget.options, strings, locale);
18211
17982
  if (score != null) {
18212
17983
  widgetScores[id] = score;
18213
17984
  }
@@ -19607,7 +19378,7 @@ class Renderer extends React.Component {
19607
19378
  const apiOptions = this.getApiOptions();
19608
19379
  const widgetProps = this.state.widgetProps[widgetId] || {};
19609
19380
 
19610
- // The widget needs access to its "rubric" at all times when in review
19381
+ // The widget needs access to its "scoring data" at all times when in review
19611
19382
  // mode (which is really just part of its widget info).
19612
19383
  const widgetInfo = this.state.widgetInfo[widgetId];
19613
19384
  const reviewModeRubric = this.props.reviewMode && widgetInfo ? widgetInfo.options : null;
@@ -19938,7 +19709,7 @@ function _objectWithoutPropertiesLoose(r, e) {
19938
19709
  return t;
19939
19710
  }
19940
19711
 
19941
- const _excluded$7 = ["position", "visibility", "left", "top"];
19712
+ const _excluded$4 = ["position", "visibility", "left", "top"];
19942
19713
  const zIndex = 10;
19943
19714
  let HorizontalDirection = /*#__PURE__*/function (HorizontalDirection) {
19944
19715
  HorizontalDirection["Left"] = "left";
@@ -19989,7 +19760,7 @@ const TooltipArrow = _ref => {
19989
19760
  left = 100,
19990
19761
  top = 0
19991
19762
  } = _ref,
19992
- props = _objectWithoutPropertiesLoose(_ref, _excluded$7);
19763
+ props = _objectWithoutPropertiesLoose(_ref, _excluded$4);
19993
19764
  // TODO(aria): Think about adding a box-shadow to the triangle here
19994
19765
  // See http://css-tricks.com/triangle-with-shadow/
19995
19766
 
@@ -20363,7 +20134,7 @@ InputWithExamples.defaultProps = {
20363
20134
  className: ""
20364
20135
  };
20365
20136
 
20366
- const _excluded$6 = ["keypadElement", "onFocus", "value"];
20137
+ const _excluded$3 = ["keypadElement", "onFocus", "value"];
20367
20138
  class SimpleKeypadInput extends React.Component {
20368
20139
  constructor(...args) {
20369
20140
  super(...args);
@@ -20408,7 +20179,7 @@ class SimpleKeypadInput extends React.Component {
20408
20179
  onFocus,
20409
20180
  value
20410
20181
  } = _this$props,
20411
- rest = _objectWithoutPropertiesLoose(_this$props, _excluded$6);
20182
+ rest = _objectWithoutPropertiesLoose(_this$props, _excluded$3);
20412
20183
  return (
20413
20184
  /*#__PURE__*/
20414
20185
  // @ts-expect-error - TS2769 - No overload matches this call.
@@ -22745,7 +22516,7 @@ const CIRCLE_LABEL_STYLE = {
22745
22516
  // TODO(LEMS-2656): remove TS suppression
22746
22517
  // @ts-expect-error: State is not assignable to ParseState
22747
22518
  const builtParser = SimpleMarkdown.parserFor(rules);
22748
- const parse$3 = (source, state) => {
22519
+ const parse$2 = (source, state) => {
22749
22520
  // @ts-expect-error - TS2322 - Type 'ParseState | {}' is not assignable to type 'ParseState | null | undefined'.
22750
22521
  state = state || {};
22751
22522
  const paragraphedSource = source + "\n\n";
@@ -22755,7 +22526,7 @@ const parse$3 = (source, state) => {
22755
22526
  // @ts-expect-error - TS2322 - Type 'ReactOutput' is not assignable to type '(arg1: SingleASTNode[]) => ReactElement<any, string | JSXElementConstructor<any>>'.
22756
22527
  const output = SimpleMarkdown.reactFor(SimpleMarkdown.ruleOutput(rules, "react"));
22757
22528
  var PassageMarkdown = {
22758
- parse: parse$3,
22529
+ parse: parse$2,
22759
22530
  output,
22760
22531
  START_REF_PREFIX,
22761
22532
  END_REF_PREFIX,
@@ -23312,10 +23083,7 @@ var PassageRef$1 = {
23312
23083
  referenceNumber: widgetOptions.referenceNumber,
23313
23084
  summaryText: widgetOptions.summaryText
23314
23085
  }),
23315
- version: {
23316
- major: 0,
23317
- minor: 1
23318
- },
23086
+ version: passageRefLogic.version,
23319
23087
  // TODO: things that aren't interactive shouldn't need scoring functions
23320
23088
  scorer: () => scoreNoop()
23321
23089
  };
@@ -24297,14 +24065,14 @@ var Choice$1 = /*#__PURE__*/React.forwardRef((props, ref) => /*#__PURE__*/React.
24297
24065
  forwardedRef: ref
24298
24066
  })));
24299
24067
 
24300
- const _excluded$5 = ["content", "forwardedRef", "showContent"];
24068
+ const _excluded$2 = ["content", "forwardedRef", "showContent"];
24301
24069
  const ChoiceNoneAbove = function ChoiceNoneAbove(_ref) {
24302
24070
  let {
24303
24071
  content,
24304
24072
  forwardedRef,
24305
24073
  showContent = true
24306
24074
  } = _ref,
24307
- rest = _objectWithoutPropertiesLoose(_ref, _excluded$5);
24075
+ rest = _objectWithoutPropertiesLoose(_ref, _excluded$2);
24308
24076
  const {
24309
24077
  strings
24310
24078
  } = usePerseusI18n();
@@ -24968,7 +24736,7 @@ class Radio$1 extends React.Component {
24968
24736
  // Current versions of the radio widget always pass in the
24969
24737
  // "correct" value through the choices. Old serialized state
24970
24738
  // for radio widgets doesn't have this though, so we have to
24971
- // pull the correctness out of the review mode rubric. This
24739
+ // pull the correctness out of the review mode scoring data. This
24972
24740
  // only works because all of the places we use
24973
24741
  // `restoreSerializedState()` also turn on reviewMode, but is
24974
24742
  // fine for now.
@@ -25014,7 +24782,6 @@ Radio$1.defaultProps = {
25014
24782
  showSolutions: "none"
25015
24783
  };
25016
24784
 
25017
- const _excluded$4 = ["noneOfTheAbove"];
25018
24785
  const {
25019
24786
  shuffle: shuffle$2,
25020
24787
  random
@@ -25101,20 +24868,6 @@ const transform = (widgetOptions, strings, problemNum) => {
25101
24868
  selectedChoices: _.pluck(choices, "correct")
25102
24869
  };
25103
24870
  };
25104
- const propUpgrades$1 = {
25105
- "1": v0props => {
25106
- const {
25107
- noneOfTheAbove
25108
- } = v0props,
25109
- rest = _objectWithoutPropertiesLoose(v0props, _excluded$4);
25110
- if (noneOfTheAbove) {
25111
- throw new Error("radio widget v0 no longer supports auto noneOfTheAbove");
25112
- }
25113
- return _extends({}, rest, {
25114
- hasNoneOfTheAbove: false
25115
- });
25116
- }
25117
- };
25118
24871
  var Radio = {
25119
24872
  name: "radio",
25120
24873
  displayName: "Radio / Multiple choice",
@@ -25122,15 +24875,15 @@ var Radio = {
25122
24875
  widget: Radio$1,
25123
24876
  transform: transform,
25124
24877
  staticTransform: transform,
25125
- version: {
25126
- major: 1,
25127
- minor: 0
25128
- },
25129
- propUpgrades: propUpgrades$1,
24878
+ version: radioLogic.version,
24879
+ propUpgrades: radioLogic.widgetOptionsUpgrades,
25130
24880
  isLintable: true,
25131
24881
  // TODO(LEMS-2656): remove TS suppression
25132
24882
  // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput
25133
- scorer: scoreRadio
24883
+ scorer: scoreRadio,
24884
+ // TODO(LEMS-2656): remove TS suppression
24885
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput
24886
+ validator: validateRadio
25134
24887
  };
25135
24888
 
25136
24889
  /* eslint-disable import/no-named-as-default */
@@ -25149,24 +24902,6 @@ const getPromptJSON$n = (renderProps, userInput) => {
25149
24902
  };
25150
24903
  };
25151
24904
 
25152
- /**
25153
- * For details on the individual options, see the
25154
- * PerseusCategorizerWidgetOptions type
25155
- */
25156
-
25157
- /**
25158
- * Given a PerseusCategorizerWidgetOptions object, return a new object with only
25159
- * the public options that should be exposed to the client.
25160
- */
25161
- function getCategorizerPublicWidgetOptions(options) {
25162
- return {
25163
- items: options.items,
25164
- categories: options.categories,
25165
- randomizeItems: options.randomizeItems,
25166
- static: options.static
25167
- };
25168
- }
25169
-
25170
24905
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
25171
24906
  class Categorizer extends React.Component {
25172
24907
  constructor(..._args) {
@@ -25346,6 +25081,9 @@ var Categorizer$1 = {
25346
25081
  // TODO(LEMS-2656): remove TS suppression
25347
25082
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
25348
25083
  scorer: scoreCategorizer,
25084
+ // TODO(LEMS-2656): remove TS suppression
25085
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
25086
+ validator: validateCategorizer,
25349
25087
  getPublicWidgetOptions: getCategorizerPublicWidgetOptions
25350
25088
  };
25351
25089
 
@@ -25783,7 +25521,10 @@ var Dropdown$1 = {
25783
25521
  transform: optionsTransform,
25784
25522
  // TODO(LEMS-2656): remove TS suppression
25785
25523
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'.
25786
- scorer: scoreDropdown
25524
+ scorer: scoreDropdown,
25525
+ // TODO(LEMS-2656): remove TS suppression
25526
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'.
25527
+ validator: validateDropdown
25787
25528
  };
25788
25529
 
25789
25530
  const getPromptJSON$j = renderProps => {
@@ -26132,7 +25873,7 @@ class GradedGroup extends React.Component {
26132
25873
  DEFAULT_INVALID_MESSAGE_2
26133
25874
  } = this.context.strings;
26134
25875
  const status = score.type === "points" ? score.total === score.earned ? GRADING_STATUSES.correct : GRADING_STATUSES.incorrect : GRADING_STATUSES.invalid;
26135
- 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}`;
25876
+ 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}`;
26136
25877
  this.setState({
26137
25878
  status: status,
26138
25879
  message: message,
@@ -26974,7 +26715,7 @@ class FunctionGrapher extends React.Component {
26974
26715
  constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
26975
26716
  // Always enforce that this is a function
26976
26717
  const isFunction = _.all(this._coords(), (otherCoord, j) => {
26977
- return i === j || !otherCoord || !number$1.equal(coord[0], otherCoord[0]);
26718
+ return i === j || !otherCoord || !number.equal(coord[0], otherCoord[0]);
26978
26719
  });
26979
26720
 
26980
26721
  // Evaluate this criteria before per-point
@@ -27282,11 +27023,22 @@ const getPromptJSON$f = rendererJSON => {
27282
27023
 
27283
27024
  // The `group` widget is basically a widget hosting a full Perseus system in
27284
27025
  // it. As such, scoring a group means scoring all widgets it contains.
27285
- function scoreGroup(userInput, options, strings, locale) {
27286
- const scores = scoreWidgetsFunctional(options.widgets, Object.keys(options.widgets), userInput, strings, locale);
27026
+ function scoreGroup(userInput, rubric, strings, locale) {
27027
+ const scores = scoreWidgetsFunctional(rubric.widgets, Object.keys(rubric.widgets), userInput, strings, locale);
27287
27028
  return flattenScores(scores);
27288
27029
  }
27289
27030
 
27031
+ function validateGroup(userInput, validationData, strings, locale) {
27032
+ const emptyWidgets = emptyWidgetsFunctional(validationData.widgets, Object.keys(validationData.widgets), userInput, strings, locale);
27033
+ if (emptyWidgets.length === 0) {
27034
+ return null;
27035
+ }
27036
+ return {
27037
+ type: "invalid",
27038
+ message: null
27039
+ };
27040
+ }
27041
+
27290
27042
  // exports has no 'transform'
27291
27043
 
27292
27044
  class Group extends React.Component {
@@ -27442,8 +27194,11 @@ var Group$1 = {
27442
27194
  widget: Group,
27443
27195
  traverseChildWidgets: traverseChildWidgets,
27444
27196
  // TODO(LEMS-2656): remove TS suppression
27445
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
27197
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGroupUserInput'.
27446
27198
  scorer: scoreGroup,
27199
+ // TODO(LEMS-2656): remove TS suppression
27200
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGroupUserInput'.
27201
+ validator: validateGroup,
27447
27202
  hidden: true,
27448
27203
  isLintable: true
27449
27204
  };
@@ -29779,7 +29534,7 @@ const styles$d = StyleSheet.create({
29779
29534
  }
29780
29535
  });
29781
29536
 
29782
- const _excluded$3 = ["onFormatChange", "checkValidity", "useArrowKeys"];
29537
+ const _excluded$1 = ["onFormatChange", "checkValidity", "useArrowKeys"];
29783
29538
  const {
29784
29539
  firstNumericalParse,
29785
29540
  captureScratchpadTouchStart
@@ -29917,7 +29672,7 @@ class NumberInput extends React.Component {
29917
29672
  };
29918
29673
  }
29919
29674
  componentDidUpdate(prevProps) {
29920
- if (!number$1.equal(this.getValue(), this.props.value)) {
29675
+ if (!number.equal(this.getValue(), this.props.value)) {
29921
29676
  this._setValue(this.props.value, this.state.format);
29922
29677
  }
29923
29678
  }
@@ -29933,7 +29688,7 @@ class NumberInput extends React.Component {
29933
29688
  classes = classes + " " + this.props.className;
29934
29689
  }
29935
29690
  const _this$props = this.props,
29936
- restProps = _objectWithoutPropertiesLoose(_this$props, _excluded$3);
29691
+ restProps = _objectWithoutPropertiesLoose(_this$props, _excluded$1);
29937
29692
  return /*#__PURE__*/React.createElement("input", _extends({}, restProps, {
29938
29693
  className: classes,
29939
29694
  type: "text",
@@ -30331,6 +30086,96 @@ function getInterceptStringForLine(line, strings, locale) {
30331
30086
  yIntercept: srFormatNumber(yIntercept, locale)
30332
30087
  });
30333
30088
  }
30089
+ function getCoordQuadrant(coord) {
30090
+ const [unroundedX, unroundedY] = coord;
30091
+ const x = Number(unroundedX.toFixed(3));
30092
+ const y = Number(unroundedY.toFixed(3));
30093
+ if (x === 0 && y === 0) {
30094
+ return "origin";
30095
+ }
30096
+ if (y === 0) {
30097
+ return "x-axis";
30098
+ }
30099
+ if (x === 0) {
30100
+ return "y-axis";
30101
+ }
30102
+ if (x > 0 && y > 0) {
30103
+ return 1;
30104
+ }
30105
+ if (x < 0 && y > 0) {
30106
+ return 2;
30107
+ }
30108
+ if (x < 0 && y < 0) {
30109
+ return 3;
30110
+ }
30111
+ return 4;
30112
+ }
30113
+ function getQuadraticVertexString(vertex, strings) {
30114
+ const location = getCoordQuadrant(vertex);
30115
+ switch (location) {
30116
+ case "origin":
30117
+ return strings.srQuadraticGraphVertexOrigin;
30118
+ case "x-axis":
30119
+ return strings.srQuadraticGraphVertexXAxis;
30120
+ case "y-axis":
30121
+ return strings.srQuadraticGraphVertexYAxis;
30122
+ default:
30123
+ return strings.srQuadraticGraphVertexQuadrant({
30124
+ quadrant: location
30125
+ });
30126
+ }
30127
+ }
30128
+ function getQuadraticPointString(pointNumber, coord, strings, locale) {
30129
+ const location = getCoordQuadrant(coord);
30130
+ const [x, y] = coord;
30131
+ switch (location) {
30132
+ case "origin":
30133
+ return strings.srQuadraticPointOrigin({
30134
+ pointNumber: pointNumber
30135
+ });
30136
+ case "x-axis":
30137
+ case "y-axis":
30138
+ return strings.srQuadraticPointAxis({
30139
+ pointNumber: pointNumber,
30140
+ x: srFormatNumber(x, locale),
30141
+ y: srFormatNumber(y, locale)
30142
+ });
30143
+ default:
30144
+ // return `Point ${pointNumber} on parabola in quadrant ${location} at ${srFormatNumber(x, locale)} comma ${srFormatNumber(y, locale)}.`;
30145
+ return strings.srQuadraticPointQuadrant({
30146
+ pointNumber: pointNumber,
30147
+ quadrant: location,
30148
+ x: srFormatNumber(x, locale),
30149
+ y: srFormatNumber(y, locale)
30150
+ });
30151
+ }
30152
+ }
30153
+ function getQuadraticXIntercepts(
30154
+ // Coefficients of the quadratic equation
30155
+ a, b, c) {
30156
+ // If a is 0, the equation is a line and has maybe only one x-intercept.
30157
+ if (a === 0) {
30158
+ // If b is 0, the equation a horizontal line and has no x-intercepts.
30159
+ if (b === 0) {
30160
+ return [];
30161
+ }
30162
+ return [-c / b];
30163
+ }
30164
+
30165
+ // Find the x-intercepts by plugging in y = 0 into the quadratic equation.
30166
+ // 0 = ax^2 + bx + c
30167
+ // x = (-b ± sqrt(b^2 - 4ac)) / 2a
30168
+ const discriminant = b * b - 4 * a * c;
30169
+ if (discriminant < 0) {
30170
+ return [];
30171
+ }
30172
+ const x1 = (-b + Math.sqrt(discriminant)) / (2 * a);
30173
+ const x2 = (-b - Math.sqrt(discriminant)) / (2 * a);
30174
+ if (x1 === x2) {
30175
+ return [x1];
30176
+ }
30177
+ return [x1, x2];
30178
+ }
30334
30179
 
30335
30180
  const {
30336
30181
  calculateAngleInDegrees: calculateAngleInDegrees$2
@@ -30748,12 +30593,12 @@ const MafsCssTransformWrapper = ({
30748
30593
  }
30749
30594
  }, children);
30750
30595
 
30751
- const _excluded$2 = ["children"];
30596
+ const _excluded = ["children"];
30752
30597
  const TextLabel = _ref => {
30753
30598
  let {
30754
30599
  children
30755
30600
  } = _ref,
30756
- rest = _objectWithoutPropertiesLoose(_ref, _excluded$2);
30601
+ rest = _objectWithoutPropertiesLoose(_ref, _excluded);
30757
30602
  return /*#__PURE__*/React__default.createElement(Text$1, _extends({
30758
30603
  size: 16,
30759
30604
  svgTextProps: {
@@ -32135,7 +31980,9 @@ function renderLinearGraph(state, dispatch) {
32135
31980
  graphState: state,
32136
31981
  dispatch: dispatch
32137
31982
  }),
32138
- interactiveElementsDescription: null
31983
+ interactiveElementsDescription: /*#__PURE__*/React.createElement(LinearGraphDescription, {
31984
+ state: state
31985
+ })
32139
31986
  };
32140
31987
  }
32141
31988
  const LinearGraph = (props, key) => {
@@ -32154,33 +32001,29 @@ const LinearGraph = (props, key) => {
32154
32001
  const interceptDescriptionId = id + "-intercept";
32155
32002
  const slopeDescriptionId = id + "-slope";
32156
32003
 
32157
- // Aria label strings
32158
- const linearGraphPointsDescription = strings.srLinearGraphPoints({
32159
- point1X: srFormatNumber(line[0][0], locale),
32160
- point1Y: srFormatNumber(line[0][1], locale),
32161
- point2X: srFormatNumber(line[1][0], locale),
32162
- point2Y: srFormatNumber(line[1][1], locale)
32163
- });
32164
- const grabHandleAriaLabel = strings.srLinearGrabHandle({
32165
- point1X: srFormatNumber(line[0][0], locale),
32166
- point1Y: srFormatNumber(line[0][1], locale),
32167
- point2X: srFormatNumber(line[1][0], locale),
32168
- point2Y: srFormatNumber(line[1][1], locale)
32004
+ // Aria strings
32005
+ const {
32006
+ srLinearGraph,
32007
+ srLinearGraphPoints,
32008
+ srLinearGrabHandle,
32009
+ slopeString,
32010
+ interceptString
32011
+ } = describeLinearGraph(props.graphState, {
32012
+ strings,
32013
+ locale
32169
32014
  });
32170
- const slopeString = getSlopeStringForLine(line, strings);
32171
- const interceptString = getInterceptStringForLine(line, strings, locale);
32172
32015
 
32173
32016
  // Linear graphs only have one line
32174
32017
  // (LEMS-2050): Update the reducer so that we have a separate action for moving one line
32175
32018
  // and another action for moving multiple lines
32176
32019
  return /*#__PURE__*/React.createElement("g", {
32177
32020
  // Outer line minimal description
32178
- "aria-label": strings.srLinearGraph,
32021
+ "aria-label": srLinearGraph,
32179
32022
  "aria-describedby": `${pointsDescriptionId} ${interceptDescriptionId} ${slopeDescriptionId}`
32180
32023
  }, /*#__PURE__*/React.createElement(MovableLine, {
32181
32024
  key: 0,
32182
32025
  ariaLabels: {
32183
- grabHandleAriaLabel: grabHandleAriaLabel
32026
+ grabHandleAriaLabel: srLinearGrabHandle
32184
32027
  },
32185
32028
  ariaDescribedBy: `${interceptDescriptionId} ${slopeDescriptionId}`,
32186
32029
  points: line,
@@ -32196,7 +32039,7 @@ const LinearGraph = (props, key) => {
32196
32039
  }), /*#__PURE__*/React.createElement("g", {
32197
32040
  id: pointsDescriptionId,
32198
32041
  style: a11y.srOnly
32199
- }, linearGraphPointsDescription), /*#__PURE__*/React.createElement("g", {
32042
+ }, srLinearGraphPoints), /*#__PURE__*/React.createElement("g", {
32200
32043
  id: interceptDescriptionId,
32201
32044
  style: a11y.srOnly
32202
32045
  }, interceptString), /*#__PURE__*/React.createElement("g", {
@@ -32204,6 +32047,55 @@ const LinearGraph = (props, key) => {
32204
32047
  style: a11y.srOnly
32205
32048
  }, slopeString));
32206
32049
  };
32050
+ function LinearGraphDescription({
32051
+ state
32052
+ }) {
32053
+ // The reason that LinearGraphDescription is a component (rather than a
32054
+ // function that returns a string) is because it needs to use a
32055
+ // hook: `usePerseusI18n`.
32056
+ const i18n = usePerseusI18n();
32057
+ const strings = describeLinearGraph(state, i18n);
32058
+ return strings.srLinearInteractiveElement;
32059
+ }
32060
+
32061
+ // Exported for testing
32062
+ function describeLinearGraph(state, i18n) {
32063
+ const {
32064
+ coords: line
32065
+ } = state;
32066
+ const {
32067
+ strings,
32068
+ locale
32069
+ } = i18n;
32070
+
32071
+ // Aria label strings
32072
+ const srLinearGraph = strings.srLinearGraph;
32073
+ const srLinearGraphPoints = strings.srLinearGraphPoints({
32074
+ point1X: srFormatNumber(line[0][0], locale),
32075
+ point1Y: srFormatNumber(line[0][1], locale),
32076
+ point2X: srFormatNumber(line[1][0], locale),
32077
+ point2Y: srFormatNumber(line[1][1], locale)
32078
+ });
32079
+ const srLinearGrabHandle = strings.srLinearGrabHandle({
32080
+ point1X: srFormatNumber(line[0][0], locale),
32081
+ point1Y: srFormatNumber(line[0][1], locale),
32082
+ point2X: srFormatNumber(line[1][0], locale),
32083
+ point2Y: srFormatNumber(line[1][1], locale)
32084
+ });
32085
+ const slopeString = getSlopeStringForLine(line, strings);
32086
+ const interceptString = getInterceptStringForLine(line, strings, locale);
32087
+ const srLinearInteractiveElement = strings.srInteractiveElements({
32088
+ elements: [srLinearGraph, srLinearGraphPoints].join(" ")
32089
+ });
32090
+ return {
32091
+ srLinearGraph,
32092
+ srLinearGraphPoints,
32093
+ srLinearGrabHandle,
32094
+ slopeString,
32095
+ interceptString,
32096
+ srLinearInteractiveElement
32097
+ };
32098
+ }
32207
32099
 
32208
32100
  function renderLinearSystemGraph(state, dispatch) {
32209
32101
  return {
@@ -32752,13 +32644,13 @@ const hasFocusVisible = element => {
32752
32644
  }
32753
32645
  };
32754
32646
 
32755
- function renderQuadraticGraph(state, dispatch) {
32647
+ function renderQuadraticGraph(state, dispatch, i18n) {
32756
32648
  return {
32757
32649
  graph: /*#__PURE__*/React.createElement(QuadraticGraph, {
32758
32650
  graphState: state,
32759
32651
  dispatch: dispatch
32760
32652
  }),
32761
- interactiveElementsDescription: null
32653
+ interactiveElementsDescription: getQuadraticGraphDescription(state, i18n)
32762
32654
  };
32763
32655
  }
32764
32656
  function QuadraticGraph(props) {
@@ -32769,6 +32661,14 @@ function QuadraticGraph(props) {
32769
32661
  const {
32770
32662
  coords
32771
32663
  } = graphState;
32664
+ const {
32665
+ strings,
32666
+ locale
32667
+ } = usePerseusI18n();
32668
+ const id = React.useId();
32669
+ const quadraticDirectionId = id + "-direction";
32670
+ const quadraticVertexId = id + "-vertex";
32671
+ const quadraticInterceptsId = id + "-intercepts";
32772
32672
 
32773
32673
  // The coefficients are used to calculate the quadratic equation, plot the graph, and to indicate
32774
32674
  // to content creators the currently selected "correct answer". ex: y = 0.200x^2 + 0.000x + 0.000
@@ -32784,15 +32684,46 @@ function QuadraticGraph(props) {
32784
32684
 
32785
32685
  // Calculate the y value based on the current x value
32786
32686
  const y = x => (a * x + b) * x + c;
32787
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Plot$2.OfX, {
32687
+
32688
+ // Aria strings
32689
+ const {
32690
+ srQuadraticGraph,
32691
+ srQuadraticDirection,
32692
+ srQuadraticVertex,
32693
+ srQuadraticXIntercepts,
32694
+ srQuadraticYIntercept
32695
+ } = describeQuadraticGraph(graphState, {
32696
+ strings,
32697
+ locale
32698
+ });
32699
+ return /*#__PURE__*/React.createElement("g", {
32700
+ // Outer graph minimal description
32701
+ "aria-label": srQuadraticGraph,
32702
+ "aria-describedby": `${quadraticDirectionId} ${quadraticVertexId} ${quadraticInterceptsId}`
32703
+ }, /*#__PURE__*/React.createElement(Plot$2.OfX, {
32788
32704
  y: y,
32789
32705
  color: color.blue
32790
- }), coords.map((coord, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
32791
- key: "point-" + i,
32792
- point: coord,
32793
- sequenceNumber: i + 1,
32794
- onMove: destination => dispatch(actions.quadratic.movePoint(i, destination))
32795
- })));
32706
+ }), coords.map((coord, i) => {
32707
+ const srQuadraticPoint = getQuadraticPointString(i + 1, coord, strings, locale);
32708
+ // Vertex might not exist if the quadratic graph is a line
32709
+ const srVertex = srQuadraticVertex ? ` ${srQuadraticVertex}` : "";
32710
+ return /*#__PURE__*/React.createElement(MovablePoint$1, {
32711
+ key: "point-" + i,
32712
+ ariaLabel: `${srQuadraticPoint}${srVertex}`,
32713
+ point: coord,
32714
+ sequenceNumber: i + 1,
32715
+ onMove: destination => dispatch(actions.quadratic.movePoint(i, destination))
32716
+ });
32717
+ }), srQuadraticDirection && /*#__PURE__*/React.createElement("g", {
32718
+ id: quadraticDirectionId,
32719
+ style: a11y.srOnly
32720
+ }, srQuadraticDirection), srQuadraticVertex && /*#__PURE__*/React.createElement("g", {
32721
+ id: quadraticVertexId,
32722
+ style: a11y.srOnly
32723
+ }, srQuadraticVertex), /*#__PURE__*/React.createElement("g", {
32724
+ id: quadraticInterceptsId,
32725
+ style: a11y.srOnly
32726
+ }, srQuadraticXIntercepts ? `${srQuadraticXIntercepts} ${srQuadraticYIntercept}` : `${srQuadraticYIntercept}`));
32796
32727
  }
32797
32728
 
32798
32729
  // Get the quadratic coefficients from the 3 control points
@@ -32815,6 +32746,60 @@ const getQuadraticCoefficients$2 = coords => {
32815
32746
  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;
32816
32747
  return [a, b, c];
32817
32748
  };
32749
+ function getQuadraticGraphDescription(state, i18n) {
32750
+ const strings = describeQuadraticGraph(state, i18n);
32751
+ return strings.srQuadraticInteractiveElements;
32752
+ }
32753
+ // Exported for testing
32754
+ function describeQuadraticGraph(state, i18n) {
32755
+ const {
32756
+ strings,
32757
+ locale
32758
+ } = i18n;
32759
+ const coeffs = getQuadraticCoefficients$2(state.coords);
32760
+ const [a, b, c] = coeffs != null ? coeffs : [0, 0, 0];
32761
+ const vertex = [-b / (2 * a), c - b * b / (4 * a)];
32762
+ const xIntercepts = getQuadraticXIntercepts(a, b, c);
32763
+
32764
+ // Aria label strings
32765
+ const srQuadraticGraph = strings.srQuadraticGraph;
32766
+ const srQuadraticFaceUp = strings.srQuadraticFaceUp;
32767
+ const srQuadraticFaceDown = strings.srQuadraticFaceDown;
32768
+ // The graph only has a direction if it is not a line.
32769
+ const srQuadraticDirection = a === 0 ? undefined : a > 0 ? srQuadraticFaceUp : srQuadraticFaceDown;
32770
+ // Only describe vertex if the quadratic graph is not a line.
32771
+ // (Undefined means the quadratic graph is a line and has no vertex.)
32772
+ const srQuadraticVertex = a !== 0 ? getQuadraticVertexString(vertex, strings) : undefined;
32773
+ // Undefined means the quadratic graph has no x-intercepts,
32774
+ // such as when the graph is a horizontal line.
32775
+ const srQuadraticXIntercepts = xIntercepts.length === 2 ? strings.srQuadraticTwoXIntercepts({
32776
+ intercept1: srFormatNumber(xIntercepts[0], locale),
32777
+ intercept2: srFormatNumber(xIntercepts[1], locale)
32778
+ }) : xIntercepts.length === 1 ? strings.srQuadraticOneXIntercept({
32779
+ intercept: srFormatNumber(xIntercepts[0], locale)
32780
+ }) : undefined;
32781
+ const srQuadraticYIntercept = strings.srQuadraticYIntercept({
32782
+ intercept: srFormatNumber(c, locale)
32783
+ });
32784
+ const srQuadraticInteractiveElements = strings.srInteractiveElements({
32785
+ elements: strings.srQuadraticInteractiveElements({
32786
+ point1X: srFormatNumber(state.coords[0][0], locale),
32787
+ point1Y: srFormatNumber(state.coords[0][1], locale),
32788
+ point2X: srFormatNumber(state.coords[1][0], locale),
32789
+ point2Y: srFormatNumber(state.coords[1][1], locale),
32790
+ point3X: srFormatNumber(state.coords[2][0], locale),
32791
+ point3Y: srFormatNumber(state.coords[2][1], locale)
32792
+ })
32793
+ });
32794
+ return {
32795
+ srQuadraticGraph,
32796
+ srQuadraticDirection,
32797
+ srQuadraticVertex,
32798
+ srQuadraticXIntercepts,
32799
+ srQuadraticYIntercept,
32800
+ srQuadraticInteractiveElements
32801
+ };
32802
+ }
32818
32803
 
32819
32804
  function renderRayGraph(state, dispatch) {
32820
32805
  return {
@@ -32940,17 +32925,69 @@ function renderSegmentGraph(state, dispatch) {
32940
32925
  graphState: state,
32941
32926
  dispatch: dispatch
32942
32927
  }),
32943
- interactiveElementsDescription: null
32928
+ interactiveElementsDescription: /*#__PURE__*/React.createElement(SegmentGraphDescription, {
32929
+ state: state
32930
+ })
32944
32931
  };
32945
32932
  }
32946
- const SegmentGraph = props => {
32947
- const {
32948
- dispatch
32949
- } = props;
32933
+ const SegmentGraph = ({
32934
+ dispatch,
32935
+ graphState
32936
+ }) => {
32950
32937
  const {
32951
32938
  coords: segments
32952
- } = props.graphState;
32953
- return /*#__PURE__*/React.createElement(React.Fragment, null, segments == null ? void 0 : segments.map((segment, i) => /*#__PURE__*/React.createElement(MovableLine, {
32939
+ } = graphState;
32940
+ const {
32941
+ strings,
32942
+ locale
32943
+ } = usePerseusI18n();
32944
+ const segmentUniqueId = React.useId();
32945
+ const lengthDescriptionId = segmentUniqueId + "-length";
32946
+ const wholeGraphDescriptionId = segmentUniqueId + "-whole-graph";
32947
+ function getWholeSegmentGraphAriaLabel() {
32948
+ return (segments == null ? void 0 : segments.length) > 1 ? strings.srMultipleSegmentGraphAriaLabel({
32949
+ countOfSegments: segments.length
32950
+ }) : strings.srSingleSegmentGraphAriaLabel;
32951
+ }
32952
+ const wholeSegmentGraphAriaLabel = getWholeSegmentGraphAriaLabel();
32953
+ function getIndividualSegmentAriaLabel(segment, index) {
32954
+ if (segments.length === 1) {
32955
+ return strings.srSingleSegmentLabel({
32956
+ point1X: srFormatNumber(segments[0][0][X], locale),
32957
+ point1Y: srFormatNumber(segments[0][0][Y], locale),
32958
+ point2X: srFormatNumber(segments[0][1][X], locale),
32959
+ point2Y: srFormatNumber(segments[0][1][Y], locale)
32960
+ });
32961
+ }
32962
+ return strings.srMultipleSegmentIndividualLabel({
32963
+ point1X: srFormatNumber(segment[0][X], locale),
32964
+ point1Y: srFormatNumber(segment[0][Y], locale),
32965
+ point2X: srFormatNumber(segment[1][X], locale),
32966
+ point2Y: srFormatNumber(segment[1][Y], locale),
32967
+ indexOfSegment: index + 1
32968
+ });
32969
+ }
32970
+ function getWholeSegmentGraphAriaDescription() {
32971
+ return segments.map((segment, index) => getIndividualSegmentAriaLabel(segment, index)).join(" ");
32972
+ }
32973
+ function formatSegment(endpointNumber, x, y, index) {
32974
+ const segObj = {
32975
+ endpointNumber: endpointNumber,
32976
+ x: srFormatNumber(x, locale),
32977
+ y: srFormatNumber(y, locale)
32978
+ };
32979
+ return segments.length > 1 ? strings.srMultipleSegmentGraphEndpointAriaLabel(_extends({}, segObj, {
32980
+ indexOfSegment: index
32981
+ })) : strings.srSingleSegmentGraphEndpointAriaLabel(segObj);
32982
+ }
32983
+ return /*#__PURE__*/React.createElement("g", {
32984
+ "aria-label": wholeSegmentGraphAriaLabel,
32985
+ "aria-describedby": wholeGraphDescriptionId
32986
+ }, segments == null ? void 0 : segments.map((segment, i) => /*#__PURE__*/React.createElement("g", {
32987
+ "aria-label": getIndividualSegmentAriaLabel(segment, i),
32988
+ "aria-describedby": lengthDescriptionId,
32989
+ key: `${segmentUniqueId}-${i}`
32990
+ }, /*#__PURE__*/React.createElement(MovableLine, {
32954
32991
  key: i,
32955
32992
  points: segment,
32956
32993
  onMoveLine: delta => {
@@ -32958,9 +32995,57 @@ const SegmentGraph = props => {
32958
32995
  },
32959
32996
  onMovePoint: (endpointIndex, destination) => {
32960
32997
  dispatch(actions.segment.movePointInFigure(i, endpointIndex, destination));
32998
+ },
32999
+ ariaLabels: {
33000
+ point1AriaLabel: formatSegment(1, segment[0][X], segment[0][Y], i + 1),
33001
+ point2AriaLabel: formatSegment(2, segment[1][X], segment[1][Y], i + 1),
33002
+ grabHandleAriaLabel: strings.srSegmentGrabHandle({
33003
+ point1X: srFormatNumber(segment[0][X], locale),
33004
+ point1Y: srFormatNumber(segment[0][Y], locale),
33005
+ point2X: srFormatNumber(segment[1][X], locale),
33006
+ point2Y: srFormatNumber(segment[1][Y], locale)
33007
+ })
32961
33008
  }
32962
- })));
33009
+ }), /*#__PURE__*/React.createElement("g", {
33010
+ id: lengthDescriptionId,
33011
+ style: a11y.srOnly
33012
+ }, strings.srSegmentLength({
33013
+ length: srFormatNumber(getLengthOfSegment(segment), locale)
33014
+ })))), /*#__PURE__*/React.createElement("g", {
33015
+ style: a11y.srOnly,
33016
+ id: wholeGraphDescriptionId
33017
+ }, getWholeSegmentGraphAriaDescription()));
32963
33018
  };
33019
+ function getLengthOfSegment(segment) {
33020
+ return point.distanceToPoint(...segment);
33021
+ }
33022
+ function SegmentGraphDescription({
33023
+ state
33024
+ }) {
33025
+ // The reason that SegmentGraphDescription is a component (rather than a
33026
+ // function that returns a string) is because it needs to use a
33027
+ // hook: `usePerseusI18n`.
33028
+ const i18n = usePerseusI18n();
33029
+ return describeSegmentGraph(state, i18n);
33030
+ }
33031
+
33032
+ // Exported for testing
33033
+ function describeSegmentGraph(state, i18n) {
33034
+ const {
33035
+ strings,
33036
+ locale
33037
+ } = i18n;
33038
+ const segmentDescriptions = state.coords.map(([point1, point2], index) => strings.srMultipleSegmentIndividualLabel({
33039
+ point1X: srFormatNumber(point1[X], locale),
33040
+ point1Y: srFormatNumber(point1[Y], locale),
33041
+ point2X: srFormatNumber(point2[X], locale),
33042
+ point2Y: srFormatNumber(point2[Y], locale),
33043
+ indexOfSegment: index + 1
33044
+ }));
33045
+ return strings.srInteractiveElements({
33046
+ elements: segmentDescriptions.join(" ")
33047
+ });
33048
+ }
32964
33049
 
32965
33050
  function renderSinusoidGraph(state, dispatch) {
32966
33051
  return {
@@ -33236,9 +33321,10 @@ const MafsGraph = props => {
33236
33321
  x: viewboxX,
33237
33322
  y: viewboxY
33238
33323
  };
33324
+ const i18n = usePerseusI18n();
33239
33325
  const {
33240
33326
  strings
33241
- } = usePerseusI18n();
33327
+ } = i18n;
33242
33328
  const interactionPrompt = isUnlimitedGraphState(state) && state.showKeyboardInteractionInvitation;
33243
33329
  useOnMountEffect(() => {
33244
33330
  analytics.onAnalyticsEvent({
@@ -33255,7 +33341,8 @@ const MafsGraph = props => {
33255
33341
  interactiveElementsDescription
33256
33342
  } = renderGraphElements({
33257
33343
  state,
33258
- dispatch
33344
+ dispatch,
33345
+ i18n
33259
33346
  });
33260
33347
  return /*#__PURE__*/React.createElement(GraphConfigContext.Provider, {
33261
33348
  value: {
@@ -33633,7 +33720,8 @@ const calculateNestedSVGCoords = (range, width, height) => {
33633
33720
  const renderGraphElements = props => {
33634
33721
  const {
33635
33722
  state,
33636
- dispatch
33723
+ dispatch,
33724
+ i18n
33637
33725
  } = props;
33638
33726
  const {
33639
33727
  type
@@ -33656,7 +33744,7 @@ const renderGraphElements = props => {
33656
33744
  case "circle":
33657
33745
  return renderCircleGraph(state, dispatch);
33658
33746
  case "quadratic":
33659
- return renderQuadraticGraph(state, dispatch);
33747
+ return renderQuadraticGraph(state, dispatch, i18n);
33660
33748
  case "sinusoid":
33661
33749
  return renderSinusoidGraph(state, dispatch);
33662
33750
  case "none":
@@ -35188,7 +35276,7 @@ class LegacyInteractiveGraph extends React.Component {
35188
35276
  };
35189
35277
  this.setTrashCanVisibility = opacity => {
35190
35278
  const graphie = this.graphie;
35191
- if (number$1.equal(opacity, 0)) {
35279
+ if (number.equal(opacity, 0)) {
35192
35280
  if (this.trashCan) {
35193
35281
  this.trashCan.remove();
35194
35282
  this.trashCan = null;
@@ -37238,19 +37326,21 @@ class LabelImage extends React.Component {
37238
37326
  this._mounted = false;
37239
37327
  }
37240
37328
  getUserInput() {
37241
- const {
37242
- markers
37243
- } = this.props;
37244
37329
  return {
37245
- markers
37330
+ markers: this.props.markers.map(marker => ({
37331
+ selected: marker.selected,
37332
+ label: marker.label
37333
+ }))
37246
37334
  };
37247
37335
  }
37248
37336
  getPromptJSON() {
37249
37337
  return getPromptJSON$a(this.props, this.getUserInput());
37250
37338
  }
37251
37339
 
37252
- // TODO(LEMS-2544): Investigate impact on scoring; possibly pull out &/or remove rubric parameter.
37253
- showRationalesForCurrentlySelectedChoices(rubric) {
37340
+ // TODO(LEMS-2544): Investigate impact on scoring
37341
+ // Also consider how scoreMarker is being called as it seems to require the marker.answers property.
37342
+ // Removed rubric parameter, but it gets a full widget options object from the renderer
37343
+ showRationalesForCurrentlySelectedChoices() {
37254
37344
  const {
37255
37345
  markers
37256
37346
  } = this.props;
@@ -37258,7 +37348,7 @@ class LabelImage extends React.Component {
37258
37348
  onChange
37259
37349
  } = this.props;
37260
37350
  const updatedMarkers = markers.map(marker => {
37261
- const score = scoreLabelImageMarker(marker);
37351
+ const score = scoreLabelImageMarker(marker.selected, marker.answers);
37262
37352
  return _extends({}, marker, {
37263
37353
  // Reveal correctness state for markers with answers.
37264
37354
  showCorrectness: score.hasAnswers ? score.isCorrect ? "correct" : "incorrect" : undefined
@@ -37364,6 +37454,7 @@ class LabelImage extends React.Component {
37364
37454
  selected: selected.length ? selected : undefined
37365
37455
  }));
37366
37456
  }
37457
+ // TODO(LEMS-2723): Investigate if possible to change this to not require answers
37367
37458
  renderMarkers() {
37368
37459
  const {
37369
37460
  markers,
@@ -37405,7 +37496,7 @@ class LabelImage extends React.Component {
37405
37496
  bottom: "top"
37406
37497
  }[markerPosition];
37407
37498
  }
37408
- const score = scoreLabelImageMarker(marker);
37499
+ const score = scoreLabelImageMarker(marker.selected, marker.answers);
37409
37500
  // Once the question is answered, show markers
37410
37501
  // with correct answers, otherwise passthrough
37411
37502
  // the correctness state.
@@ -38950,14 +39041,16 @@ var Matrix$1 = {
38950
39041
  isLintable: true,
38951
39042
  // TODO(LEMS-2656): remove TS suppression
38952
39043
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
38953
- scorer: scoreMatrix
39044
+ scorer: scoreMatrix,
39045
+ // TODO(LEMS-2656): remove TS suppression
39046
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
39047
+ validator: validateMatrix
38954
39048
  };
38955
39049
 
38956
39050
  const getPromptJSON$7 = () => {
38957
39051
  return getUnsupportedPromptJSON("measurer");
38958
39052
  };
38959
39053
 
38960
- const _excluded$1 = ["imageUrl", "imageTop", "imageLeft"];
38961
39054
  const defaultImage = {
38962
39055
  url: null,
38963
39056
  top: 0,
@@ -39062,33 +39155,13 @@ Measurer.defaultProps = {
39062
39155
  rulerPixels: 40,
39063
39156
  rulerLength: 10
39064
39157
  };
39065
- const propUpgrades = {
39066
- "1": v0props => {
39067
- const {
39068
- imageUrl,
39069
- imageTop,
39070
- imageLeft
39071
- } = v0props,
39072
- rest = _objectWithoutPropertiesLoose(v0props, _excluded$1);
39073
- return _extends({}, rest, {
39074
- image: {
39075
- url: imageUrl,
39076
- top: imageTop,
39077
- left: imageLeft
39078
- }
39079
- });
39080
- }
39081
- };
39082
39158
  var Measurer$1 = {
39083
39159
  name: "measurer",
39084
39160
  displayName: "Measurer",
39085
39161
  hidden: true,
39086
39162
  widget: Measurer,
39087
- version: {
39088
- major: 1,
39089
- minor: 0
39090
- },
39091
- propUpgrades: propUpgrades,
39163
+ version: measurerLogic.version,
39164
+ propUpgrades: measurerLogic.widgetOptionsUpgrades,
39092
39165
  // TODO: things that aren't interactive shouldn't need scoring functions
39093
39166
  scorer: () => scoreNoop(1)
39094
39167
  };
@@ -39661,10 +39734,10 @@ function parseBondModifier(smiles, ctx) {
39661
39734
  const firstChar = smiles[0];
39662
39735
  const rest = smiles.slice(1);
39663
39736
  if (firstChar === "=") {
39664
- return parse$2(rest, _mset(ctx, ["bond", "bondType"], "double"));
39737
+ return parse$1(rest, _mset(ctx, ["bond", "bondType"], "double"));
39665
39738
  }
39666
39739
  if (firstChar === "#") {
39667
- return parse$2(rest, _mset(ctx, ["bond", "bondType"], "triple"));
39740
+ return parse$1(rest, _mset(ctx, ["bond", "bondType"], "triple"));
39668
39741
  }
39669
39742
  throw new ParseError$1("Invalid character: " + firstChar);
39670
39743
  }
@@ -39723,8 +39796,8 @@ function parseParenthesizedExpression(smiles, ctx) {
39723
39796
  idx: newCtx.idx.concat([[inBranchIdx, 0]]),
39724
39797
  parens: newCtx.parens.concat("(")
39725
39798
  });
39726
- const parenExpr = parse$2(rest, parenCtx);
39727
- const remainder = parse$2(sliceFromMatchingCloseParen(rest, ["("]), newCtx);
39799
+ const parenExpr = parse$1(rest, parenCtx);
39800
+ const remainder = parse$1(sliceFromMatchingCloseParen(rest, ["("]), newCtx);
39728
39801
  return [parenExpr].concat(remainder);
39729
39802
  }
39730
39803
  if (firstChar === ")") {
@@ -39788,7 +39861,7 @@ function parseAtom(smiles, ctx) {
39788
39861
 
39789
39862
  // increment the atom counter and reset the branch counter
39790
39863
  const newCtx = _mset(ctx, ["idx", ctx.idx.length - 1], [1 + ctx.idx[ctx.idx.length - 1][0], 0]);
39791
- let restOfMolecule = parse$2(rest, _mset(newCtx, ["bond", "bondType"], "single"));
39864
+ let restOfMolecule = parse$1(rest, _mset(newCtx, ["bond", "bondType"], "single"));
39792
39865
  if (!Array.isArray(restOfMolecule) && !!restOfMolecule) {
39793
39866
  //TODO(colin): fix this awkwardness.
39794
39867
  restOfMolecule = [restOfMolecule];
@@ -39827,7 +39900,7 @@ function isModifierChar(s) {
39827
39900
  * ParseError: if the input is not valid SMILES or contains features not
39828
39901
  * yet implemented.
39829
39902
  */
39830
- function parse$2(smiles, ctx) {
39903
+ function parse$1(smiles, ctx) {
39831
39904
  if (!validate(smiles)) {
39832
39905
  throw new ParseError$1("Invalid input.");
39833
39906
  }
@@ -39852,7 +39925,7 @@ function parse$2(smiles, ctx) {
39852
39925
  return parseParenthesizedExpression(smiles, ctx);
39853
39926
  }
39854
39927
  var SmilesParser = {
39855
- parse: parse$2,
39928
+ parse: parse$1,
39856
39929
  ParseError: ParseError$1
39857
39930
  };
39858
39931
 
@@ -39860,7 +39933,7 @@ var SmilesParser = {
39860
39933
  const {
39861
39934
  layout
39862
39935
  } = MoleculeLayout;
39863
- const parse$1 = SmilesParser.parse;
39936
+ const parse = SmilesParser.parse;
39864
39937
  const ParseError = SmilesParser.ParseError;
39865
39938
  const borderSize = 30;
39866
39939
  class Molecule extends React.Component {
@@ -39874,7 +39947,7 @@ class Molecule extends React.Component {
39874
39947
  try {
39875
39948
  this.setState({
39876
39949
  // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
39877
- parsedSmiles: parse$1(smiles),
39950
+ parsedSmiles: parse(smiles),
39878
39951
  error: null
39879
39952
  });
39880
39953
  } catch (e) {
@@ -40093,7 +40166,7 @@ const TickMarks = Graphie.createSimpleClass((graphie, props) => {
40093
40166
  const x = range[0] + i * tickStep;
40094
40167
  fractions.push(x);
40095
40168
  }
40096
- const getDenom = x => number$1.toFraction(x)[1];
40169
+ const getDenom = x => number.toFraction(x)[1];
40097
40170
  const denoms = _.map(fractions, getDenom);
40098
40171
  base = _.reduce(denoms, (x, y) => KhanMath.getLCM(x, y));
40099
40172
  } else {
@@ -40156,7 +40229,7 @@ class NumberLine extends React.Component {
40156
40229
  let initialX = this.props.numLinePosition;
40157
40230
  const divisionRange = this.props.divisionRange;
40158
40231
  initialX = initialX == null ? range[0] : initialX;
40159
- return range[0] < range[1] && number$1.sign(initialX - range[0]) >= 0 && number$1.sign(initialX - range[1]) <= 0 && divisionRange[0] < divisionRange[1] && 0 < this.props.numDivisions && 0 < this.props.snapDivisions;
40232
+ return range[0] < range[1] && number.sign(initialX - range[0]) >= 0 && number.sign(initialX - range[1]) <= 0 && divisionRange[0] < divisionRange[1] && 0 < this.props.numDivisions && 0 < this.props.snapDivisions;
40160
40233
  };
40161
40234
  this.onNumDivisionsChange = (numDivisions, cb) => {
40162
40235
  const divRange = this.props.divisionRange.slice();
@@ -40259,7 +40332,7 @@ class NumberLine extends React.Component {
40259
40332
  const right = props.range[1];
40260
40333
  const snapX = props.tickStep / props.snapDivisions;
40261
40334
  let x = bound(numLinePosition, left, right);
40262
- x = left + number$1.roundTo(x - left, snapX);
40335
+ x = left + number.roundTo(x - left, snapX);
40263
40336
  assert$1(_.isFinite(x));
40264
40337
  return x;
40265
40338
  };
@@ -40550,7 +40623,10 @@ var NumberLine$1 = {
40550
40623
  staticTransform: staticTransform$1,
40551
40624
  // TODO(LEMS-2656): remove TS suppression
40552
40625
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
40553
- scorer: scoreNumberLine
40626
+ scorer: scoreNumberLine,
40627
+ // TODO(LEMS-2656): remove TS suppression
40628
+ // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
40629
+ validator: validateNumberLine
40554
40630
  };
40555
40631
 
40556
40632
  const getPromptJSON$5 = (renderProps, userInput) => {
@@ -41113,7 +41189,11 @@ var Orderer$1 = {
41113
41189
  isLintable: true,
41114
41190
  // TODO(LEMS-2656): remove TS suppression
41115
41191
  // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
41116
- scorer: scoreOrderer
41192
+ scorer: scoreOrderer,
41193
+ // TODO(LEMS-2656): remove TS suppression
41194
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
41195
+ validator: validateOrderer,
41196
+ getPublicWidgetOptions: getOrdererPublicWidgetOptions
41117
41197
  };
41118
41198
 
41119
41199
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -41157,10 +41237,6 @@ var PassageRefTarget$1 = {
41157
41237
  transform: editorProps => {
41158
41238
  return _.pick(editorProps, "content");
41159
41239
  },
41160
- version: {
41161
- major: 0,
41162
- minor: 0
41163
- },
41164
41240
  isLintable: true,
41165
41241
  // TODO: things that aren't interactive shouldn't need scoring functions
41166
41242
  scorer: () => scoreNoop()
@@ -42183,7 +42259,10 @@ var Plotter$1 = {
42183
42259
  staticTransform: staticTransform,
42184
42260
  // TODO(LEMS-2656): remove TS suppression
42185
42261
  // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
42186
- scorer: scorePlotter
42262
+ scorer: scorePlotter,
42263
+ // TODO(LEMS-2656): remove TS suppression
42264
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
42265
+ validator: validatePlotter
42187
42266
  };
42188
42267
 
42189
42268
  const getPromptJSON$2 = () => {
@@ -42348,7 +42427,10 @@ var Sorter$1 = {
42348
42427
  isLintable: true,
42349
42428
  // TODO(LEMS-2656): remove TS suppression
42350
42429
  // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
42351
- scorer: scoreSorter
42430
+ scorer: scoreSorter,
42431
+ // TODO(LEMS-2656): remove TS suppression
42432
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
42433
+ validator: validateSorter
42352
42434
  };
42353
42435
 
42354
42436
  const {
@@ -42570,7 +42652,10 @@ var Table$1 = {
42570
42652
  isLintable: true,
42571
42653
  // TODO(LEMS-2656): remove TS suppression
42572
42654
  // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
42573
- scorer: scoreTable
42655
+ scorer: scoreTable,
42656
+ // TODO(LEMS-2656): remove TS suppression
42657
+ // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
42658
+ validator: validateTable
42574
42659
  };
42575
42660
 
42576
42661
  const getPromptJSON = () => {
@@ -42753,7 +42838,7 @@ const init = function init() {
42753
42838
 
42754
42839
  // This file is processed by a Rollup plugin (replace) to inject the production
42755
42840
  const libName = "@khanacademy/perseus";
42756
- const libVersion = "51.0.1";
42841
+ const libVersion = "52.0.1";
42757
42842
  addLibraryVersionToPerseusDebug(libName, libVersion);
42758
42843
 
42759
42844
  const apiVersion = {
@@ -44191,6 +44276,71 @@ const registerAllWidgetsForTesting = () => {
44191
44276
  replaceDeprecatedWidgets();
44192
44277
  };
44193
44278
 
44279
+ /**
44280
+ * Get a widget type by a widget's ID
44281
+ * (please don't derive type from ID)
44282
+ *
44283
+ * @param {string} widgetId the ID of the widget
44284
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44285
+ * @returns {string} the widget type (ie "radio")
44286
+ */
44287
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
44288
+ var _widget$type;
44289
+ const widget = widgetMap[WidgetId];
44290
+ return (_widget$type = widget == null ? void 0 : widget.type) != null ? _widget$type : null;
44291
+ }
44292
+
44293
+ /**
44294
+ * Does the content have a specific type of widget?
44295
+ *
44296
+ * @param {string} type the type of the widget in question (ie "radio")
44297
+ * @param {string} content the string to search through
44298
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
44299
+ * @returns {boolean} if the content includes the widget type
44300
+ */
44301
+ function contentHasWidgetType(type, content, widgetMap) {
44302
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
44303
+ }
44304
+
44305
+ /**
44306
+ * Pull the widget map out of ItemData
44307
+ *
44308
+ * @param {PerseusItem} PerseusItem containing a widgetMap
44309
+ * @returns {WidgetMap} the widget map in the PerseusItem
44310
+ */
44311
+ function getWidgetsMapFromItemData(itemData) {
44312
+ return itemData.question.widgets;
44313
+ }
44314
+
44315
+ /**
44316
+ * Get the widget information from a WidgetMap
44317
+ *
44318
+ * @param {string} widgetId the ID of the widget
44319
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
44320
+ */
44321
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
44322
+ var _widgetMap$widgetId;
44323
+ return (_widgetMap$widgetId = widgetMap[widgetId]) != null ? _widgetMap$widgetId : null;
44324
+ }
44325
+
44326
+ /**
44327
+ * Get select widgets from a widget map.
44328
+ *
44329
+ * @param {ReadonlyArray<string>} widgetIds to extract
44330
+ * @param {PerseusWidgetsMap} widgetMap to extract from
44331
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
44332
+ */
44333
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
44334
+ const widgets = {};
44335
+ widgetIds.forEach(widgetId => {
44336
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
44337
+ if (widget) {
44338
+ widgets[widgetId] = widget;
44339
+ }
44340
+ });
44341
+ return widgets;
44342
+ }
44343
+
44194
44344
  /**
44195
44345
  * This function extracts the answers from the widgets.
44196
44346
  *
@@ -44714,1670 +44864,6 @@ const getValidWidgetIds = perseusItem => {
44714
44864
  return keys(widgets).filter(id => isWidgetIdInContent(perseusItem, id));
44715
44865
  };
44716
44866
 
44717
- function isRealJSONParse(jsonParse) {
44718
- const randomPhrase = buildRandomPhrase();
44719
- const randomHintPhrase = buildRandomPhrase();
44720
- const randomString = buildRandomString();
44721
- const testingObject = JSON.stringify({
44722
- answerArea: {
44723
- calculator: false,
44724
- chi2Table: false,
44725
- financialCalculatorMonthlyPayment: false,
44726
- financialCalculatorTimeToPayOff: false,
44727
- financialCalculatorTotalAmount: false,
44728
- periodicTable: false,
44729
- periodicTableWithKey: false,
44730
- tTable: false,
44731
- zTable: false
44732
- },
44733
- hints: [randomHintPhrase, `=${Math.floor(Math.random() * 50) + 1}`],
44734
- itemDataVersion: {
44735
- major: 0,
44736
- minor: 1
44737
- },
44738
- question: {
44739
- content: `${randomPhrase}`,
44740
- images: {},
44741
- widgets: {
44742
- expression1: {
44743
- alignment: "default",
44744
- graded: false,
44745
- options: {
44746
- answerForms: [{
44747
- considered: "wrong",
44748
- form: false,
44749
- key: 0,
44750
- simplify: false,
44751
- value: `${randomString}`
44752
- }],
44753
- ariaLabel: "Answer",
44754
- buttonSets: ["basic"],
44755
- functions: ["f", "g", "h"],
44756
- static: true,
44757
- times: false,
44758
- visibleLabel: "Answer"
44759
- },
44760
- static: true,
44761
- type: "expression",
44762
- version: {
44763
- major: 1,
44764
- minor: 0
44765
- }
44766
- }
44767
- }
44768
- }
44769
- });
44770
- const testJSON = buildTestData(testingObject.replace(/"/g, '\\"'));
44771
- const parsedTestJSON = jsonParse(testJSON);
44772
- const parsedTestItemData = parsedTestJSON.data.assessmentItem.item.itemData;
44773
- return approximateDeepEqual(parsedTestItemData, testingObject);
44774
- }
44775
- function buildRandomString(capitalize = false) {
44776
- let randomString = "";
44777
- const randomLength = Math.floor(Math.random() * 8) + 3;
44778
- for (let i = 0; i < randomLength; i++) {
44779
- const randomLetter = String.fromCharCode(97 + Math.floor(Math.random() * 26));
44780
- randomString += capitalize && i === 0 ? randomLetter.toUpperCase() : randomLetter;
44781
- }
44782
- return randomString;
44783
- }
44784
- function buildRandomPhrase() {
44785
- const phrases = [];
44786
- const randomLength = Math.floor(Math.random() * 10) + 5;
44787
- for (let i = 0; i < randomLength; i++) {
44788
- phrases.push(buildRandomString(i === 0));
44789
- }
44790
- const modifierStart = ["**", "$"];
44791
- const modifierEnd = ["**", "$"];
44792
- const modifierIndex = Math.floor(Math.random() * modifierStart.length);
44793
- return `${modifierStart[modifierIndex]}${phrases.join(" ")}${modifierEnd[modifierIndex]}`;
44794
- }
44795
- function buildTestData(testObject) {
44796
- return `{"data":{"assessmentItem":{"__typename":"AssessmentItemOrError","error":null,"item":{"__typename":"AssessmentItem","id":"x890b3c70f3e8f4a6","itemData":"${testObject}","problemType":"Type 1","sha":"c7284a3ad65214b4e62bccce236d92f7f5d35941"}}}}`;
44797
- }
44798
-
44799
- function success(value) {
44800
- return {
44801
- type: "success",
44802
- value
44803
- };
44804
- }
44805
- function failure(detail) {
44806
- return {
44807
- type: "failure",
44808
- detail
44809
- };
44810
- }
44811
- function isFailure(result) {
44812
- return result.type === "failure";
44813
- }
44814
- function isSuccess(result) {
44815
- return result.type === "success";
44816
- }
44817
-
44818
- // Result's `all` function is similar to Promise.all: given an array of
44819
- // results, it returns success if all succeeded, and failure if any failed.
44820
- function all(results, combineFailureDetails = a => a) {
44821
- const values = [];
44822
- const failureDetails = [];
44823
- for (const result of results) {
44824
- if (result.type === "success") {
44825
- values.push(result.value);
44826
- } else {
44827
- failureDetails.push(result.detail);
44828
- }
44829
- }
44830
- if (failureDetails.length > 0) {
44831
- return failure(failureDetails.reduce(combineFailureDetails));
44832
- }
44833
- return success(values);
44834
- }
44835
-
44836
- class ErrorTrackingParseContext {
44837
- constructor(path) {
44838
- this.path = path;
44839
- }
44840
- failure(expected, badValue) {
44841
- return failure([{
44842
- expected: wrapInArray(expected),
44843
- badValue,
44844
- path: this.path
44845
- }]);
44846
- }
44847
- forSubtree(key) {
44848
- return new ErrorTrackingParseContext([...this.path, key]);
44849
- }
44850
- success(value) {
44851
- return success(value);
44852
- }
44853
- }
44854
- function wrapInArray(a) {
44855
- return Array.isArray(a) ? a : [a];
44856
- }
44857
-
44858
- function formatPath(path) {
44859
- return "(root)" + path.map(formatPathSegment).join("");
44860
- }
44861
- function formatPathSegment(segment) {
44862
- if (typeof segment === "string") {
44863
- return validIdentifier.test(segment) ? "." + segment : `[${JSON.stringify(segment)}]`;
44864
- }
44865
- return `[${segment.toString()}]`;
44866
- }
44867
- const validIdentifier = /^[A-Za-z$_][A-Za-z$_0-9]*$/;
44868
-
44869
- function message(failure) {
44870
- const expected = conjoin(failure.expected);
44871
- const path = formatPath(failure.path);
44872
- const badValue = JSON.stringify(failure.badValue);
44873
- return `At ${path} -- expected ${expected}, but got ${badValue}`;
44874
- }
44875
- function conjoin(items) {
44876
- switch (items.length) {
44877
- // TODO(benchristel): handle 0 if this is reused elsewhere.
44878
- case 1:
44879
- return items[0];
44880
- case 2:
44881
- return items.join(" or ");
44882
- default:
44883
- {
44884
- const allButLast = items.slice(0, items.length - 1);
44885
- const last = items[items.length - 1];
44886
- return allButLast.join(", ") + ", or " + last;
44887
- }
44888
- }
44889
- }
44890
-
44891
- function parse(value, parser) {
44892
- const result = parser(value, new ErrorTrackingParseContext([]));
44893
- if (isFailure(result)) {
44894
- return failure(result.detail.map(message).join("; "));
44895
- }
44896
- return result;
44897
- }
44898
-
44899
- const any = (rawValue, ctx) => ctx.success(rawValue);
44900
-
44901
- function array(elementParser) {
44902
- return (rawValue, ctx) => {
44903
- if (!Array.isArray(rawValue)) {
44904
- return ctx.failure("array", rawValue);
44905
- }
44906
- const elementResults = rawValue.map((elem, i) => elementParser(elem, ctx.forSubtree(i)));
44907
- return all(elementResults, concat);
44908
- };
44909
- }
44910
- function concat(a, b) {
44911
- return [...a, ...b];
44912
- }
44913
-
44914
- function boolean(rawValue, ctx) {
44915
- if (typeof rawValue === "boolean") {
44916
- return ctx.success(rawValue);
44917
- }
44918
- return ctx.failure("boolean", rawValue);
44919
- }
44920
-
44921
- function constant(acceptedValue) {
44922
- return (rawValue, ctx) => {
44923
- if (rawValue !== acceptedValue) {
44924
- return ctx.failure(String(JSON.stringify(acceptedValue)), rawValue);
44925
- }
44926
- return ctx.success(acceptedValue);
44927
- };
44928
- }
44929
-
44930
- function enumeration(...acceptedValues) {
44931
- return (rawValue, ctx) => {
44932
- if (typeof rawValue === "string") {
44933
- const index = acceptedValues.indexOf(rawValue);
44934
- if (index > -1) {
44935
- return ctx.success(acceptedValues[index]);
44936
- }
44937
- }
44938
- const expected = acceptedValues.map(v => JSON.stringify(v));
44939
- return ctx.failure(expected, rawValue);
44940
- };
44941
- }
44942
-
44943
- function isObject(x) {
44944
- return x != null && Object.getPrototypeOf(x) === Object.prototype;
44945
- }
44946
-
44947
- function nullable(parseValue) {
44948
- return (rawValue, ctx) => {
44949
- if (rawValue === null) {
44950
- return ctx.success(rawValue);
44951
- }
44952
- return parseValue(rawValue, ctx);
44953
- };
44954
- }
44955
-
44956
- const number = (rawValue, ctx) => {
44957
- if (typeof rawValue === "number") {
44958
- return ctx.success(rawValue);
44959
- }
44960
- return ctx.failure("number", rawValue);
44961
- };
44962
-
44963
- function object(schema) {
44964
- return (rawValue, ctx) => {
44965
- if (!isObject(rawValue)) {
44966
- return ctx.failure("object", rawValue);
44967
- }
44968
- const ret = _extends({}, rawValue);
44969
- const mismatches = [];
44970
- for (const [prop, propParser] of Object.entries(schema)) {
44971
- const result = propParser(rawValue[prop], ctx.forSubtree(prop));
44972
- if (isSuccess(result)) {
44973
- if (result.value !== undefined || prop in rawValue) {
44974
- ret[prop] = result.value;
44975
- }
44976
- } else {
44977
- mismatches.push(...result.detail);
44978
- }
44979
- }
44980
- if (mismatches.length > 0) {
44981
- return failure(mismatches);
44982
- }
44983
- return ctx.success(ret);
44984
- };
44985
- }
44986
-
44987
- function optional(parseValue) {
44988
- return (rawValue, ctx) => {
44989
- if (rawValue === undefined) {
44990
- return ctx.success(rawValue);
44991
- }
44992
- return parseValue(rawValue, ctx);
44993
- };
44994
- }
44995
-
44996
- function pair(parseA, parseB) {
44997
- return (rawValue, ctx) => {
44998
- if (!Array.isArray(rawValue)) {
44999
- return ctx.failure("array", rawValue);
45000
- }
45001
- if (rawValue.length !== 2) {
45002
- return ctx.failure("array of length 2", rawValue);
45003
- }
45004
- const [rawA, rawB] = rawValue;
45005
- const resultA = parseA(rawA, ctx.forSubtree(0));
45006
- if (isFailure(resultA)) {
45007
- return resultA;
45008
- }
45009
- const resultB = parseB(rawB, ctx.forSubtree(1));
45010
- if (isFailure(resultB)) {
45011
- return resultB;
45012
- }
45013
- return ctx.success([resultA.value, resultB.value]);
45014
- };
45015
- }
45016
-
45017
- function pipeParsers(p) {
45018
- return new ParserPipeline(p);
45019
- }
45020
- class ParserPipeline {
45021
- constructor(parser) {
45022
- this.parser = parser;
45023
- }
45024
- then(nextParser) {
45025
- return new ParserPipeline(composeParsers(this.parser, nextParser));
45026
- }
45027
- }
45028
- function composeParsers(parserA, parserB) {
45029
- return (rawValue, ctx) => {
45030
- const partialResult = parserA(rawValue, ctx);
45031
- if (isFailure(partialResult)) {
45032
- return partialResult;
45033
- }
45034
- return parserB(partialResult.value, ctx);
45035
- };
45036
- }
45037
-
45038
- function record(parseKey, parseValue) {
45039
- return (rawValue, ctx) => {
45040
- if (!isObject(rawValue)) {
45041
- return ctx.failure("object", rawValue);
45042
- }
45043
- const result = {};
45044
- const mismatches = [];
45045
- for (const [key, value] of Object.entries(rawValue)) {
45046
- const entryCtx = ctx.forSubtree(key);
45047
- const keyResult = parseKey(key, entryCtx);
45048
- if (isFailure(keyResult)) {
45049
- mismatches.push(...keyResult.detail);
45050
- }
45051
- const valueResult = parseValue(value, entryCtx);
45052
- if (isFailure(valueResult)) {
45053
- mismatches.push(...valueResult.detail);
45054
- }
45055
- if (isSuccess(keyResult) && isSuccess(valueResult)) {
45056
- result[keyResult.value] = valueResult.value;
45057
- }
45058
- }
45059
- if (mismatches.length > 0) {
45060
- return failure(mismatches);
45061
- }
45062
- return ctx.success(result);
45063
- };
45064
- }
45065
-
45066
- const string = (rawValue, ctx) => {
45067
- if (typeof rawValue === "string") {
45068
- return ctx.success(rawValue);
45069
- }
45070
- return ctx.failure("string", rawValue);
45071
- };
45072
-
45073
- function trio(parseA, parseB, parseC) {
45074
- return (rawValue, ctx) => {
45075
- if (!Array.isArray(rawValue)) {
45076
- return ctx.failure("array", rawValue);
45077
- }
45078
- if (rawValue.length !== 3) {
45079
- return ctx.failure("array of length 3", rawValue);
45080
- }
45081
- const resultA = parseA(rawValue[0], ctx.forSubtree(0));
45082
- if (isFailure(resultA)) {
45083
- return resultA;
45084
- }
45085
- const resultB = parseB(rawValue[1], ctx.forSubtree(1));
45086
- if (isFailure(resultB)) {
45087
- return resultB;
45088
- }
45089
- const resultC = parseC(rawValue[2], ctx.forSubtree(2));
45090
- if (isFailure(resultC)) {
45091
- return resultC;
45092
- }
45093
- return ctx.success([resultA.value, resultB.value, resultC.value]);
45094
- };
45095
- }
45096
-
45097
- function union(parseBranch) {
45098
- return new UnionBuilder(parseBranch);
45099
- }
45100
- class UnionBuilder {
45101
- constructor(parser) {
45102
- this.parser = parser;
45103
- }
45104
- or(newBranch) {
45105
- return new UnionBuilder(either(this.parser, newBranch));
45106
- }
45107
- }
45108
- function either(parseA, parseB) {
45109
- return (rawValue, ctx) => {
45110
- const resultA = parseA(rawValue, ctx);
45111
- if (isSuccess(resultA)) {
45112
- return resultA;
45113
- }
45114
- return parseB(rawValue, ctx);
45115
- };
45116
- }
45117
-
45118
- function defaulted(parser, fallback) {
45119
- return (rawValue, ctx) => {
45120
- if (rawValue == null) {
45121
- return success(fallback(rawValue));
45122
- }
45123
- return parser(rawValue, ctx);
45124
- };
45125
- }
45126
-
45127
- const parseImages = defaulted(record(string, object({
45128
- width: number,
45129
- height: number
45130
- })), () => ({}));
45131
-
45132
- function parseWidget(parseType, parseOptions) {
45133
- return object({
45134
- type: parseType,
45135
- static: optional(boolean),
45136
- graded: optional(boolean),
45137
- alignment: optional(string),
45138
- options: parseOptions,
45139
- key: optional(number),
45140
- version: optional(object({
45141
- major: number,
45142
- minor: number
45143
- }))
45144
- });
45145
- }
45146
- function parseWidgetWithVersion(parseVersion, parseType, parseOptions) {
45147
- return object({
45148
- type: parseType,
45149
- static: optional(boolean),
45150
- graded: optional(boolean),
45151
- alignment: optional(string),
45152
- options: parseOptions,
45153
- key: optional(number),
45154
- version: parseVersion
45155
- });
45156
- }
45157
-
45158
- const parseCategorizerWidget = parseWidget(constant("categorizer"), object({
45159
- items: array(string),
45160
- categories: array(string),
45161
- randomizeItems: defaulted(boolean, () => false),
45162
- static: defaulted(boolean, () => false),
45163
- values: array(defaulted(number, () => 0)),
45164
- highlightLint: optional(boolean),
45165
- linterContext: optional(object({
45166
- contentType: string,
45167
- paths: array(string),
45168
- stack: array(string)
45169
- }))
45170
- }));
45171
-
45172
- const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
45173
- programID: string,
45174
- programType: any,
45175
- settings: array(object({
45176
- name: string,
45177
- value: string
45178
- })),
45179
- showEditor: boolean,
45180
- showButtons: boolean,
45181
- height: number,
45182
- static: defaulted(boolean, () => false)
45183
- }));
45184
-
45185
- const parseDefinitionWidget = parseWidget(constant("definition"), object({
45186
- togglePrompt: string,
45187
- definition: string,
45188
- static: defaulted(boolean, () => false)
45189
- }));
45190
-
45191
- const parseDropdownWidget = parseWidget(constant("dropdown"), object({
45192
- placeholder: defaulted(string, () => ""),
45193
- ariaLabel: optional(string),
45194
- visibleLabel: optional(string),
45195
- static: defaulted(boolean, () => false),
45196
- choices: array(object({
45197
- content: string,
45198
- correct: boolean
45199
- }))
45200
- }));
45201
-
45202
- const parseExplanationWidget = parseWidget(constant("explanation"), object({
45203
- showPrompt: string,
45204
- hidePrompt: string,
45205
- explanation: string,
45206
- // We wrap parseWidgetsMap in a function here to make sure it is not
45207
- // referenced before it is defined. There is an import cycle between
45208
- // this file and widgets-map.ts that could cause it to be undefined.
45209
- widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
45210
- static: defaulted(boolean, () => false)
45211
- }));
45212
-
45213
- // Given a function, creates a PartialParser that converts one type to another
45214
- // using that function. The returned parser never fails.
45215
- function convert(f) {
45216
- return (rawValue, ctx) => ctx.success(f(rawValue));
45217
- }
45218
-
45219
- /**
45220
- * Creates a parser for a widget options type with multiple major versions. Old
45221
- * versions are migrated to the latest version. The parse fails if the input
45222
- * data does not match any of the versions.
45223
- *
45224
- * @example
45225
- * const parseOptions = versionedWidgetOptions(3, parseOptionsV3)
45226
- * .withMigrationFrom(2, parseOptionsV2, migrateV2ToV3)
45227
- * .withMigrationFrom(1, parseOptionsV1, migrateV1ToV2)
45228
- * .withMigrationFrom(0, parseOptionsV0, migrateV0ToV1)
45229
- * .parser;
45230
- *
45231
- * @param latestMajorVersion the latest major version of the widget options.
45232
- * @param parseLatest a {@link Parser} for the latest version of the widget
45233
- * options.
45234
- * @returns a builder object, to which migrations from earlier versions can be
45235
- * added. Migrations must be added in "reverse chronological" order as in the
45236
- * example above.
45237
- */
45238
- function versionedWidgetOptions(latestMajorVersion, parseLatest) {
45239
- return new VersionedWidgetOptionsParserBuilder(latestMajorVersion, parseLatest, latest => latest, (raw, ctx) => ctx.failure("widget options with a known version number", raw));
45240
- }
45241
- class VersionedWidgetOptionsParserBuilder {
45242
- constructor(majorVersion, parseThisVersion, migrateToLatest, parseOtherVersions) {
45243
- this.parser = void 0;
45244
- this.migrateToLatest = migrateToLatest;
45245
- this.parseOtherVersions = parseOtherVersions;
45246
- const parseThisVersionAndMigrateToLatest = pipeParsers(parseThisVersion).then(convert(this.migrateToLatest)).parser;
45247
- this.parser = (raw, ctx) => {
45248
- if (!isObject(raw)) {
45249
- return ctx.failure("object", raw);
45250
- }
45251
- const versionParseResult = parseVersionedObject(raw, ctx);
45252
- if (isFailure(versionParseResult)) {
45253
- return versionParseResult;
45254
- }
45255
- if (versionParseResult.value.version.major !== majorVersion) {
45256
- return this.parseOtherVersions(raw, ctx);
45257
- }
45258
- return parseThisVersionAndMigrateToLatest(raw, ctx);
45259
- };
45260
- }
45261
-
45262
- /**
45263
- * Add a migration from an old version of the widget options.
45264
- */
45265
- withMigrationFrom(majorVersion, parseOldVersion, migrateToNextVersion) {
45266
- const parseOtherVersions = this.parser;
45267
- const migrateToLatest = old => this.migrateToLatest(migrateToNextVersion(old));
45268
- return new VersionedWidgetOptionsParserBuilder(majorVersion, parseOldVersion, migrateToLatest, parseOtherVersions);
45269
- }
45270
- }
45271
- const parseVersionedObject = object({
45272
- version: defaulted(object({
45273
- major: number,
45274
- minor: number
45275
- }), () => ({
45276
- major: 0,
45277
- minor: 0
45278
- }))
45279
- });
45280
-
45281
- const stringOrNumberOrNullOrUndefined = union(string).or(number).or(constant(null)).or(constant(undefined)).parser;
45282
- const parsePossiblyInvalidAnswerForm = object({
45283
- // `value` is the possibly invalid part of this. It should always be a
45284
- // string, but some answer forms don't have it. The Expression widget
45285
- // ignores invalid values, so we can safely filter them out during parsing.
45286
- value: optional(string),
45287
- form: defaulted(boolean, () => false),
45288
- simplify: defaulted(boolean, () => false),
45289
- considered: enumeration("correct", "wrong", "ungraded"),
45290
- key: pipeParsers(stringOrNumberOrNullOrUndefined).then(convert(String)).parser
45291
- });
45292
- function removeInvalidAnswerForms(possiblyInvalid) {
45293
- const valid = [];
45294
- for (const answerForm of possiblyInvalid) {
45295
- const {
45296
- value
45297
- } = answerForm;
45298
- if (value != null) {
45299
- // Copying the object seems to be needed to make TypeScript happy
45300
- valid.push(_extends({}, answerForm, {
45301
- value
45302
- }));
45303
- }
45304
- }
45305
- return valid;
45306
- }
45307
-
45308
- // NOTE(benchristel): I copied the default buttonSets from
45309
- // expression.tsx. See the parse-perseus-json/README.md for
45310
- // an explanation of why we want to duplicate the default here.
45311
- const parseButtonSets = defaulted(array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")), () => ["basic", "trig", "prealgebra", "logarithms"]);
45312
- const version1 = object({
45313
- major: constant(1),
45314
- minor: number
45315
- });
45316
- const parseExpressionWidgetV1 = parseWidgetWithVersion(version1, constant("expression"), object({
45317
- answerForms: pipeParsers(array(parsePossiblyInvalidAnswerForm)).then(convert(removeInvalidAnswerForms)).parser,
45318
- functions: array(string),
45319
- times: boolean,
45320
- visibleLabel: optional(string),
45321
- ariaLabel: optional(string),
45322
- buttonSets: parseButtonSets,
45323
- buttonsVisible: optional(enumeration("always", "never", "focused"))
45324
- }));
45325
- const version0 = optional(object({
45326
- major: constant(0),
45327
- minor: number
45328
- }));
45329
- const parseExpressionWidgetV0 = parseWidgetWithVersion(version0, constant("expression"), object({
45330
- functions: array(string),
45331
- times: boolean,
45332
- visibleLabel: optional(string),
45333
- ariaLabel: optional(string),
45334
- form: boolean,
45335
- simplify: boolean,
45336
- value: string,
45337
- buttonSets: parseButtonSets,
45338
- buttonsVisible: optional(enumeration("always", "never", "focused"))
45339
- }));
45340
- function migrateV0ToV1(widget) {
45341
- const {
45342
- options
45343
- } = widget;
45344
- return _extends({}, widget, {
45345
- version: {
45346
- major: 1,
45347
- minor: 0
45348
- },
45349
- options: {
45350
- times: options.times,
45351
- buttonSets: options.buttonSets,
45352
- functions: options.functions,
45353
- buttonsVisible: options.buttonsVisible,
45354
- visibleLabel: options.visibleLabel,
45355
- ariaLabel: options.ariaLabel,
45356
- answerForms: [{
45357
- considered: "correct",
45358
- form: options.form,
45359
- simplify: options.simplify,
45360
- value: options.value
45361
- }]
45362
- }
45363
- });
45364
- }
45365
- const parseExpressionWidget = versionedWidgetOptions(1, parseExpressionWidgetV1).withMigrationFrom(0, parseExpressionWidgetV0, migrateV0ToV1).parser;
45366
-
45367
- const falseToNull = pipeParsers(constant(false)).then(convert(() => null)).parser;
45368
- const parseGradedGroupWidgetOptions = object({
45369
- title: defaulted(string, () => ""),
45370
- hasHint: optional(nullable(boolean)),
45371
- // This module has an import cycle with parsePerseusRenderer.
45372
- // The anonymous function below ensures that we don't try to access
45373
- // parsePerseusRenderer before it's defined.
45374
- hint: union(falseToNull).or(constant(null)).or(constant(undefined)).or((rawVal, ctx) => parsePerseusRenderer(rawVal, ctx)).parser,
45375
- content: string,
45376
- // This module has an import cycle with parseWidgetsMap.
45377
- // The anonymous function below ensures that we don't try to access
45378
- // parseWidgetsMap before it's defined.
45379
- widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
45380
- widgetEnabled: optional(nullable(boolean)),
45381
- immutableWidgets: optional(nullable(boolean)),
45382
- images: record(string, object({
45383
- width: number,
45384
- height: number
45385
- }))
45386
- });
45387
- const parseGradedGroupWidget = parseWidget(constant("graded-group"), parseGradedGroupWidgetOptions);
45388
-
45389
- const parseGradedGroupSetWidget = parseWidget(constant("graded-group-set"), object({
45390
- gradedGroups: array(parseGradedGroupWidgetOptions)
45391
- }));
45392
-
45393
- /**
45394
- * discriminatedUnion() should be preferred over union() when parsing a
45395
- * discriminated union type, because discriminatedUnion() produces more
45396
- * understandable failure messages. It takes the discriminant as the source of
45397
- * truth for which variant is to be parsed, and expects the other data to match
45398
- * that variant.
45399
- */
45400
- function discriminatedUnionOn(discriminantKey) {
45401
- const noMoreBranches = (raw, ctx) => {
45402
- if (!isObject(raw)) {
45403
- return ctx.failure("object", raw);
45404
- }
45405
- return ctx.forSubtree(discriminantKey).failure("a valid value", raw[discriminantKey]);
45406
- };
45407
- return new DiscriminatedUnionBuilder(discriminantKey, noMoreBranches);
45408
- }
45409
- class DiscriminatedUnionBuilder {
45410
- constructor(discriminantKey, parser) {
45411
- this.discriminantKey = discriminantKey;
45412
- this.parser = parser;
45413
- }
45414
- withBranch(discriminantValue, parseNewVariant) {
45415
- const parseNewBranch = discriminatedUnionBranch(this.discriminantKey, discriminantValue, parseNewVariant, this.parser);
45416
- return new DiscriminatedUnionBuilder(this.discriminantKey, parseNewBranch);
45417
- }
45418
- }
45419
- function discriminatedUnionBranch(discriminantKey, discriminantValue, parseVariant, parseOtherBranches) {
45420
- return (raw, ctx) => {
45421
- if (!isObject(raw)) {
45422
- return ctx.failure("object", raw);
45423
- }
45424
- if (raw[discriminantKey] === discriminantValue) {
45425
- return parseVariant(raw, ctx);
45426
- }
45427
- return parseOtherBranches(raw, ctx);
45428
- };
45429
- }
45430
-
45431
- const pairOfNumbers$3 = pair(number, number);
45432
- const pairOfPoints = pair(pairOfNumbers$3, pairOfNumbers$3);
45433
- const parseGrapherWidget = parseWidget(constant("grapher"), object({
45434
- availableTypes: array(enumeration("absolute_value", "exponential", "linear", "logarithm", "quadratic", "sinusoid", "tangent")),
45435
- correct: discriminatedUnionOn("type").withBranch("absolute_value", object({
45436
- type: constant("absolute_value"),
45437
- coords: nullable(pairOfPoints)
45438
- })).withBranch("exponential", object({
45439
- type: constant("exponential"),
45440
- asymptote: pairOfPoints,
45441
- coords: nullable(pairOfPoints)
45442
- })).withBranch("linear", object({
45443
- type: constant("linear"),
45444
- coords: nullable(pairOfPoints)
45445
- })).withBranch("logarithm", object({
45446
- type: constant("logarithm"),
45447
- asymptote: pairOfPoints,
45448
- coords: nullable(pairOfPoints)
45449
- })).withBranch("quadratic", object({
45450
- type: constant("quadratic"),
45451
- coords: nullable(pairOfPoints)
45452
- })).withBranch("sinusoid", object({
45453
- type: constant("sinusoid"),
45454
- coords: nullable(pairOfPoints)
45455
- })).withBranch("tangent", object({
45456
- type: constant("tangent"),
45457
- coords: nullable(pairOfPoints)
45458
- })).parser,
45459
- graph: object({
45460
- backgroundImage: object({
45461
- bottom: optional(number),
45462
- height: optional(number),
45463
- left: optional(number),
45464
- scale: optional(number),
45465
- url: optional(nullable(string)),
45466
- width: optional(number)
45467
- }),
45468
- box: optional(pairOfNumbers$3),
45469
- editableSettings: optional(array(enumeration("graph", "snap", "image", "measure"))),
45470
- gridStep: optional(pairOfNumbers$3),
45471
- labels: pair(string, string),
45472
- markings: enumeration("graph", "none", "grid"),
45473
- range: pair(pairOfNumbers$3, pairOfNumbers$3),
45474
- rulerLabel: constant(""),
45475
- rulerTicks: number,
45476
- showProtractor: optional(boolean),
45477
- showRuler: optional(boolean),
45478
- showTooltips: optional(boolean),
45479
- snapStep: optional(pairOfNumbers$3),
45480
- step: pairOfNumbers$3,
45481
- valid: optional(union(boolean).or(string).parser)
45482
- })
45483
- }));
45484
-
45485
- const parseGroupWidget = parseWidget(constant("group"),
45486
- // This module has an import cycle with parsePerseusRenderer.
45487
- // The anonymous function below ensures that we don't try to access
45488
- // parsePerseusRenderer before it's defined.
45489
- (rawVal, ctx) => parsePerseusRenderer(rawVal, ctx));
45490
-
45491
- const parseIframeWidget = parseWidget(constant("iframe"), object({
45492
- url: string,
45493
- settings: optional(array(object({
45494
- name: string,
45495
- value: string
45496
- }))),
45497
- width: union(number).or(string).parser,
45498
- height: union(number).or(string).parser,
45499
- allowFullScreen: defaulted(boolean, () => false),
45500
- allowTopNavigation: optional(boolean),
45501
- static: defaulted(boolean, () => false)
45502
- }));
45503
-
45504
- const stringToNumber = (rawValue, ctx) => {
45505
- if (typeof rawValue === "number") {
45506
- return ctx.success(rawValue);
45507
- }
45508
- const parsedNumber = +rawValue;
45509
- if (rawValue === "" || isNaN(parsedNumber)) {
45510
- return ctx.failure("a number or numeric string", rawValue);
45511
- }
45512
- return ctx.success(parsedNumber);
45513
- };
45514
-
45515
- function emptyToZero(x) {
45516
- return x === "" ? 0 : x;
45517
- }
45518
- const imageDimensionToNumber = pipeParsers(union(number).or(string).parser)
45519
- // In this specific case, empty string is equivalent to zero. An empty
45520
- // string parses to either NaN (using parseInt) or 0 (using unary +) and
45521
- // CSS will treat NaN as invalid and default to 0 instead.
45522
- .then(convert(emptyToZero)).then(stringToNumber).parser;
45523
- const parsePerseusImageBackground = object({
45524
- url: optional(nullable(string)),
45525
- width: optional(imageDimensionToNumber),
45526
- height: optional(imageDimensionToNumber),
45527
- top: optional(imageDimensionToNumber),
45528
- left: optional(imageDimensionToNumber),
45529
- bottom: optional(imageDimensionToNumber),
45530
- scale: optional(imageDimensionToNumber)
45531
- });
45532
-
45533
- const pairOfNumbers$2 = pair(number, number);
45534
- const parseImageWidget = parseWidget(constant("image"), object({
45535
- title: optional(string),
45536
- caption: optional(string),
45537
- alt: optional(string),
45538
- backgroundImage: parsePerseusImageBackground,
45539
- static: optional(boolean),
45540
- labels: optional(array(object({
45541
- content: string,
45542
- alignment: string,
45543
- coordinates: array(number)
45544
- }))),
45545
- range: optional(pair(pairOfNumbers$2, pairOfNumbers$2)),
45546
- box: optional(pairOfNumbers$2)
45547
- }));
45548
-
45549
- const booleanToString = (rawValue, ctx) => {
45550
- if (typeof rawValue === "boolean") {
45551
- return ctx.success(String(rawValue));
45552
- }
45553
- return ctx.failure("boolean", rawValue);
45554
- };
45555
- const parseInputNumberWidget = parseWidget(constant("input-number"), object({
45556
- answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
45557
- inexact: optional(boolean),
45558
- maxError: optional(union(number).or(string).parser),
45559
- rightAlign: optional(boolean),
45560
- simplify: enumeration("required", "optional", "enforced"),
45561
- size: enumeration("normal", "small"),
45562
- // TODO(benchristel): there are some content items where value is a
45563
- // boolean, even though that makes no sense. We should figure out if
45564
- // those content items are actually published anywhere, and consider
45565
- // updating them.
45566
- value: union(number).or(string).or(booleanToString).parser,
45567
- customKeypad: optional(boolean)
45568
- }));
45569
-
45570
- const pairOfNumbers$1 = pair(number, number);
45571
- const stringOrEmpty = defaulted(string, () => "");
45572
- const parseKey = pipeParsers(optional(string)).then(convert(String)).parser;
45573
- const parseFunctionElement = object({
45574
- type: constant("function"),
45575
- key: parseKey,
45576
- options: object({
45577
- value: string,
45578
- funcName: string,
45579
- rangeMin: string,
45580
- rangeMax: string,
45581
- color: string,
45582
- strokeDasharray: string,
45583
- strokeWidth: number
45584
- })
45585
- });
45586
- const parseLabelElement = object({
45587
- type: constant("label"),
45588
- key: parseKey,
45589
- options: object({
45590
- label: string,
45591
- color: string,
45592
- coordX: string,
45593
- coordY: string
45594
- })
45595
- });
45596
- const parseLineElement = object({
45597
- type: constant("line"),
45598
- key: parseKey,
45599
- options: object({
45600
- color: string,
45601
- startX: string,
45602
- startY: string,
45603
- endX: string,
45604
- endY: string,
45605
- strokeDasharray: string,
45606
- strokeWidth: number,
45607
- arrows: string
45608
- })
45609
- });
45610
- const parseMovableLineElement = object({
45611
- type: constant("movable-line"),
45612
- key: parseKey,
45613
- options: object({
45614
- startX: string,
45615
- startY: string,
45616
- startSubscript: number,
45617
- endX: string,
45618
- endY: string,
45619
- endSubscript: number,
45620
- constraint: string,
45621
- snap: number,
45622
- constraintFn: string,
45623
- constraintXMin: string,
45624
- constraintXMax: string,
45625
- constraintYMin: string,
45626
- constraintYMax: string
45627
- })
45628
- });
45629
- const parseMovablePointElement = object({
45630
- type: constant("movable-point"),
45631
- key: parseKey,
45632
- options: object({
45633
- startX: string,
45634
- startY: string,
45635
- varSubscript: number,
45636
- constraint: string,
45637
- snap: number,
45638
- constraintFn: string,
45639
- constraintXMin: stringOrEmpty,
45640
- constraintXMax: stringOrEmpty,
45641
- constraintYMin: stringOrEmpty,
45642
- constraintYMax: stringOrEmpty
45643
- })
45644
- });
45645
- const parseParametricElement = object({
45646
- type: constant("parametric"),
45647
- key: parseKey,
45648
- options: object({
45649
- x: string,
45650
- y: string,
45651
- rangeMin: string,
45652
- rangeMax: string,
45653
- color: string,
45654
- strokeDasharray: string,
45655
- strokeWidth: number
45656
- })
45657
- });
45658
- const parsePointElement = object({
45659
- type: constant("point"),
45660
- key: parseKey,
45661
- options: object({
45662
- color: string,
45663
- coordX: string,
45664
- coordY: string
45665
- })
45666
- });
45667
- const parseRectangleElement = object({
45668
- type: constant("rectangle"),
45669
- key: parseKey,
45670
- options: object({
45671
- color: string,
45672
- coordX: string,
45673
- coordY: string,
45674
- width: string,
45675
- height: string
45676
- })
45677
- });
45678
- const parseInteractionWidget = parseWidget(constant("interaction"), object({
45679
- static: defaulted(boolean, () => false),
45680
- graph: object({
45681
- editableSettings: optional(array(enumeration("canvas", "graph"))),
45682
- box: pairOfNumbers$1,
45683
- labels: array(string),
45684
- range: pair(pairOfNumbers$1, pairOfNumbers$1),
45685
- gridStep: pairOfNumbers$1,
45686
- markings: enumeration("graph", "grid", "none"),
45687
- snapStep: optional(pairOfNumbers$1),
45688
- valid: optional(union(boolean).or(string).parser),
45689
- backgroundImage: optional(parsePerseusImageBackground),
45690
- showProtractor: optional(boolean),
45691
- showRuler: optional(boolean),
45692
- rulerLabel: optional(string),
45693
- rulerTicks: optional(number),
45694
- tickStep: pairOfNumbers$1
45695
- }),
45696
- 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)
45697
- }));
45698
-
45699
- // Used to represent 2-D points and ranges
45700
- const pairOfNumbers = pair(number, number);
45701
- const parsePerseusGraphTypeAngle = object({
45702
- type: constant("angle"),
45703
- showAngles: optional(boolean),
45704
- allowReflexAngles: optional(boolean),
45705
- angleOffsetDeg: optional(number),
45706
- snapDegrees: optional(number),
45707
- match: optional(constant("congruent")),
45708
- coords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
45709
- startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))
45710
- });
45711
- const parsePerseusGraphTypeCircle = object({
45712
- type: constant("circle"),
45713
- center: optional(pairOfNumbers),
45714
- radius: optional(number),
45715
- startCoords: optional(object({
45716
- center: pairOfNumbers,
45717
- radius: number
45718
- })),
45719
- // TODO: remove coord? it's legacy.
45720
- coord: optional(pairOfNumbers)
45721
- });
45722
- const parsePerseusGraphTypeLinear = object({
45723
- type: constant("linear"),
45724
- coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
45725
- startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
45726
- // TODO: remove coord? it's legacy.
45727
- coord: optional(pairOfNumbers)
45728
- });
45729
- const parsePerseusGraphTypeLinearSystem = object({
45730
- type: constant("linear-system"),
45731
- // TODO(benchristel): default coords to empty array?
45732
- coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
45733
- startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
45734
- // TODO: remove coord? it's legacy.
45735
- coord: optional(pairOfNumbers)
45736
- });
45737
- const parsePerseusGraphTypeNone = object({
45738
- type: constant("none")
45739
- });
45740
- const parsePerseusGraphTypePoint = object({
45741
- type: constant("point"),
45742
- numPoints: optional(union(number).or(constant("unlimited")).parser),
45743
- coords: optional(nullable(array(pairOfNumbers))),
45744
- startCoords: optional(array(pairOfNumbers)),
45745
- // TODO: remove coord? it's legacy.
45746
- coord: optional(pairOfNumbers)
45747
- });
45748
- const parsePerseusGraphTypePolygon = object({
45749
- type: constant("polygon"),
45750
- numSides: optional(union(number).or(constant("unlimited")).parser),
45751
- showAngles: optional(boolean),
45752
- showSides: optional(boolean),
45753
- snapTo: optional(enumeration("grid", "angles", "sides")),
45754
- match: optional(enumeration("similar", "congruent", "approx", "exact")),
45755
- startCoords: optional(array(pairOfNumbers)),
45756
- // TODO: remove coord? it's legacy.
45757
- coord: optional(pairOfNumbers)
45758
- });
45759
- const parsePerseusGraphTypeQuadratic = object({
45760
- type: constant("quadratic"),
45761
- coords: optional(nullable(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))),
45762
- startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
45763
- // TODO: remove coord? it's legacy.
45764
- coord: optional(pairOfNumbers)
45765
- });
45766
- const parsePerseusGraphTypeRay = object({
45767
- type: constant("ray"),
45768
- coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
45769
- startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
45770
- // TODO: remove coord? it's legacy.
45771
- coord: optional(pairOfNumbers)
45772
- });
45773
- const parsePerseusGraphTypeSegment = object({
45774
- type: constant("segment"),
45775
- // TODO(benchristel): default numSegments?
45776
- numSegments: optional(number),
45777
- coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
45778
- startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
45779
- // TODO: remove coord? it's legacy.
45780
- coord: optional(pairOfNumbers)
45781
- });
45782
- const parsePerseusGraphTypeSinusoid = object({
45783
- type: constant("sinusoid"),
45784
- coords: optional(nullable(array(pairOfNumbers))),
45785
- startCoords: optional(array(pairOfNumbers)),
45786
- // TODO: remove coord? it's legacy.
45787
- coord: optional(pairOfNumbers)
45788
- });
45789
- 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;
45790
- const parseLockedFigureColor = enumeration(...lockedFigureColorNames);
45791
- const parseLockedFigureFillType = enumeration("none", "white", "translucent", "solid");
45792
- const parseLockedLineStyle = enumeration("solid", "dashed");
45793
- const parseLockedLabelType = object({
45794
- type: constant("label"),
45795
- coord: pairOfNumbers,
45796
- text: string,
45797
- color: parseLockedFigureColor,
45798
- size: enumeration("small", "medium", "large")
45799
- });
45800
- const parseLockedPointType = object({
45801
- type: constant("point"),
45802
- coord: pairOfNumbers,
45803
- color: parseLockedFigureColor,
45804
- filled: boolean,
45805
- // TODO(benchristel): default labels to empty array?
45806
- labels: optional(array(parseLockedLabelType)),
45807
- ariaLabel: optional(string)
45808
- });
45809
- const parseLockedLineType = object({
45810
- type: constant("line"),
45811
- kind: enumeration("line", "ray", "segment"),
45812
- points: pair(parseLockedPointType, parseLockedPointType),
45813
- color: parseLockedFigureColor,
45814
- lineStyle: parseLockedLineStyle,
45815
- showPoint1: defaulted(boolean, () => false),
45816
- showPoint2: defaulted(boolean, () => false),
45817
- // TODO(benchristel): default labels to empty array?
45818
- labels: optional(array(parseLockedLabelType)),
45819
- ariaLabel: optional(string)
45820
- });
45821
- const parseLockedVectorType = object({
45822
- type: constant("vector"),
45823
- points: pair(pairOfNumbers, pairOfNumbers),
45824
- color: parseLockedFigureColor,
45825
- // TODO(benchristel): default labels to empty array?
45826
- labels: optional(array(parseLockedLabelType)),
45827
- ariaLabel: optional(string)
45828
- });
45829
- const parseLockedEllipseType = object({
45830
- type: constant("ellipse"),
45831
- center: pairOfNumbers,
45832
- radius: pairOfNumbers,
45833
- angle: number,
45834
- color: parseLockedFigureColor,
45835
- fillStyle: parseLockedFigureFillType,
45836
- strokeStyle: parseLockedLineStyle,
45837
- // TODO(benchristel): default labels to empty array?
45838
- labels: optional(array(parseLockedLabelType)),
45839
- ariaLabel: optional(string)
45840
- });
45841
- const parseLockedPolygonType = object({
45842
- type: constant("polygon"),
45843
- points: array(pairOfNumbers),
45844
- color: parseLockedFigureColor,
45845
- showVertices: boolean,
45846
- fillStyle: parseLockedFigureFillType,
45847
- strokeStyle: parseLockedLineStyle,
45848
- // TODO(benchristel): default labels to empty array?
45849
- labels: optional(array(parseLockedLabelType)),
45850
- ariaLabel: optional(string)
45851
- });
45852
- const parseLockedFunctionType = object({
45853
- type: constant("function"),
45854
- color: parseLockedFigureColor,
45855
- strokeStyle: parseLockedLineStyle,
45856
- equation: string,
45857
- directionalAxis: enumeration("x", "y"),
45858
- domain: optional(pairOfNumbers),
45859
- // TODO(benchristel): default labels to empty array?
45860
- labels: optional(array(parseLockedLabelType)),
45861
- ariaLabel: optional(string)
45862
- });
45863
- 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;
45864
- const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
45865
- step: pairOfNumbers,
45866
- // TODO(benchristel): rather than making gridStep and snapStep
45867
- // optional, we should duplicate the defaulting logic from the
45868
- // InteractiveGraph component. See parse-perseus-json/README.md for
45869
- // why.
45870
- gridStep: optional(pairOfNumbers),
45871
- snapStep: optional(pairOfNumbers),
45872
- backgroundImage: optional(parsePerseusImageBackground),
45873
- markings: enumeration("graph", "grid", "none"),
45874
- labels: optional(array(string)),
45875
- showProtractor: boolean,
45876
- showRuler: optional(boolean),
45877
- showTooltips: optional(boolean),
45878
- rulerLabel: optional(string),
45879
- rulerTicks: optional(number),
45880
- range: pair(pairOfNumbers, pairOfNumbers),
45881
- // NOTE(benchristel): I copied the default graph from
45882
- // interactive-graph.tsx. See the parse-perseus-json/README.md for
45883
- // an explanation of why we want to duplicate the default here.
45884
- graph: defaulted(parsePerseusGraphType, () => ({
45885
- type: "linear"
45886
- })),
45887
- correct: parsePerseusGraphType,
45888
- // TODO(benchristel): default lockedFigures to empty array
45889
- lockedFigures: optional(array(parseLockedFigure)),
45890
- fullGraphLabel: optional(string),
45891
- fullGraphAriaDescription: optional(string)
45892
- }));
45893
-
45894
- const parseLabelImageWidget = parseWidget(constant("label-image"), object({
45895
- choices: array(string),
45896
- imageUrl: string,
45897
- imageAlt: string,
45898
- imageHeight: number,
45899
- imageWidth: number,
45900
- markers: array(object({
45901
- answers: array(string),
45902
- label: string,
45903
- x: number,
45904
- y: number
45905
- })),
45906
- hideChoicesFromInstructions: boolean,
45907
- multipleAnswers: boolean,
45908
- static: defaulted(boolean, () => false)
45909
- }));
45910
-
45911
- const parseMatcherWidget = parseWidget(constant("matcher"), object({
45912
- labels: array(string),
45913
- left: array(string),
45914
- right: array(string),
45915
- orderMatters: boolean,
45916
- padding: boolean
45917
- }));
45918
-
45919
- const numberOrString = union(number).or(string).parser;
45920
- const numeric = pipeParsers(defaulted(numberOrString, () => NaN)).then(stringToNumber).parser;
45921
- const parseMatrixWidget = parseWidget(defaulted(constant("matrix"), () => "matrix"), object({
45922
- prefix: optional(string),
45923
- suffix: optional(string),
45924
- answers: array(array(numeric)),
45925
- cursorPosition: optional(array(number)),
45926
- matrixBoardSize: array(number),
45927
- static: optional(boolean)
45928
- }));
45929
-
45930
- const parseMeasurerWidget = parseWidget(constant("measurer"), object({
45931
- // The default value for image comes from measurer.tsx.
45932
- // See parse-perseus-json/README.md for why we want to duplicate the
45933
- // defaults here.
45934
- image: defaulted(parsePerseusImageBackground, () => ({
45935
- url: null,
45936
- top: 0,
45937
- left: 0
45938
- })),
45939
- showProtractor: boolean,
45940
- showRuler: boolean,
45941
- rulerLabel: string,
45942
- rulerTicks: number,
45943
- rulerPixels: number,
45944
- rulerLength: number,
45945
- box: pair(number, number),
45946
- // TODO(benchristel): static is not used. Remove it?
45947
- static: defaulted(boolean, () => false)
45948
- }));
45949
-
45950
- const parseMoleculeRendererWidget = parseWidget(constant("molecule-renderer"), object({
45951
- widgetId: string,
45952
- rotationAngle: optional(number),
45953
- smiles: optional(string)
45954
- }));
45955
-
45956
- const emptyStringToNull = pipeParsers(constant("")).then(convert(() => null)).parser;
45957
- const parseNumberLineWidget = parseWidget(constant("number-line"), object({
45958
- range: array(number),
45959
- labelRange: array(nullable(union(number).or(emptyStringToNull).parser)),
45960
- labelStyle: string,
45961
- labelTicks: boolean,
45962
- isTickCtrl: optional(nullable(boolean)),
45963
- divisionRange: array(number),
45964
- numDivisions: optional(nullable(number)),
45965
- // NOTE(benchristel): I copied the default snapDivisions from
45966
- // number-line.tsx. See the parse-perseus-json/README.md for
45967
- // an explanation of why we want to duplicate the default here.
45968
- snapDivisions: defaulted(number, () => 2),
45969
- tickStep: optional(nullable(number)),
45970
- correctRel: optional(nullable(string)),
45971
- correctX: nullable(number),
45972
- initialX: optional(nullable(number)),
45973
- showTooltip: optional(boolean),
45974
- static: defaulted(boolean, () => false)
45975
- }));
45976
-
45977
- const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "decimal", "percent", "pi");
45978
- const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
45979
- answers: array(object({
45980
- message: string,
45981
- // TODO(benchristel): value should never be null or undefined,
45982
- // but we have some content where it is anyway. If we backfill
45983
- // the data, simplify this.
45984
- value: optional(nullable(number)),
45985
- status: string,
45986
- answerForms: optional(array(parseMathFormat)),
45987
- strict: boolean,
45988
- maxError: optional(nullable(number)),
45989
- // TODO(benchristel): simplify should never be a boolean, but we
45990
- // have some content where it is anyway. If we ever backfill
45991
- // the data, we should simplify `simplify`.
45992
- simplify: optional(nullable(union(string).or(pipeParsers(boolean).then(convert(String)).parser).parser))
45993
- })),
45994
- labelText: optional(string),
45995
- size: string,
45996
- coefficient: defaulted(boolean, () => false),
45997
- rightAlign: optional(boolean),
45998
- static: defaulted(boolean, () => false),
45999
- answerForms: optional(array(object({
46000
- name: parseMathFormat,
46001
- simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
46002
- })))
46003
- }));
46004
-
46005
- // There is an import cycle between orderer-widget.ts and perseus-renderer.ts.
46006
- // This wrapper ensures that we don't refer to parsePerseusRenderer before
46007
- // it's defined.
46008
- function parseRenderer(rawValue, ctx) {
46009
- return parsePerseusRenderer(rawValue, ctx);
46010
- }
46011
- const largeToAuto = (height, ctx) => {
46012
- if (height === "large") {
46013
- return ctx.success("auto");
46014
- }
46015
- return ctx.success(height);
46016
- };
46017
- const parseOrdererWidget = parseWidget(constant("orderer"), object({
46018
- options: defaulted(array(parseRenderer), () => []),
46019
- correctOptions: array(parseRenderer),
46020
- otherOptions: array(parseRenderer),
46021
- height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
46022
- layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
46023
- }));
46024
-
46025
- const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
46026
- passageNumber: number,
46027
- referenceNumber: number,
46028
- summaryText: optional(string)
46029
- }));
46030
-
46031
- const parsePassageWidget = parseWidget(constant("passage"), object({
46032
- footnotes: defaulted(string, () => ""),
46033
- passageText: string,
46034
- passageTitle: defaulted(string, () => ""),
46035
- showLineNumbers: boolean,
46036
- static: defaulted(boolean, () => false)
46037
- }));
46038
-
46039
- const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
46040
- url: string,
46041
- description: string
46042
- }));
46043
-
46044
- const parsePlotterWidget = parseWidget(constant("plotter"), object({
46045
- labels: array(string),
46046
- categories: array(string),
46047
- type: enumeration(...plotterPlotTypes),
46048
- maxY: number,
46049
- // The default value for scaleY comes from plotter.tsx.
46050
- // See parse-perseus-json/README.md for why we want to duplicate the
46051
- // defaults here.
46052
- scaleY: defaulted(number, () => 1),
46053
- labelInterval: optional(nullable(number)),
46054
- // The default value for snapsPerLine comes from plotter.tsx.
46055
- // See parse-perseus-json/README.md for why we want to duplicate the
46056
- // defaults here.
46057
- snapsPerLine: defaulted(number, () => 2),
46058
- starting: array(number),
46059
- correct: array(number),
46060
- picUrl: optional(nullable(string)),
46061
- picSize: optional(nullable(number)),
46062
- picBoxHeight: optional(nullable(number)),
46063
- // NOTE(benchristel): I copied the default plotDimensions from
46064
- // plotter.tsx. See the parse-perseus-json/README.md for an explanation
46065
- // of why we want to duplicate the defaults here.
46066
- plotDimensions: defaulted(array(number), () => [380, 300])
46067
- }));
46068
-
46069
- const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
46070
- programID: string,
46071
- height: number
46072
- }));
46073
-
46074
- const parseRadioWidget = parseWidget(constant("radio"), object({
46075
- choices: array(object({
46076
- content: defaulted(string, () => ""),
46077
- clue: optional(string),
46078
- correct: optional(boolean),
46079
- isNoneOfTheAbove: optional(boolean),
46080
- // deprecated
46081
- // There is an import cycle between radio-widget.ts and
46082
- // widgets-map.ts. The anonymous function below ensures that we
46083
- // don't refer to parseWidgetsMap before it's defined.
46084
- widgets: optional((rawVal, ctx) => parseWidgetsMap(rawVal, ctx))
46085
- })),
46086
- hasNoneOfTheAbove: optional(boolean),
46087
- countChoices: optional(boolean),
46088
- randomize: optional(boolean),
46089
- multipleSelect: optional(boolean),
46090
- deselectEnabled: optional(boolean),
46091
- // deprecated
46092
- onePerLine: optional(boolean),
46093
- // deprecated
46094
- displayCount: optional(any),
46095
- // v0 props
46096
- // `noneOfTheAbove` is still in use (but only set to `false`).
46097
- noneOfTheAbove: optional(constant(false))
46098
- }));
46099
-
46100
- const parseSorterWidget = parseWidget(constant("sorter"), object({
46101
- correct: array(string),
46102
- padding: boolean,
46103
- layout: enumeration("horizontal", "vertical")
46104
- }));
46105
-
46106
- const parseTableWidget = parseWidget(constant("table"), object({
46107
- headers: array(string),
46108
- rows: number,
46109
- columns: number,
46110
- answers: array(array(string))
46111
- }));
46112
-
46113
- const parseVideoWidget = parseWidget(constant("video"), object({
46114
- location: string,
46115
- static: optional(boolean)
46116
- }));
46117
-
46118
- const parseWidgetsMap = (rawValue, ctx) => {
46119
- if (!isObject(rawValue)) {
46120
- return ctx.failure("PerseusWidgetsMap", rawValue);
46121
- }
46122
- const widgetsMap = {};
46123
- for (const key of Object.keys(rawValue)) {
46124
- // parseWidgetsMapEntry modifies the widgetsMap. This is kind of gross,
46125
- // but it's the only way I could find to make TypeScript check the key
46126
- // against the widget type.
46127
- const entryResult = parseWidgetsMapEntry([key, rawValue[key]], widgetsMap, ctx.forSubtree(key));
46128
- if (isFailure(entryResult)) {
46129
- return entryResult;
46130
- }
46131
- }
46132
- return ctx.success(widgetsMap);
46133
- };
46134
- const parseWidgetsMapEntry = ([id, widget], widgetMap, ctx) => {
46135
- const idComponentsResult = parseWidgetIdComponents(id.split(" "), ctx.forSubtree("(widget ID)"));
46136
- if (isFailure(idComponentsResult)) {
46137
- return idComponentsResult;
46138
- }
46139
- const [type, n] = idComponentsResult.value;
46140
- function parseAndAssign(key, parse) {
46141
- const widgetResult = parse(widget, ctx);
46142
- if (isFailure(widgetResult)) {
46143
- return widgetResult;
46144
- }
46145
- widgetMap[key] = widgetResult.value;
46146
- return ctx.success(undefined);
46147
- }
46148
- switch (type) {
46149
- case "categorizer":
46150
- return parseAndAssign(`categorizer ${n}`, parseCategorizerWidget);
46151
- case "cs-program":
46152
- return parseAndAssign(`cs-program ${n}`, parseCSProgramWidget);
46153
- case "definition":
46154
- return parseAndAssign(`definition ${n}`, parseDefinitionWidget);
46155
- case "dropdown":
46156
- return parseAndAssign(`dropdown ${n}`, parseDropdownWidget);
46157
- case "explanation":
46158
- return parseAndAssign(`explanation ${n}`, parseExplanationWidget);
46159
- case "expression":
46160
- return parseAndAssign(`expression ${n}`, parseExpressionWidget);
46161
- case "grapher":
46162
- return parseAndAssign(`grapher ${n}`, parseGrapherWidget);
46163
- case "group":
46164
- return parseAndAssign(`group ${n}`, parseGroupWidget);
46165
- case "graded-group":
46166
- return parseAndAssign(`graded-group ${n}`, parseGradedGroupWidget);
46167
- case "graded-group-set":
46168
- return parseAndAssign(`graded-group-set ${n}`, parseGradedGroupSetWidget);
46169
- case "iframe":
46170
- return parseAndAssign(`iframe ${n}`, parseIframeWidget);
46171
- case "image":
46172
- return parseAndAssign(`image ${n}`, parseImageWidget);
46173
- case "input-number":
46174
- return parseAndAssign(`input-number ${n}`, parseInputNumberWidget);
46175
- case "interaction":
46176
- return parseAndAssign(`interaction ${n}`, parseInteractionWidget);
46177
- case "interactive-graph":
46178
- return parseAndAssign(`interactive-graph ${n}`, parseInteractiveGraphWidget);
46179
- case "label-image":
46180
- return parseAndAssign(`label-image ${n}`, parseLabelImageWidget);
46181
- case "matcher":
46182
- return parseAndAssign(`matcher ${n}`, parseMatcherWidget);
46183
- case "matrix":
46184
- return parseAndAssign(`matrix ${n}`, parseMatrixWidget);
46185
- case "measurer":
46186
- return parseAndAssign(`measurer ${n}`, parseMeasurerWidget);
46187
- case "molecule-renderer":
46188
- return parseAndAssign(`molecule-renderer ${n}`, parseMoleculeRendererWidget);
46189
- case "number-line":
46190
- return parseAndAssign(`number-line ${n}`, parseNumberLineWidget);
46191
- case "numeric-input":
46192
- return parseAndAssign(`numeric-input ${n}`, parseNumericInputWidget);
46193
- case "orderer":
46194
- return parseAndAssign(`orderer ${n}`, parseOrdererWidget);
46195
- case "passage":
46196
- return parseAndAssign(`passage ${n}`, parsePassageWidget);
46197
- case "passage-ref":
46198
- return parseAndAssign(`passage-ref ${n}`, parsePassageRefWidget);
46199
- case "passage-ref-target":
46200
- // NOTE(benchristel): as of 2024-11-12, passage-ref-target is only
46201
- // used in test content. See:
46202
- // https://www.khanacademy.org/devadmin/content/search?query=widget:passage-ref-target
46203
- return parseAndAssign(`passage-ref-target ${n}`, any);
46204
- case "phet-simulation":
46205
- return parseAndAssign(`phet-simulation ${n}`, parsePhetSimulationWidget);
46206
- case "plotter":
46207
- return parseAndAssign(`plotter ${n}`, parsePlotterWidget);
46208
- case "python-program":
46209
- return parseAndAssign(`python-program ${n}`, parsePythonProgramWidget);
46210
- case "radio":
46211
- return parseAndAssign(`radio ${n}`, parseRadioWidget);
46212
- case "sorter":
46213
- return parseAndAssign(`sorter ${n}`, parseSorterWidget);
46214
- case "table":
46215
- return parseAndAssign(`table ${n}`, parseTableWidget);
46216
- case "video":
46217
- return parseAndAssign(`video ${n}`, parseVideoWidget);
46218
- case "sequence":
46219
- // sequence is a deprecated widget type, and the corresponding
46220
- // widget component no longer exists.
46221
- return parseAndAssign(`sequence ${n}`, parseDeprecatedWidget);
46222
- case "lights-puzzle":
46223
- return parseAndAssign(`lights-puzzle ${n}`, parseDeprecatedWidget);
46224
- case "simulator":
46225
- return parseAndAssign(`simulator ${n}`, parseDeprecatedWidget);
46226
- case "transformer":
46227
- return parseAndAssign(`transformer ${n}`, parseDeprecatedWidget);
46228
- default:
46229
- if (getWidget(type)) {
46230
- // @ts-expect-error - 'type' is not a valid widget type
46231
- return parseAndAssign(`${type} ${n}`, any);
46232
- }
46233
- return ctx.failure("a valid widget type", type);
46234
- }
46235
- };
46236
- const parseDeprecatedWidget = parseWidget(
46237
- // Ignore the incoming widget type and hardcode "deprecated-standin"
46238
- (_, ctx) => ctx.success("deprecated-standin"),
46239
- // Allow any widget options
46240
- object({}));
46241
- const parseStringToPositiveInt = (rawValue, ctx) => {
46242
- if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
46243
- return ctx.failure("a string representing a positive integer", rawValue);
46244
- }
46245
- return ctx.success(+rawValue);
46246
- };
46247
- const parseWidgetIdComponents = pair(string, parseStringToPositiveInt);
46248
-
46249
- const parsePerseusRenderer = defaulted(object({
46250
- // TODO(benchristel): content is also defaulted to empty string in
46251
- // renderer.tsx. See if we can remove one default or the other.
46252
- content: defaulted(string, () => ""),
46253
- // This module has an import cycle with parseWidgetsMap, because the
46254
- // `group` widget can contain another renderer.
46255
- // The anonymous function below ensures that we don't try to access
46256
- // parseWidgetsMap before it's defined.
46257
- widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
46258
- metadata: optional(array(string)),
46259
- images: parseImages
46260
- }),
46261
- // Default value
46262
- () => ({
46263
- content: "",
46264
- widgets: {},
46265
- images: {}
46266
- }));
46267
-
46268
- const parsePerseusArticle = union(parsePerseusRenderer).or(array(parsePerseusRenderer)).parser;
46269
-
46270
- const parseHint = object({
46271
- replace: optional(boolean),
46272
- content: string,
46273
- widgets: defaulted(parseWidgetsMap, () => ({})),
46274
- metadata: optional(array(string)),
46275
- images: parseImages
46276
- });
46277
-
46278
- const _excluded = ["type", "options"];
46279
- const parsePerseusItem$1 = object({
46280
- question: parsePerseusRenderer,
46281
- hints: defaulted(array(parseHint), () => []),
46282
- answerArea: pipeParsers(defaulted(object({}), () => ({}))).then(migrateAnswerArea).then(record(enumeration(...ItemExtras), boolean)).parser,
46283
- itemDataVersion: optional(object({
46284
- major: number,
46285
- minor: number
46286
- })),
46287
- // Deprecated field
46288
- answer: any
46289
- });
46290
-
46291
- // Some answerAreas have extra fields, like:
46292
- //
46293
- // "answerArea": {
46294
- // "type": "multiple",
46295
- // "options": {
46296
- // "content": "",
46297
- // "images": {},
46298
- // "widgets": {}
46299
- // }
46300
- // }
46301
- //
46302
- // The "type" and "options" fields don't seem to be used anywhere. This
46303
- // migration function removes them.
46304
- function migrateAnswerArea(rawValue, ctx) {
46305
- const rest = _objectWithoutPropertiesLoose(rawValue, _excluded);
46306
- return ctx.success(rest);
46307
- }
46308
-
46309
- /**
46310
- * Helper to parse PerseusItem JSON
46311
- * Why not just use JSON.parse? We want:
46312
- * - To make sure types are correct
46313
- * - To give us a central place to validate/transform output if needed
46314
- * @deprecated - use parseAndMigratePerseusItem instead
46315
- * @param {string} json - the stringified PerseusItem JSON
46316
- * @returns {PerseusItem} the parsed PerseusItem object
46317
- */
46318
- function parsePerseusItem(json) {
46319
- // Try to block a cheating vector which relies on monkey-patching
46320
- // JSON.parse
46321
- if (isRealJSONParse(JSON.parse)) {
46322
- return JSON.parse(json);
46323
- }
46324
- throw new Error("Something went wrong.");
46325
- }
46326
- /**
46327
- * Parses a PerseusItem from a JSON string, migrates old formats to the latest
46328
- * schema, and runtime-typechecks the result. Use this to parse assessmentItem
46329
- * data.
46330
- *
46331
- * @returns a {@link Result} of the parsed PerseusItem. If the result is a
46332
- * failure, it will contain an error message describing where in the tree
46333
- * parsing failed.
46334
- * @throws SyntaxError if the argument is not well-formed JSON.
46335
- */
46336
- function parseAndMigratePerseusItem(json) {
46337
- throwErrorIfCheatingDetected();
46338
- const object = JSON.parse(json);
46339
- const result = parse(object, parsePerseusItem$1);
46340
- if (isFailure(result)) {
46341
- return failure({
46342
- message: result.detail,
46343
- invalidObject: object
46344
- });
46345
- }
46346
- return result;
46347
- }
46348
-
46349
- /**
46350
- * Parses a PerseusArticle from a JSON string, migrates old formats to the
46351
- * latest schema, and runtime-typechecks the result.
46352
- *
46353
- * @returns a {@link Result} of the parsed PerseusArticle. If the result is a
46354
- * failure, it will contain an error message describing where in the tree
46355
- * parsing failed.
46356
- * @throws SyntaxError if the argument is not well-formed JSON.
46357
- */
46358
- function parseAndMigratePerseusArticle(json) {
46359
- throwErrorIfCheatingDetected();
46360
- const object = JSON.parse(json);
46361
- const result = parse(object, parsePerseusArticle);
46362
- if (isFailure(result)) {
46363
- return failure({
46364
- message: result.detail,
46365
- invalidObject: object
46366
- });
46367
- }
46368
- return result;
46369
- }
46370
-
46371
- /**
46372
- * Tries to block a cheating vector that relies on monkey-patching JSON.parse.
46373
- */
46374
- // TODO(LEMS-2331): delete this function once server-side scoring is done.
46375
- function throwErrorIfCheatingDetected() {
46376
- if (!isRealJSONParse(JSON.parse)) {
46377
- throw new Error("Something went wrong.");
46378
- }
46379
- }
46380
-
46381
44867
  const genericPerseusItemData = {
46382
44868
  question: {
46383
44869
  content: "",
@@ -46595,5 +45081,5 @@ const ScoringUtil = {
46595
45081
  * Types
46596
45082
  */
46597
45083
 
46598
- export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, JiptParagraphs, KhanColors, context as LoadingContext, Log, Matrix$1 as MatrixWidget, NumericInput$1 as NumericInput, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ScoringUtil, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, addWidget, apiVersion, bodyXsmallBold, components, containerSizeClass, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isFailure, isItemRenderableByVersion, isSuccess, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, ItemVersion as itemVersion, libVersion, makeSafeUrl, mathOnlyParser, mediaQueries, parseAndMigratePerseusArticle, parseAndMigratePerseusItem, parseDataFromJSONP, parsePerseusItem, preprocessTex, registerAllWidgetsForTesting, scorePerseusItem, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
45084
+ export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, JiptParagraphs, KhanColors, context as LoadingContext, Log, Matrix$1 as MatrixWidget, NumericInput$1 as NumericInput, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, Radio, Renderer, ScoringUtil, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, apiVersion, bodyXsmallBold, components, containerSizeClass, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, ItemVersion as itemVersion, libVersion, makeSafeUrl, mathOnlyParser, mediaQueries, parseDataFromJSONP, preprocessTex, registerAllWidgetsForTesting, scorePerseusItem, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
46599
45085
  //# sourceMappingURL=index.js.map