@khanacademy/perseus-core 0.0.0-PR862-20231207182234 → 0.0.0-PR875-20250222011102

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 (170) hide show
  1. package/LICENSE +18 -0
  2. package/dist/analytics.d.ts +37 -1
  3. package/dist/data-schema.d.ts +1033 -0
  4. package/dist/error/errors.d.ts +41 -0
  5. package/dist/error/perseus-error.d.ts +11 -0
  6. package/dist/es/index.js +3804 -36
  7. package/dist/es/index.js.map +1 -1
  8. package/dist/index.d.ts +103 -2
  9. package/dist/index.js +3921 -35
  10. package/dist/index.js.map +1 -1
  11. package/dist/parse-perseus-json/error-tracking-parse-context.d.ts +9 -0
  12. package/dist/parse-perseus-json/exhaustive-test-tool/index.d.ts +2 -0
  13. package/dist/parse-perseus-json/general-purpose-parsers/any.d.ts +2 -0
  14. package/dist/parse-perseus-json/general-purpose-parsers/array.d.ts +2 -0
  15. package/dist/parse-perseus-json/general-purpose-parsers/array.typetest.d.ts +1 -0
  16. package/dist/parse-perseus-json/general-purpose-parsers/boolean.d.ts +2 -0
  17. package/dist/parse-perseus-json/general-purpose-parsers/constant.d.ts +4 -0
  18. package/dist/parse-perseus-json/general-purpose-parsers/convert.d.ts +2 -0
  19. package/dist/parse-perseus-json/general-purpose-parsers/defaulted.d.ts +2 -0
  20. package/dist/parse-perseus-json/general-purpose-parsers/discriminated-union.d.ts +21 -0
  21. package/dist/parse-perseus-json/general-purpose-parsers/discriminated-union.typetest.d.ts +1 -0
  22. package/dist/parse-perseus-json/general-purpose-parsers/enumeration.d.ts +2 -0
  23. package/dist/parse-perseus-json/general-purpose-parsers/enumeration.typetest.d.ts +1 -0
  24. package/dist/parse-perseus-json/general-purpose-parsers/index.d.ts +16 -0
  25. package/dist/parse-perseus-json/general-purpose-parsers/is-object.d.ts +1 -0
  26. package/dist/parse-perseus-json/general-purpose-parsers/nullable.d.ts +2 -0
  27. package/dist/parse-perseus-json/general-purpose-parsers/number.d.ts +2 -0
  28. package/dist/parse-perseus-json/general-purpose-parsers/object.d.ts +6 -0
  29. package/dist/parse-perseus-json/general-purpose-parsers/optional.d.ts +2 -0
  30. package/dist/parse-perseus-json/general-purpose-parsers/pair.d.ts +2 -0
  31. package/dist/parse-perseus-json/general-purpose-parsers/pipe-parsers.d.ts +7 -0
  32. package/dist/parse-perseus-json/general-purpose-parsers/pipe-parsers.typetest.d.ts +1 -0
  33. package/dist/parse-perseus-json/general-purpose-parsers/record.d.ts +2 -0
  34. package/dist/parse-perseus-json/general-purpose-parsers/string-to-number.d.ts +2 -0
  35. package/dist/parse-perseus-json/general-purpose-parsers/string.d.ts +2 -0
  36. package/dist/parse-perseus-json/general-purpose-parsers/test-helpers.d.ts +9 -0
  37. package/dist/parse-perseus-json/general-purpose-parsers/trio.d.ts +2 -0
  38. package/dist/parse-perseus-json/general-purpose-parsers/union.d.ts +7 -0
  39. package/dist/parse-perseus-json/general-purpose-parsers/union.typetest.d.ts +1 -0
  40. package/dist/parse-perseus-json/index.d.ts +45 -0
  41. package/dist/parse-perseus-json/object-path.d.ts +2 -0
  42. package/dist/parse-perseus-json/parse-failure-detail.d.ts +2 -0
  43. package/dist/parse-perseus-json/parse.d.ts +3 -0
  44. package/dist/parse-perseus-json/parser-types.d.ts +30 -0
  45. package/dist/parse-perseus-json/perseus-parsers/categorizer-widget.d.ts +3 -0
  46. package/dist/parse-perseus-json/perseus-parsers/cs-program-widget.d.ts +3 -0
  47. package/dist/parse-perseus-json/perseus-parsers/definition-widget.d.ts +3 -0
  48. package/dist/parse-perseus-json/perseus-parsers/dropdown-widget.d.ts +3 -0
  49. package/dist/parse-perseus-json/perseus-parsers/explanation-widget.d.ts +3 -0
  50. package/dist/parse-perseus-json/perseus-parsers/expression-widget.d.ts +3 -0
  51. package/dist/parse-perseus-json/perseus-parsers/graded-group-set-widget.d.ts +3 -0
  52. package/dist/parse-perseus-json/perseus-parsers/graded-group-widget.d.ts +16 -0
  53. package/dist/parse-perseus-json/perseus-parsers/grapher-widget.d.ts +3 -0
  54. package/dist/parse-perseus-json/perseus-parsers/group-widget.d.ts +3 -0
  55. package/dist/parse-perseus-json/perseus-parsers/hint.d.ts +3 -0
  56. package/dist/parse-perseus-json/perseus-parsers/iframe-widget.d.ts +3 -0
  57. package/dist/parse-perseus-json/perseus-parsers/image-widget.d.ts +3 -0
  58. package/dist/parse-perseus-json/perseus-parsers/images-map.d.ts +5 -0
  59. package/dist/parse-perseus-json/perseus-parsers/input-number-widget.d.ts +3 -0
  60. package/dist/parse-perseus-json/perseus-parsers/interaction-widget.d.ts +3 -0
  61. package/dist/parse-perseus-json/perseus-parsers/interactive-graph-widget.d.ts +3 -0
  62. package/dist/parse-perseus-json/perseus-parsers/label-image-widget.d.ts +3 -0
  63. package/dist/parse-perseus-json/perseus-parsers/legacy-button-sets.d.ts +2 -0
  64. package/dist/parse-perseus-json/perseus-parsers/legacy-button-sets.typetest.d.ts +1 -0
  65. package/dist/parse-perseus-json/perseus-parsers/matcher-widget.d.ts +3 -0
  66. package/dist/parse-perseus-json/perseus-parsers/matrix-widget.d.ts +3 -0
  67. package/dist/parse-perseus-json/perseus-parsers/measurer-widget.d.ts +3 -0
  68. package/dist/parse-perseus-json/perseus-parsers/molecule-renderer-widget.d.ts +3 -0
  69. package/dist/parse-perseus-json/perseus-parsers/number-line-widget.d.ts +3 -0
  70. package/dist/parse-perseus-json/perseus-parsers/numeric-input-widget.d.ts +3 -0
  71. package/dist/parse-perseus-json/perseus-parsers/orderer-widget.d.ts +3 -0
  72. package/dist/parse-perseus-json/perseus-parsers/passage-ref-widget.d.ts +3 -0
  73. package/dist/parse-perseus-json/perseus-parsers/passage-widget.d.ts +3 -0
  74. package/dist/parse-perseus-json/perseus-parsers/perseus-answer-area.d.ts +2 -0
  75. package/dist/parse-perseus-json/perseus-parsers/perseus-article.d.ts +3 -0
  76. package/dist/parse-perseus-json/perseus-parsers/perseus-image-background.d.ts +3 -0
  77. package/dist/parse-perseus-json/perseus-parsers/perseus-item.d.ts +3 -0
  78. package/dist/parse-perseus-json/perseus-parsers/perseus-renderer.d.ts +3 -0
  79. package/dist/parse-perseus-json/perseus-parsers/phet-simulation-widget.d.ts +3 -0
  80. package/dist/parse-perseus-json/perseus-parsers/plotter-widget.d.ts +3 -0
  81. package/dist/parse-perseus-json/perseus-parsers/python-program-widget.d.ts +3 -0
  82. package/dist/parse-perseus-json/perseus-parsers/radio-widget.d.ts +3 -0
  83. package/dist/parse-perseus-json/perseus-parsers/sorter-widget.d.ts +3 -0
  84. package/dist/parse-perseus-json/perseus-parsers/table-widget.d.ts +3 -0
  85. package/dist/parse-perseus-json/perseus-parsers/versioned-widget-options.d.ts +36 -0
  86. package/dist/parse-perseus-json/perseus-parsers/video-widget.d.ts +3 -0
  87. package/dist/parse-perseus-json/perseus-parsers/widget.d.ts +7 -0
  88. package/dist/parse-perseus-json/perseus-parsers/widgets-map.d.ts +3 -0
  89. package/dist/parse-perseus-json/result.d.ts +23 -0
  90. package/dist/types.d.ts +13 -1
  91. package/dist/utils/deep-clone.d.ts +3 -0
  92. package/dist/utils/equality.d.ts +9 -0
  93. package/dist/utils/get-decimal-separator.d.ts +5 -0
  94. package/dist/utils/get-matrix-size.d.ts +2 -0
  95. package/dist/utils/grapher-types.d.ts +58 -0
  96. package/dist/utils/grapher-util.d.ts +20 -0
  97. package/dist/utils/is-real-json-parse.d.ts +1 -0
  98. package/dist/utils/objective_.d.ts +30 -0
  99. package/dist/utils/random-util.d.ts +5 -0
  100. package/dist/utils/split-perseus-item.d.ts +2 -0
  101. package/dist/utils/widget-id-utils.d.ts +38 -0
  102. package/dist/widgets/categorizer/categorizer-util.d.ts +17 -0
  103. package/dist/widgets/categorizer/index.d.ts +5 -0
  104. package/dist/widgets/core-widget-registry.d.ts +30 -0
  105. package/dist/widgets/cs-program/cs-program-util.d.ts +2 -0
  106. package/dist/widgets/cs-program/index.d.ts +5 -0
  107. package/dist/widgets/definition/index.d.ts +5 -0
  108. package/dist/widgets/dropdown/dropdown-util.d.ts +20 -0
  109. package/dist/widgets/dropdown/index.d.ts +5 -0
  110. package/dist/widgets/explanation/index.d.ts +5 -0
  111. package/dist/widgets/expression/expression-upgrade.d.ts +10 -0
  112. package/dist/widgets/expression/expression-util.d.ts +19 -0
  113. package/dist/widgets/expression/index.d.ts +4 -0
  114. package/dist/widgets/graded-group/index.d.ts +5 -0
  115. package/dist/widgets/graded-group-set/index.d.ts +5 -0
  116. package/dist/widgets/grapher/grapher-util.d.ts +4 -0
  117. package/dist/widgets/grapher/index.d.ts +5 -0
  118. package/dist/widgets/group/index.d.ts +5 -0
  119. package/dist/widgets/iframe/iframe-util.d.ts +2 -0
  120. package/dist/widgets/iframe/index.d.ts +5 -0
  121. package/dist/widgets/image/index.d.ts +5 -0
  122. package/dist/widgets/input-number/index.d.ts +5 -0
  123. package/dist/widgets/interaction/index.d.ts +5 -0
  124. package/dist/widgets/interactive-graph/index.d.ts +5 -0
  125. package/dist/widgets/interactive-graph/interactive-graph-util.d.ts +3 -0
  126. package/dist/widgets/label-image/index.d.ts +5 -0
  127. package/dist/widgets/label-image/label-image-util.d.ts +19 -0
  128. package/dist/widgets/logic-export.types.d.ts +42 -0
  129. package/dist/widgets/matcher/index.d.ts +5 -0
  130. package/dist/widgets/matcher/matcher-util.d.ts +28 -0
  131. package/dist/widgets/matrix/index.d.ts +5 -0
  132. package/dist/widgets/matrix/matrix-util.d.ts +4 -0
  133. package/dist/widgets/measurer/index.d.ts +4 -0
  134. package/dist/widgets/measurer/measurer-upgrade.d.ts +10 -0
  135. package/dist/widgets/number-line/index.d.ts +5 -0
  136. package/dist/widgets/number-line/number-line-util.d.ts +4 -0
  137. package/dist/widgets/numeric-input/index.d.ts +5 -0
  138. package/dist/widgets/numeric-input/numeric-input-util.d.ts +19 -0
  139. package/dist/widgets/orderer/index.d.ts +5 -0
  140. package/dist/widgets/orderer/orderer-util.d.ts +16 -0
  141. package/dist/widgets/passage/index.d.ts +5 -0
  142. package/dist/widgets/passage-ref/index.d.ts +4 -0
  143. package/dist/widgets/passage-ref/passage-ref-upgrade.d.ts +7 -0
  144. package/dist/widgets/passage-ref-target/index.d.ts +5 -0
  145. package/dist/widgets/phet-simulation/index.d.ts +5 -0
  146. package/dist/widgets/plotter/index.d.ts +5 -0
  147. package/dist/widgets/plotter/plotter-util.d.ts +12 -0
  148. package/dist/widgets/python-program/index.d.ts +5 -0
  149. package/dist/widgets/radio/index.d.ts +4 -0
  150. package/dist/widgets/radio/radio-upgrade.d.ts +10 -0
  151. package/dist/widgets/radio/radio-util.d.ts +26 -0
  152. package/dist/widgets/sorter/index.d.ts +5 -0
  153. package/dist/widgets/sorter/sorter-util.d.ts +17 -0
  154. package/dist/widgets/table/index.d.ts +5 -0
  155. package/dist/widgets/table/table-util.d.ts +4 -0
  156. package/dist/widgets/upgrade.d.ts +3 -0
  157. package/dist/widgets/video/index.d.ts +5 -0
  158. package/package.json +34 -27
  159. package/.babelrc.js +0 -8
  160. package/.eslintrc.js +0 -12
  161. package/CHANGELOG.md +0 -85
  162. package/src/analytics.ts +0 -64
  163. package/src/index.ts +0 -11
  164. package/src/types.ts +0 -30
  165. package/src/utils/add-library-version-to-perseus-debug.test.ts +0 -109
  166. package/src/utils/add-library-version-to-perseus-debug.ts +0 -52
  167. package/src/version.ts +0 -10
  168. package/tsconfig-build.json +0 -19
  169. package/tsconfig-build.tsbuildinfo +0 -1
  170. /package/dist/{utils → shared-utils}/add-library-version-to-perseus-debug.d.ts +0 -0
package/dist/index.js CHANGED
@@ -2,52 +2,3938 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var _ = require('underscore');
6
+ var perseusCore = require('@khanacademy/perseus-core');
7
+
8
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
9
+
10
+ var ___default = /*#__PURE__*/_interopDefaultLegacy(_);
11
+
12
+ function getMatrixSize(matrix) {
13
+ const matrixSize = [1, 1];
14
+
15
+ // We need to find the widest row and tallest column to get the correct
16
+ // matrix size.
17
+ ___default["default"](matrix).each((matrixRow, row) => {
18
+ let rowWidth = 0;
19
+ ___default["default"](matrixRow).each((matrixCol, col) => {
20
+ if (matrixCol != null && matrixCol.toString().length) {
21
+ rowWidth = col + 1;
22
+ }
23
+ });
24
+
25
+ // Matrix width:
26
+ matrixSize[1] = Math.max(matrixSize[1], rowWidth);
27
+
28
+ // Matrix height:
29
+ if (rowWidth > 0) {
30
+ matrixSize[0] = Math.max(matrixSize[0], row + 1);
31
+ }
32
+ });
33
+ return matrixSize;
34
+ }
35
+
5
36
  /**
6
- * Adds the given perseus library version information to the __perseus_debug__
7
- * object and ensures that the object is attached to `globalThis` (`window` in
8
- * browser environments).
37
+ * Get the character used for separating decimals.
38
+ */
39
+ const getDecimalSeparator = locale => {
40
+ switch (locale) {
41
+ // TODO(somewhatabstract): Remove this when Chrome supports the `ka`
42
+ // locale properly.
43
+ // https://github.com/formatjs/formatjs/issues/1526#issuecomment-559891201
44
+ //
45
+ // Supported locales in Chrome:
46
+ // https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/scripts/chrome_ui_languages.list
47
+ case "ka":
48
+ return ",";
49
+ default:
50
+ const numberWithDecimalSeparator = 1.1;
51
+ // TODO(FEI-3647): Update to use .formatToParts() once we no longer have to
52
+ // support Safari 12.
53
+ const match = new Intl.NumberFormat(locale).format(numberWithDecimalSeparator)
54
+ // 0x661 is ARABIC-INDIC DIGIT ONE
55
+ // 0x6F1 is EXTENDED ARABIC-INDIC DIGIT ONE
56
+ .match(/[^\d\u0661\u06F1]/);
57
+ return match?.[0] ?? ".";
58
+ }
59
+ };
60
+
61
+ /**
62
+ * APPROXIMATE equality on numbers and primitives.
63
+ */
64
+ function approximateEqual(x, y) {
65
+ if (typeof x === "number" && typeof y === "number") {
66
+ return Math.abs(x - y) < 1e-9;
67
+ }
68
+ return x === y;
69
+ }
70
+
71
+ /**
72
+ * Deep APPROXIMATE equality on primitives, numbers, arrays, and objects.
73
+ * Recursive.
74
+ */
75
+ function approximateDeepEqual(x, y) {
76
+ if (Array.isArray(x) && Array.isArray(y)) {
77
+ if (x.length !== y.length) {
78
+ return false;
79
+ }
80
+ for (let i = 0; i < x.length; i++) {
81
+ if (!approximateDeepEqual(x[i], y[i])) {
82
+ return false;
83
+ }
84
+ }
85
+ return true;
86
+ }
87
+ if (Array.isArray(x) || Array.isArray(y)) {
88
+ return false;
89
+ }
90
+ if (typeof x === "function" && typeof y === "function") {
91
+ return approximateEqual(x, y);
92
+ }
93
+ if (typeof x === "function" || typeof y === "function") {
94
+ return false;
95
+ }
96
+ if (typeof x === "object" && typeof y === "object" && !!x && !!y) {
97
+ return x === y || ___default["default"].all(x, function (v, k) {
98
+ // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
99
+ return approximateDeepEqual(y[k], v);
100
+ }) && ___default["default"].all(y, function (v, k) {
101
+ // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
102
+ return approximateDeepEqual(x[k], v);
103
+ });
104
+ }
105
+ if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
106
+ return false;
107
+ }
108
+ return approximateEqual(x, y);
109
+ }
110
+
111
+ /**
112
+ * Add a widget placeholder using the widget ID.
113
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
9
114
  *
10
- * This allows each library to provide runtime version information to assist in
11
- * debugging in production environments.
115
+ * @param {string} id
116
+ * @returns {string}
12
117
  */
13
- const addLibraryVersionToPerseusDebug = (libraryName, libraryVersion) => {
14
- // If the library version is the default value, then we don't want to
15
- // prefix it with a "v" to indicate that it is a version number.
16
- let prefix = "v";
17
- if (libraryVersion === "__lib_version__") {
18
- prefix = "";
118
+ function addWidget(id) {
119
+ return `[[☃ ${id}]]`;
120
+ }
121
+
122
+ /**
123
+ * Regex for widget placeholders in a string.
124
+ *
125
+ * First capture group is the widget ID (ex. 'radio 1')
126
+ * Second capture group is the widget type (ex. "radio)
127
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
128
+ */
129
+ function getWidgetRegex() {
130
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
131
+ }
132
+
133
+ /**
134
+ * Extract all widget IDs, which includes the widget type and instance number.
135
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
136
+ *
137
+ * Content should contain Perseus widget placeholders,
138
+ * which look like: '[[☃ radio 1]]'.
139
+ *
140
+ * @param {string} content
141
+ * @returns {ReadonlyArray<string>} widgetIds
142
+ */
143
+ function getWidgetIdsFromContent(content) {
144
+ const widgets = [];
145
+ const localWidgetRegex = getWidgetRegex();
146
+ let match = localWidgetRegex.exec(content);
147
+ while (match !== null) {
148
+ widgets.push(match[1]);
149
+ match = localWidgetRegex.exec(content);
19
150
  }
20
- const formattedVersion = `${prefix}${libraryVersion}`;
21
- if (typeof globalThis !== "undefined") {
22
- globalThis.__perseus_debug__ = globalThis.__perseus_debug__ ?? {};
23
- const existingVersionEntry = globalThis.__perseus_debug__[libraryName];
24
- if (existingVersionEntry) {
25
- // If we already have an entry and it doesn't match the registered
26
- // version, we morph the entry into an array and log a warning.
27
- if (existingVersionEntry !== formattedVersion) {
28
- // Existing entry might be an array already (oops, at least 2
29
- // versions of the library already loaded!).
30
- const allVersions = Array.isArray(existingVersionEntry) ? existingVersionEntry : [existingVersionEntry];
31
- allVersions.push(formattedVersion);
32
- globalThis.__perseus_debug__[libraryName] = allVersions;
151
+ return widgets;
152
+ }
33
153
 
34
- // eslint-disable-next-line no-console
35
- console.warn(`Multiple versions of ${libraryName} loaded on this page: ${allVersions.sort().join(", ")}`);
154
+ /**
155
+ * Get a list of widget IDs from content,
156
+ * but only for specific widget types
157
+ *
158
+ * @param {string} type the type of widget (ie "radio")
159
+ * @param {string} content the string to parse
160
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
161
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
162
+ */
163
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
164
+ const rv = [];
165
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
166
+ widgetIdsInContent.forEach(widgetId => {
167
+ const widget = widgetMap[widgetId];
168
+ if (widget?.type === type) {
169
+ rv.push(widgetId);
170
+ }
171
+ });
172
+ return rv;
173
+ }
174
+
175
+ // TODO(benchristel): in the future, we may want to make deepClone work for
176
+ // Record<string, Cloneable> as well. Currently, it only does arrays.
177
+
178
+ function deepClone(obj) {
179
+ if (Array.isArray(obj)) {
180
+ return obj.map(deepClone);
181
+ }
182
+ return obj;
183
+ }
184
+
185
+ const MOVABLES = {
186
+ PLOT: "PLOT",
187
+ PARABOLA: "PARABOLA",
188
+ SINUSOID: "SINUSOID"
189
+ };
190
+
191
+ // TODO(charlie): These really need to go into a utility file as they're being
192
+ // used by both interactive-graph and now grapher.
193
+ function canonicalSineCoefficients(coeffs) {
194
+ // For a curve of the form f(x) = a * Sin(b * x - c) + d,
195
+ // this function ensures that a, b > 0, and c is its
196
+ // smallest possible positive value.
197
+ let amplitude = coeffs[0];
198
+ let angularFrequency = coeffs[1];
199
+ let phase = coeffs[2];
200
+ const verticalOffset = coeffs[3];
201
+
202
+ // Guarantee a > 0
203
+ if (amplitude < 0) {
204
+ amplitude *= -1;
205
+ angularFrequency *= -1;
206
+ phase *= -1;
207
+ }
208
+ const period = 2 * Math.PI;
209
+ // Guarantee b > 0
210
+ if (angularFrequency < 0) {
211
+ angularFrequency *= -1;
212
+ phase *= -1;
213
+ phase += period / 2;
214
+ }
215
+
216
+ // Guarantee c is smallest possible positive value
217
+ while (phase > 0) {
218
+ phase -= period;
219
+ }
220
+ while (phase < 0) {
221
+ phase += period;
222
+ }
223
+ return [amplitude, angularFrequency, phase, verticalOffset];
224
+ }
225
+ function canonicalTangentCoefficients(coeffs) {
226
+ // For a curve of the form f(x) = a * Tan(b * x - c) + d,
227
+ // this function ensures that a, b > 0, and c is its
228
+ // smallest possible positive value.
229
+ let amplitude = coeffs[0];
230
+ let angularFrequency = coeffs[1];
231
+ let phase = coeffs[2];
232
+ const verticalOffset = coeffs[3];
233
+
234
+ // Guarantee a > 0
235
+ if (amplitude < 0) {
236
+ amplitude *= -1;
237
+ angularFrequency *= -1;
238
+ phase *= -1;
239
+ }
240
+ const period = Math.PI;
241
+ // Guarantee b > 0
242
+ if (angularFrequency < 0) {
243
+ angularFrequency *= -1;
244
+ phase *= -1;
245
+ phase += period / 2;
246
+ }
247
+
248
+ // Guarantee c is smallest possible positive value
249
+ while (phase > 0) {
250
+ phase -= period;
251
+ }
252
+ while (phase < 0) {
253
+ phase += period;
254
+ }
255
+ return [amplitude, angularFrequency, phase, verticalOffset];
256
+ }
257
+ const PlotDefaults = {
258
+ areEqual: function (coeffs1, coeffs2) {
259
+ return approximateDeepEqual(coeffs1, coeffs2);
260
+ },
261
+ movable: MOVABLES.PLOT,
262
+ getPropsForCoeffs: function (coeffs) {
263
+ return {
264
+ // @ts-expect-error - TS2339 - Property 'getFunctionForCoeffs' does not exist on type '{ readonly areEqual: (coeffs1: any, coeffs2: any) => boolean; readonly Movable: any; readonly getPropsForCoeffs: (coeffs: any) => any; }'.
265
+ fn: ___default["default"].partial(this.getFunctionForCoeffs, coeffs)
266
+ };
267
+ }
268
+ };
269
+ const Linear = ___default["default"].extend({}, PlotDefaults, {
270
+ url: "https://ka-perseus-graphie.s3.amazonaws.com/67aaf581e6d9ef9038c10558a1f70ac21c11c9f8.png",
271
+ defaultCoords: [[0.25, 0.75], [0.75, 0.75]],
272
+ getCoefficients: function (coords) {
273
+ const p1 = coords[0];
274
+ const p2 = coords[1];
275
+ const denom = p2[0] - p1[0];
276
+ const num = p2[1] - p1[1];
277
+ if (denom === 0) {
278
+ return;
279
+ }
280
+ const m = num / denom;
281
+ const b = p2[1] - m * p2[0];
282
+ return [m, b];
283
+ },
284
+ getFunctionForCoeffs: function (coeffs, x) {
285
+ const m = coeffs[0];
286
+ const b = coeffs[1];
287
+ return m * x + b;
288
+ },
289
+ getEquationString: function (coords) {
290
+ const coeffs = this.getCoefficients(coords);
291
+ const m = coeffs[0];
292
+ const b = coeffs[1];
293
+ return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
294
+ }
295
+ });
296
+ const Quadratic = ___default["default"].extend({}, PlotDefaults, {
297
+ url: "https://ka-perseus-graphie.s3.amazonaws.com/e23d36e6fc29ee37174e92c9daba2a66677128ab.png",
298
+ defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
299
+ movable: MOVABLES.PARABOLA,
300
+ getCoefficients: function (coords) {
301
+ const p1 = coords[0];
302
+ const p2 = coords[1];
303
+
304
+ // Parabola with vertex (h, k) has form: y = a * (h - k)^2 + k
305
+ const h = p1[0];
306
+ const k = p1[1];
307
+
308
+ // Use these to calculate familiar a, b, c
309
+ const a = (p2[1] - k) / ((p2[0] - h) * (p2[0] - h));
310
+ const b = -2 * h * a;
311
+ const c = a * h * h + k;
312
+ return [a, b, c];
313
+ },
314
+ getFunctionForCoeffs: function (coeffs, x) {
315
+ const a = coeffs[0];
316
+ const b = coeffs[1];
317
+ const c = coeffs[2];
318
+ return (a * x + b) * x + c;
319
+ },
320
+ getPropsForCoeffs: function (coeffs) {
321
+ return {
322
+ a: coeffs[0],
323
+ b: coeffs[1],
324
+ c: coeffs[2]
325
+ };
326
+ },
327
+ getEquationString: function (coords) {
328
+ const coeffs = this.getCoefficients(coords);
329
+ const a = coeffs[0];
330
+ const b = coeffs[1];
331
+ const c = coeffs[2];
332
+ return "y = " + a.toFixed(3) + "x^2 + " + b.toFixed(3) + "x + " + c.toFixed(3);
333
+ }
334
+ });
335
+ const Sinusoid = ___default["default"].extend({}, PlotDefaults, {
336
+ url: "https://ka-perseus-graphie.s3.amazonaws.com/3d68e7718498475f53b206c2ab285626baf8857e.png",
337
+ defaultCoords: [[0.5, 0.5], [0.6, 0.6]],
338
+ movable: MOVABLES.SINUSOID,
339
+ getCoefficients: function (coords) {
340
+ const p1 = coords[0];
341
+ const p2 = coords[1];
342
+ const a = p2[1] - p1[1];
343
+ const b = Math.PI / (2 * (p2[0] - p1[0]));
344
+ const c = p1[0] * b;
345
+ const d = p1[1];
346
+ return [a, b, c, d];
347
+ },
348
+ getFunctionForCoeffs: function (coeffs, x) {
349
+ const a = coeffs[0];
350
+ const b = coeffs[1];
351
+ const c = coeffs[2];
352
+ const d = coeffs[3];
353
+ return a * Math.sin(b * x - c) + d;
354
+ },
355
+ getPropsForCoeffs: function (coeffs) {
356
+ return {
357
+ a: coeffs[0],
358
+ b: coeffs[1],
359
+ c: coeffs[2],
360
+ d: coeffs[3]
361
+ };
362
+ },
363
+ getEquationString: function (coords) {
364
+ const coeffs = this.getCoefficients(coords);
365
+ const a = coeffs[0];
366
+ const b = coeffs[1];
367
+ const c = coeffs[2];
368
+ const d = coeffs[3];
369
+ return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
370
+ },
371
+ areEqual: function (coeffs1, coeffs2) {
372
+ return approximateDeepEqual(canonicalSineCoefficients(coeffs1), canonicalSineCoefficients(coeffs2));
373
+ }
374
+ });
375
+ const Tangent = ___default["default"].extend({}, PlotDefaults, {
376
+ url: "https://ka-perseus-graphie.s3.amazonaws.com/7db80d23c35214f98659fe1cf0765811c1bbfbba.png",
377
+ defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
378
+ getCoefficients: function (coords) {
379
+ const p1 = coords[0];
380
+ const p2 = coords[1];
381
+ const a = p2[1] - p1[1];
382
+ const b = Math.PI / (4 * (p2[0] - p1[0]));
383
+ const c = p1[0] * b;
384
+ const d = p1[1];
385
+ return [a, b, c, d];
386
+ },
387
+ getFunctionForCoeffs: function (coeffs, x) {
388
+ const a = coeffs[0];
389
+ const b = coeffs[1];
390
+ const c = coeffs[2];
391
+ const d = coeffs[3];
392
+ return a * Math.tan(b * x - c) + d;
393
+ },
394
+ getEquationString: function (coords) {
395
+ const coeffs = this.getCoefficients(coords);
396
+ const a = coeffs[0];
397
+ const b = coeffs[1];
398
+ const c = coeffs[2];
399
+ const d = coeffs[3];
400
+ return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
401
+ },
402
+ areEqual: function (coeffs1, coeffs2) {
403
+ return approximateDeepEqual(canonicalTangentCoefficients(coeffs1), canonicalTangentCoefficients(coeffs2));
404
+ }
405
+ });
406
+ const Exponential = ___default["default"].extend({}, PlotDefaults, {
407
+ url: "https://ka-perseus-graphie.s3.amazonaws.com/9cbfad55525e3ce755a31a631b074670a5dad611.png",
408
+ defaultCoords: [[0.5, 0.55], [0.75, 0.75]],
409
+ defaultAsymptote: [[0, 0.5], [1.0, 0.5]],
410
+ /**
411
+ * Add extra constraints for movement of the points or asymptote (below):
412
+ * newCoord: [x, y]
413
+ * The end position of the point or asymptote endpoint
414
+ * oldCoord: [x, y]
415
+ * The old position of the point or asymptote endpoint
416
+ * coords:
417
+ * An array of coordinates representing the proposed end configuration
418
+ * of the plot coordinates.
419
+ * asymptote:
420
+ * An array of coordinates representing the proposed end configuration
421
+ * of the asymptote.
422
+ *
423
+ * Return: either a coordinate (to be used as the resulting coordinate of
424
+ * the move) or a boolean, where `true` uses newCoord as the resulting
425
+ * coordinate, and `false` uses oldCoord as the resulting coordinate.
426
+ */
427
+ extraCoordConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
428
+ const y = asymptote[0][1];
429
+ return ___default["default"].all(coords, coord => coord[1] !== y);
430
+ },
431
+ extraAsymptoteConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
432
+ const y = newCoord[1];
433
+ const isValid = ___default["default"].all(coords, coord => coord[1] > y) || ___default["default"].all(coords, coord => coord[1] < y);
434
+ if (isValid) {
435
+ return [oldCoord[0], y];
436
+ }
437
+ // Snap the asymptote as close as possible, i.e., if the user moves
438
+ // the mouse really quickly into an invalid region
439
+ const oldY = oldCoord[1];
440
+ const wasBelow = ___default["default"].all(coords, coord => coord[1] > oldY);
441
+ if (wasBelow) {
442
+ const bottomMost = ___default["default"].min(___default["default"].map(coords, coord => coord[1]));
443
+ return [oldCoord[0], bottomMost - graph.snapStep[1]];
444
+ }
445
+ const topMost = ___default["default"].max(___default["default"].map(coords, coord => coord[1]));
446
+ return [oldCoord[0], topMost + graph.snapStep[1]];
447
+ },
448
+ allowReflectOverAsymptote: true,
449
+ getCoefficients: function (coords, asymptote) {
450
+ const p1 = coords[0];
451
+ const p2 = coords[1];
452
+ const c = asymptote[0][1];
453
+ const b = Math.log((p1[1] - c) / (p2[1] - c)) / (p1[0] - p2[0]);
454
+ const a = (p1[1] - c) / Math.exp(b * p1[0]);
455
+ return [a, b, c];
456
+ },
457
+ getFunctionForCoeffs: function (coeffs, x) {
458
+ const a = coeffs[0];
459
+ const b = coeffs[1];
460
+ const c = coeffs[2];
461
+ return a * Math.exp(b * x) + c;
462
+ },
463
+ getEquationString: function (coords, asymptote) {
464
+ if (!asymptote) {
465
+ return null;
466
+ }
467
+ const coeffs = this.getCoefficients(coords, asymptote);
468
+ const a = coeffs[0];
469
+ const b = coeffs[1];
470
+ const c = coeffs[2];
471
+ return "y = " + a.toFixed(3) + "e^(" + b.toFixed(3) + "x) + " + c.toFixed(3);
472
+ }
473
+ });
474
+ const Logarithm = ___default["default"].extend({}, PlotDefaults, {
475
+ url: "https://ka-perseus-graphie.s3.amazonaws.com/f6491e99d34af34d924bfe0231728ad912068dc3.png",
476
+ defaultCoords: [[0.55, 0.5], [0.75, 0.75]],
477
+ defaultAsymptote: [[0.5, 0], [0.5, 1.0]],
478
+ extraCoordConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
479
+ const x = asymptote[0][0];
480
+ return ___default["default"].all(coords, coord => coord[0] !== x) && coords[0][1] !== coords[1][1];
481
+ },
482
+ extraAsymptoteConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
483
+ const x = newCoord[0];
484
+ const isValid = ___default["default"].all(coords, coord => coord[0] > x) || ___default["default"].all(coords, coord => coord[0] < x);
485
+ if (isValid) {
486
+ return [x, oldCoord[1]];
487
+ }
488
+ // Snap the asymptote as close as possible, i.e., if the user moves
489
+ // the mouse really quickly into an invalid region
490
+ const oldX = oldCoord[0];
491
+ const wasLeft = ___default["default"].all(coords, coord => coord[0] > oldX);
492
+ if (wasLeft) {
493
+ const leftMost = ___default["default"].min(___default["default"].map(coords, coord => coord[0]));
494
+ return [leftMost - graph.snapStep[0], oldCoord[1]];
495
+ }
496
+ const rightMost = ___default["default"].max(___default["default"].map(coords, coord => coord[0]));
497
+ return [rightMost + graph.snapStep[0], oldCoord[1]];
498
+ },
499
+ allowReflectOverAsymptote: true,
500
+ getCoefficients: function (coords, asymptote) {
501
+ // It's easiest to calculate the logarithm's coefficients by thinking
502
+ // about it as the inverse of the exponential, so we flip x and y and
503
+ // perform some algebra on the coefficients. This also unifies the
504
+ // logic between the two 'models'.
505
+ const flip = coord => [coord[1], coord[0]];
506
+ const inverseCoeffs = Exponential.getCoefficients(___default["default"].map(coords, flip), ___default["default"].map(asymptote, flip));
507
+ if (inverseCoeffs) {
508
+ const c = -inverseCoeffs[2] / inverseCoeffs[0];
509
+ const b = 1 / inverseCoeffs[0];
510
+ const a = 1 / inverseCoeffs[1];
511
+ return [a, b, c];
512
+ }
513
+ },
514
+ getFunctionForCoeffs: function (coeffs, x, asymptote) {
515
+ const a = coeffs[0];
516
+ const b = coeffs[1];
517
+ const c = coeffs[2];
518
+ return a * Math.log(b * x + c);
519
+ },
520
+ getEquationString: function (coords, asymptote) {
521
+ if (!asymptote) {
522
+ return null;
523
+ }
524
+ const coeffs = this.getCoefficients(coords, asymptote);
525
+ const a = coeffs[0];
526
+ const b = coeffs[1];
527
+ const c = coeffs[2];
528
+ return "y = ln(" + a.toFixed(3) + "x + " + b.toFixed(3) + ") + " + c.toFixed(3);
529
+ }
530
+ });
531
+ const AbsoluteValue = ___default["default"].extend({}, PlotDefaults, {
532
+ url: "https://ka-perseus-graphie.s3.amazonaws.com/8256a630175a0cb1d11de223d6de0266daf98721.png",
533
+ defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
534
+ getCoefficients: function (coords) {
535
+ const p1 = coords[0];
536
+ const p2 = coords[1];
537
+ const denom = p2[0] - p1[0];
538
+ const num = p2[1] - p1[1];
539
+ if (denom === 0) {
540
+ return;
541
+ }
542
+ let m = Math.abs(num / denom);
543
+ if (p2[1] < p1[1]) {
544
+ m *= -1;
545
+ }
546
+ const horizontalOffset = p1[0];
547
+ const verticalOffset = p1[1];
548
+ return [m, horizontalOffset, verticalOffset];
549
+ },
550
+ getFunctionForCoeffs: function (coeffs, x) {
551
+ const m = coeffs[0];
552
+ const horizontalOffset = coeffs[1];
553
+ const verticalOffset = coeffs[2];
554
+ return m * Math.abs(x - horizontalOffset) + verticalOffset;
555
+ },
556
+ getEquationString: function (coords) {
557
+ const coeffs = this.getCoefficients(coords);
558
+ const m = coeffs[0];
559
+ const horizontalOffset = coeffs[1];
560
+ const verticalOffset = coeffs[2];
561
+ return "y = " + m.toFixed(3) + "| x - " + horizontalOffset.toFixed(3) + "| + " + verticalOffset.toFixed(3);
562
+ }
563
+ });
564
+
565
+ /* Utility functions for dealing with graphing interfaces. */
566
+ const functionTypeMapping = {
567
+ linear: Linear,
568
+ quadratic: Quadratic,
569
+ sinusoid: Sinusoid,
570
+ tangent: Tangent,
571
+ exponential: Exponential,
572
+ logarithm: Logarithm,
573
+ absolute_value: AbsoluteValue
574
+ };
575
+ const allTypes = ___default["default"].keys(functionTypeMapping);
576
+ function functionForType(type) {
577
+ // @ts-expect-error: TypeScript doesn't know how to use deal with generics
578
+ // and conditional types in this way.
579
+ return functionTypeMapping[type];
580
+ }
581
+
582
+ var grapherUtil = /*#__PURE__*/Object.freeze({
583
+ __proto__: null,
584
+ MOVABLES: MOVABLES,
585
+ allTypes: allTypes,
586
+ functionForType: functionForType
587
+ });
588
+
589
+ function isRealJSONParse(jsonParse) {
590
+ const randomPhrase = buildRandomPhrase();
591
+ const randomHintPhrase = buildRandomPhrase();
592
+ const randomString = buildRandomString();
593
+ const testingObject = JSON.stringify({
594
+ answerArea: {
595
+ calculator: false,
596
+ chi2Table: false,
597
+ financialCalculatorMonthlyPayment: false,
598
+ financialCalculatorTimeToPayOff: false,
599
+ financialCalculatorTotalAmount: false,
600
+ periodicTable: false,
601
+ periodicTableWithKey: false,
602
+ tTable: false,
603
+ zTable: false
604
+ },
605
+ hints: [randomHintPhrase, `=${Math.floor(Math.random() * 50) + 1}`],
606
+ itemDataVersion: {
607
+ major: 0,
608
+ minor: 1
609
+ },
610
+ question: {
611
+ content: `${randomPhrase}`,
612
+ images: {},
613
+ widgets: {
614
+ expression1: {
615
+ alignment: "default",
616
+ graded: false,
617
+ options: {
618
+ answerForms: [{
619
+ considered: "wrong",
620
+ form: false,
621
+ key: 0,
622
+ simplify: false,
623
+ value: `${randomString}`
624
+ }],
625
+ ariaLabel: "Answer",
626
+ buttonSets: ["basic"],
627
+ functions: ["f", "g", "h"],
628
+ static: true,
629
+ times: false,
630
+ visibleLabel: "Answer"
631
+ },
632
+ static: true,
633
+ type: "expression",
634
+ version: {
635
+ major: 1,
636
+ minor: 0
637
+ }
638
+ }
36
639
  }
640
+ }
641
+ });
642
+ const testJSON = buildTestData(testingObject.replace(/"/g, '\\"'));
643
+ const parsedTestJSON = jsonParse(testJSON);
644
+ const parsedTestItemData = parsedTestJSON.data.assessmentItem.item.itemData;
645
+ return approximateDeepEqual(parsedTestItemData, testingObject);
646
+ }
647
+ function buildRandomString() {
648
+ let capitalize = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
649
+ let randomString = "";
650
+ const randomLength = Math.floor(Math.random() * 8) + 3;
651
+ for (let i = 0; i < randomLength; i++) {
652
+ const randomLetter = String.fromCharCode(97 + Math.floor(Math.random() * 26));
653
+ randomString += capitalize && i === 0 ? randomLetter.toUpperCase() : randomLetter;
654
+ }
655
+ return randomString;
656
+ }
657
+ function buildRandomPhrase() {
658
+ const phrases = [];
659
+ const randomLength = Math.floor(Math.random() * 10) + 5;
660
+ for (let i = 0; i < randomLength; i++) {
661
+ phrases.push(buildRandomString(i === 0));
662
+ }
663
+ const modifierStart = ["**", "$"];
664
+ const modifierEnd = ["**", "$"];
665
+ const modifierIndex = Math.floor(Math.random() * modifierStart.length);
666
+ return `${modifierStart[modifierIndex]}${phrases.join(" ")}${modifierEnd[modifierIndex]}`;
667
+ }
668
+ function buildTestData(testObject) {
669
+ return `{"data":{"assessmentItem":{"__typename":"AssessmentItemOrError","error":null,"item":{"__typename":"AssessmentItem","id":"x890b3c70f3e8f4a6","itemData":"${testObject}","problemType":"Type 1","sha":"c7284a3ad65214b4e62bccce236d92f7f5d35941"}}}}`;
670
+ }
671
+
672
+ process.env.NODE_ENV === 'production';
673
+
674
+ function success(value) {
675
+ return {
676
+ type: "success",
677
+ value
678
+ };
679
+ }
680
+ function failure(detail) {
681
+ return {
682
+ type: "failure",
683
+ detail
684
+ };
685
+ }
686
+ function isFailure(result) {
687
+ return result.type === "failure";
688
+ }
689
+ function isSuccess(result) {
690
+ return result.type === "success";
691
+ }
692
+
693
+ // Result's `all` function is similar to Promise.all: given an array of
694
+ // results, it returns success if all succeeded, and failure if any failed.
695
+ function all(results) {
696
+ let combineFailureDetails = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : a => a;
697
+ const values = [];
698
+ const failureDetails = [];
699
+ for (const result of results) {
700
+ if (result.type === "success") {
701
+ values.push(result.value);
37
702
  } else {
38
- globalThis.__perseus_debug__[libraryName] = formattedVersion;
703
+ failureDetails.push(result.detail);
39
704
  }
40
- } else {
41
- // eslint-disable-next-line no-console
42
- console.warn(`globalThis not found found (${formattedVersion})`);
43
705
  }
706
+ if (failureDetails.length > 0) {
707
+ return failure(failureDetails.reduce(combineFailureDetails));
708
+ }
709
+ return success(values);
710
+ }
711
+
712
+ class ErrorTrackingParseContext {
713
+ constructor(path) {
714
+ this.path = path;
715
+ }
716
+ failure(expected, badValue) {
717
+ return failure([{
718
+ expected: wrapInArray(expected),
719
+ badValue,
720
+ path: this.path
721
+ }]);
722
+ }
723
+ forSubtree(key) {
724
+ return new ErrorTrackingParseContext([...this.path, key]);
725
+ }
726
+ success(value) {
727
+ return success(value);
728
+ }
729
+ }
730
+ function wrapInArray(a) {
731
+ return Array.isArray(a) ? a : [a];
732
+ }
733
+
734
+ function formatPath(path) {
735
+ return "(root)" + path.map(formatPathSegment).join("");
736
+ }
737
+ function formatPathSegment(segment) {
738
+ if (typeof segment === "string") {
739
+ return validIdentifier.test(segment) ? "." + segment : `[${JSON.stringify(segment)}]`;
740
+ }
741
+ return `[${segment.toString()}]`;
742
+ }
743
+ const validIdentifier = /^[A-Za-z$_][A-Za-z$_0-9]*$/;
744
+
745
+ function message(failure) {
746
+ const expected = conjoin(failure.expected);
747
+ const path = formatPath(failure.path);
748
+ const badValue = JSON.stringify(failure.badValue);
749
+ return `At ${path} -- expected ${expected}, but got ${badValue}`;
750
+ }
751
+ function conjoin(items) {
752
+ switch (items.length) {
753
+ // TODO(benchristel): handle 0 if this is reused elsewhere.
754
+ case 1:
755
+ return items[0];
756
+ case 2:
757
+ return items.join(" or ");
758
+ default:
759
+ {
760
+ const allButLast = items.slice(0, items.length - 1);
761
+ const last = items[items.length - 1];
762
+ return allButLast.join(", ") + ", or " + last;
763
+ }
764
+ }
765
+ }
766
+
767
+ function parse(value, parser) {
768
+ const result = parser(value, new ErrorTrackingParseContext([]));
769
+ if (isFailure(result)) {
770
+ return failure(result.detail.map(message).join("; "));
771
+ }
772
+ return result;
773
+ }
774
+
775
+ const any = (rawValue, ctx) => ctx.success(rawValue);
776
+
777
+ function array(elementParser) {
778
+ return (rawValue, ctx) => {
779
+ if (!Array.isArray(rawValue)) {
780
+ return ctx.failure("array", rawValue);
781
+ }
782
+ const elementResults = rawValue.map((elem, i) => elementParser(elem, ctx.forSubtree(i)));
783
+ return all(elementResults, concat);
784
+ };
785
+ }
786
+ function concat(a, b) {
787
+ return [...a, ...b];
788
+ }
789
+
790
+ function boolean(rawValue, ctx) {
791
+ if (typeof rawValue === "boolean") {
792
+ return ctx.success(rawValue);
793
+ }
794
+ return ctx.failure("boolean", rawValue);
795
+ }
796
+
797
+ function constant(acceptedValue) {
798
+ return (rawValue, ctx) => {
799
+ if (rawValue !== acceptedValue) {
800
+ return ctx.failure(String(JSON.stringify(acceptedValue)), rawValue);
801
+ }
802
+ return ctx.success(acceptedValue);
803
+ };
804
+ }
805
+
806
+ function enumeration() {
807
+ for (var _len = arguments.length, acceptedValues = new Array(_len), _key = 0; _key < _len; _key++) {
808
+ acceptedValues[_key] = arguments[_key];
809
+ }
810
+ return (rawValue, ctx) => {
811
+ if (typeof rawValue === "string") {
812
+ const index = acceptedValues.indexOf(rawValue);
813
+ if (index > -1) {
814
+ return ctx.success(acceptedValues[index]);
815
+ }
816
+ }
817
+ const expected = acceptedValues.map(v => JSON.stringify(v));
818
+ return ctx.failure(expected, rawValue);
819
+ };
820
+ }
821
+
822
+ function isObject(x) {
823
+ return x != null && Object.getPrototypeOf(x) === Object.prototype;
824
+ }
825
+
826
+ function nullable(parseValue) {
827
+ return (rawValue, ctx) => {
828
+ if (rawValue === null) {
829
+ return ctx.success(rawValue);
830
+ }
831
+ return parseValue(rawValue, ctx);
832
+ };
833
+ }
834
+
835
+ const number = (rawValue, ctx) => {
836
+ if (typeof rawValue === "number") {
837
+ return ctx.success(rawValue);
838
+ }
839
+ return ctx.failure("number", rawValue);
44
840
  };
45
841
 
46
- // This file is processed by a Rollup plugin (replace) to inject the production
47
- const libName = "@khanacademy/perseus-core";
48
- const libVersion = "1.4.0";
49
- addLibraryVersionToPerseusDebug(libName, libVersion);
842
+ function object(schema) {
843
+ return (rawValue, ctx) => {
844
+ if (!isObject(rawValue)) {
845
+ return ctx.failure("object", rawValue);
846
+ }
847
+ const ret = {
848
+ ...rawValue
849
+ };
850
+ const mismatches = [];
851
+ for (const [prop, propParser] of Object.entries(schema)) {
852
+ const result = propParser(rawValue[prop], ctx.forSubtree(prop));
853
+ if (isSuccess(result)) {
854
+ if (result.value !== undefined || prop in rawValue) {
855
+ ret[prop] = result.value;
856
+ }
857
+ } else {
858
+ mismatches.push(...result.detail);
859
+ }
860
+ }
861
+ if (mismatches.length > 0) {
862
+ return failure(mismatches);
863
+ }
864
+ return ctx.success(ret);
865
+ };
866
+ }
867
+
868
+ function optional(parseValue) {
869
+ return (rawValue, ctx) => {
870
+ if (rawValue === undefined) {
871
+ return ctx.success(rawValue);
872
+ }
873
+ return parseValue(rawValue, ctx);
874
+ };
875
+ }
876
+
877
+ function pair(parseA, parseB) {
878
+ return (rawValue, ctx) => {
879
+ if (!Array.isArray(rawValue)) {
880
+ return ctx.failure("array", rawValue);
881
+ }
882
+ if (rawValue.length !== 2) {
883
+ return ctx.failure("array of length 2", rawValue);
884
+ }
885
+ const [rawA, rawB] = rawValue;
886
+ const resultA = parseA(rawA, ctx.forSubtree(0));
887
+ if (isFailure(resultA)) {
888
+ return resultA;
889
+ }
890
+ const resultB = parseB(rawB, ctx.forSubtree(1));
891
+ if (isFailure(resultB)) {
892
+ return resultB;
893
+ }
894
+ return ctx.success([resultA.value, resultB.value]);
895
+ };
896
+ }
897
+
898
+ function pipeParsers(p) {
899
+ return new ParserPipeline(p);
900
+ }
901
+ class ParserPipeline {
902
+ constructor(parser) {
903
+ this.parser = parser;
904
+ }
905
+ then(nextParser) {
906
+ return new ParserPipeline(composeParsers(this.parser, nextParser));
907
+ }
908
+ }
909
+ function composeParsers(parserA, parserB) {
910
+ return (rawValue, ctx) => {
911
+ const partialResult = parserA(rawValue, ctx);
912
+ if (isFailure(partialResult)) {
913
+ return partialResult;
914
+ }
915
+ return parserB(partialResult.value, ctx);
916
+ };
917
+ }
918
+
919
+ function record(parseKey, parseValue) {
920
+ return (rawValue, ctx) => {
921
+ if (!isObject(rawValue)) {
922
+ return ctx.failure("object", rawValue);
923
+ }
924
+ const result = {};
925
+ const mismatches = [];
926
+ for (const [key, value] of Object.entries(rawValue)) {
927
+ const entryCtx = ctx.forSubtree(key);
928
+ const keyResult = parseKey(key, entryCtx);
929
+ if (isFailure(keyResult)) {
930
+ mismatches.push(...keyResult.detail);
931
+ }
932
+ const valueResult = parseValue(value, entryCtx);
933
+ if (isFailure(valueResult)) {
934
+ mismatches.push(...valueResult.detail);
935
+ }
936
+ if (isSuccess(keyResult) && isSuccess(valueResult)) {
937
+ result[keyResult.value] = valueResult.value;
938
+ }
939
+ }
940
+ if (mismatches.length > 0) {
941
+ return failure(mismatches);
942
+ }
943
+ return ctx.success(result);
944
+ };
945
+ }
946
+
947
+ const string = (rawValue, ctx) => {
948
+ if (typeof rawValue === "string") {
949
+ return ctx.success(rawValue);
950
+ }
951
+ return ctx.failure("string", rawValue);
952
+ };
953
+
954
+ function trio(parseA, parseB, parseC) {
955
+ return (rawValue, ctx) => {
956
+ if (!Array.isArray(rawValue)) {
957
+ return ctx.failure("array", rawValue);
958
+ }
959
+ if (rawValue.length !== 3) {
960
+ return ctx.failure("array of length 3", rawValue);
961
+ }
962
+ const resultA = parseA(rawValue[0], ctx.forSubtree(0));
963
+ if (isFailure(resultA)) {
964
+ return resultA;
965
+ }
966
+ const resultB = parseB(rawValue[1], ctx.forSubtree(1));
967
+ if (isFailure(resultB)) {
968
+ return resultB;
969
+ }
970
+ const resultC = parseC(rawValue[2], ctx.forSubtree(2));
971
+ if (isFailure(resultC)) {
972
+ return resultC;
973
+ }
974
+ return ctx.success([resultA.value, resultB.value, resultC.value]);
975
+ };
976
+ }
977
+
978
+ function union(parseBranch) {
979
+ return new UnionBuilder(parseBranch);
980
+ }
981
+ class UnionBuilder {
982
+ constructor(parser) {
983
+ this.parser = parser;
984
+ }
985
+ or(newBranch) {
986
+ return new UnionBuilder(either(this.parser, newBranch));
987
+ }
988
+ }
989
+ function either(parseA, parseB) {
990
+ return (rawValue, ctx) => {
991
+ const resultA = parseA(rawValue, ctx);
992
+ if (isSuccess(resultA)) {
993
+ return resultA;
994
+ }
995
+ return parseB(rawValue, ctx);
996
+ };
997
+ }
998
+
999
+ function defaulted(parser, fallback) {
1000
+ return (rawValue, ctx) => {
1001
+ if (rawValue == null) {
1002
+ return success(fallback(rawValue));
1003
+ }
1004
+ return parser(rawValue, ctx);
1005
+ };
1006
+ }
1007
+
1008
+ const parseImages = defaulted(record(string, object({
1009
+ width: number,
1010
+ height: number
1011
+ })), () => ({}));
1012
+
1013
+ function parseWidget(parseType, parseOptions) {
1014
+ return object({
1015
+ type: parseType,
1016
+ static: optional(boolean),
1017
+ graded: optional(boolean),
1018
+ alignment: optional(string),
1019
+ options: parseOptions,
1020
+ key: optional(nullable(number)),
1021
+ version: optional(object({
1022
+ major: number,
1023
+ minor: number
1024
+ }))
1025
+ });
1026
+ }
1027
+ function parseWidgetWithVersion(parseVersion, parseType, parseOptions) {
1028
+ return object({
1029
+ type: parseType,
1030
+ static: optional(boolean),
1031
+ graded: optional(boolean),
1032
+ alignment: optional(string),
1033
+ options: parseOptions,
1034
+ key: optional(number),
1035
+ version: parseVersion
1036
+ });
1037
+ }
1038
+
1039
+ const parseCategorizerWidget = parseWidget(constant("categorizer"), object({
1040
+ items: array(string),
1041
+ categories: array(string),
1042
+ randomizeItems: defaulted(boolean, () => false),
1043
+ static: defaulted(boolean, () => false),
1044
+ values: array(defaulted(number, () => 0)),
1045
+ highlightLint: optional(boolean),
1046
+ linterContext: optional(object({
1047
+ contentType: string,
1048
+ paths: array(string),
1049
+ stack: array(string)
1050
+ }))
1051
+ }));
1052
+
1053
+ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
1054
+ programID: string,
1055
+ programType: any,
1056
+ settings: array(object({
1057
+ name: string,
1058
+ value: string
1059
+ })),
1060
+ showEditor: boolean,
1061
+ showButtons: boolean,
1062
+ height: number,
1063
+ static: defaulted(boolean, () => false)
1064
+ }));
1065
+
1066
+ const parseDefinitionWidget = parseWidget(constant("definition"), object({
1067
+ togglePrompt: string,
1068
+ definition: string,
1069
+ static: defaulted(boolean, () => false)
1070
+ }));
1071
+
1072
+ const parseDropdownWidget = parseWidget(constant("dropdown"), object({
1073
+ placeholder: defaulted(string, () => ""),
1074
+ ariaLabel: optional(string),
1075
+ visibleLabel: optional(string),
1076
+ static: defaulted(boolean, () => false),
1077
+ choices: array(object({
1078
+ content: string,
1079
+ correct: boolean
1080
+ }))
1081
+ }));
1082
+
1083
+ const parseExplanationWidget = parseWidget(constant("explanation"), object({
1084
+ showPrompt: string,
1085
+ hidePrompt: string,
1086
+ explanation: string,
1087
+ // We wrap parseWidgetsMap in a function here to make sure it is not
1088
+ // referenced before it is defined. There is an import cycle between
1089
+ // this file and widgets-map.ts that could cause it to be undefined.
1090
+ widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
1091
+ static: defaulted(boolean, () => false)
1092
+ }));
1093
+
1094
+ // Given a function, creates a PartialParser that converts one type to another
1095
+ // using that function. The returned parser never fails.
1096
+ function convert(f) {
1097
+ return (rawValue, ctx) => ctx.success(f(rawValue));
1098
+ }
1099
+
1100
+ const parseLegacyButtonSet = enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations", "scientific");
1101
+ const parseLegacyButtonSets = defaulted(array(parseLegacyButtonSet),
1102
+ // NOTE(benchristel): I copied the default buttonSets from
1103
+ // expression.tsx. See the parse-perseus-json/README.md for
1104
+ // an explanation of why we want to duplicate the default here.
1105
+ () => ["basic", "trig", "prealgebra", "logarithms"]);
1106
+
1107
+ /**
1108
+ * Creates a parser for a widget options type with multiple major versions. Old
1109
+ * versions are migrated to the latest version. The parse fails if the input
1110
+ * data does not match any of the versions.
1111
+ *
1112
+ * @example
1113
+ * const parseOptions = versionedWidgetOptions(3, parseOptionsV3)
1114
+ * .withMigrationFrom(2, parseOptionsV2, migrateV2ToV3)
1115
+ * .withMigrationFrom(1, parseOptionsV1, migrateV1ToV2)
1116
+ * .withMigrationFrom(0, parseOptionsV0, migrateV0ToV1)
1117
+ * .parser;
1118
+ *
1119
+ * @param latestMajorVersion the latest major version of the widget options.
1120
+ * @param parseLatest a {@link Parser} for the latest version of the widget
1121
+ * options.
1122
+ * @returns a builder object, to which migrations from earlier versions can be
1123
+ * added. Migrations must be added in "reverse chronological" order as in the
1124
+ * example above.
1125
+ */
1126
+ function versionedWidgetOptions(latestMajorVersion, parseLatest) {
1127
+ return new VersionedWidgetOptionsParserBuilder(latestMajorVersion, parseLatest, latest => latest, (raw, ctx) => ctx.failure("widget options with a known version number", raw));
1128
+ }
1129
+ class VersionedWidgetOptionsParserBuilder {
1130
+ parser;
1131
+ constructor(majorVersion, parseThisVersion, migrateToLatest, parseOtherVersions) {
1132
+ this.migrateToLatest = migrateToLatest;
1133
+ this.parseOtherVersions = parseOtherVersions;
1134
+ const parseThisVersionAndMigrateToLatest = pipeParsers(parseThisVersion).then(convert(this.migrateToLatest)).parser;
1135
+ this.parser = (raw, ctx) => {
1136
+ if (!isObject(raw)) {
1137
+ return ctx.failure("object", raw);
1138
+ }
1139
+ const versionParseResult = parseVersionedObject(raw, ctx);
1140
+ if (isFailure(versionParseResult)) {
1141
+ return versionParseResult;
1142
+ }
1143
+ if (versionParseResult.value.version.major !== majorVersion) {
1144
+ return this.parseOtherVersions(raw, ctx);
1145
+ }
1146
+ return parseThisVersionAndMigrateToLatest(raw, ctx);
1147
+ };
1148
+ }
1149
+
1150
+ /**
1151
+ * Add a migration from an old version of the widget options.
1152
+ */
1153
+ withMigrationFrom(majorVersion, parseOldVersion, migrateToNextVersion) {
1154
+ const parseOtherVersions = this.parser;
1155
+ const migrateToLatest = old => this.migrateToLatest(migrateToNextVersion(old));
1156
+ return new VersionedWidgetOptionsParserBuilder(majorVersion, parseOldVersion, migrateToLatest, parseOtherVersions);
1157
+ }
1158
+ }
1159
+ const parseVersionedObject = object({
1160
+ version: defaulted(object({
1161
+ major: number,
1162
+ minor: number
1163
+ }), () => ({
1164
+ major: 0,
1165
+ minor: 0
1166
+ }))
1167
+ });
1168
+
1169
+ const stringOrNumberOrNullOrUndefined = union(string).or(number).or(constant(null)).or(constant(undefined)).parser;
1170
+ const parsePossiblyInvalidAnswerForm = object({
1171
+ // `value` is the possibly invalid part of this. It should always be a
1172
+ // string, but some answer forms don't have it. The Expression widget
1173
+ // ignores invalid values, so we can safely filter them out during parsing.
1174
+ value: optional(string),
1175
+ form: defaulted(boolean, () => false),
1176
+ simplify: defaulted(boolean, () => false),
1177
+ considered: enumeration("correct", "wrong", "ungraded"),
1178
+ key: pipeParsers(stringOrNumberOrNullOrUndefined).then(convert(String)).parser
1179
+ });
1180
+ function removeInvalidAnswerForms(possiblyInvalid) {
1181
+ const valid = [];
1182
+ for (const answerForm of possiblyInvalid) {
1183
+ const {
1184
+ value
1185
+ } = answerForm;
1186
+ if (value != null) {
1187
+ // Copying the object seems to be needed to make TypeScript happy
1188
+ valid.push({
1189
+ ...answerForm,
1190
+ value
1191
+ });
1192
+ }
1193
+ }
1194
+ return valid;
1195
+ }
1196
+ const version1 = object({
1197
+ major: constant(1),
1198
+ minor: number
1199
+ });
1200
+ const parseExpressionWidgetV1 = parseWidgetWithVersion(version1, constant("expression"), object({
1201
+ answerForms: pipeParsers(array(parsePossiblyInvalidAnswerForm)).then(convert(removeInvalidAnswerForms)).parser,
1202
+ functions: array(string),
1203
+ times: boolean,
1204
+ visibleLabel: optional(string),
1205
+ ariaLabel: optional(string),
1206
+ buttonSets: parseLegacyButtonSets,
1207
+ buttonsVisible: optional(enumeration("always", "never", "focused"))
1208
+ }));
1209
+ const version0 = optional(object({
1210
+ major: constant(0),
1211
+ minor: number
1212
+ }));
1213
+ const parseExpressionWidgetV0 = parseWidgetWithVersion(version0, constant("expression"), object({
1214
+ functions: array(string),
1215
+ times: boolean,
1216
+ visibleLabel: optional(string),
1217
+ ariaLabel: optional(string),
1218
+ form: boolean,
1219
+ simplify: boolean,
1220
+ value: string,
1221
+ buttonSets: parseLegacyButtonSets,
1222
+ buttonsVisible: optional(enumeration("always", "never", "focused"))
1223
+ }));
1224
+ function migrateV0ToV1(widget) {
1225
+ const {
1226
+ options
1227
+ } = widget;
1228
+ return {
1229
+ ...widget,
1230
+ version: {
1231
+ major: 1,
1232
+ minor: 0
1233
+ },
1234
+ options: {
1235
+ times: options.times,
1236
+ buttonSets: options.buttonSets,
1237
+ functions: options.functions,
1238
+ buttonsVisible: options.buttonsVisible,
1239
+ visibleLabel: options.visibleLabel,
1240
+ ariaLabel: options.ariaLabel,
1241
+ answerForms: [{
1242
+ considered: "correct",
1243
+ form: options.form,
1244
+ simplify: options.simplify,
1245
+ value: options.value
1246
+ }]
1247
+ }
1248
+ };
1249
+ }
1250
+ const parseExpressionWidget = versionedWidgetOptions(1, parseExpressionWidgetV1).withMigrationFrom(0, parseExpressionWidgetV0, migrateV0ToV1).parser;
1251
+
1252
+ const falseToNull = pipeParsers(constant(false)).then(convert(() => null)).parser;
1253
+ const parseGradedGroupWidgetOptions = object({
1254
+ title: defaulted(string, () => ""),
1255
+ hasHint: optional(nullable(boolean)),
1256
+ // This module has an import cycle with parsePerseusRenderer.
1257
+ // The anonymous function below ensures that we don't try to access
1258
+ // parsePerseusRenderer before it's defined.
1259
+ hint: union(falseToNull).or(constant(null)).or(constant(undefined)).or((rawVal, ctx) => parsePerseusRenderer(rawVal, ctx)).parser,
1260
+ content: string,
1261
+ // This module has an import cycle with parseWidgetsMap.
1262
+ // The anonymous function below ensures that we don't try to access
1263
+ // parseWidgetsMap before it's defined.
1264
+ widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
1265
+ widgetEnabled: optional(nullable(boolean)),
1266
+ immutableWidgets: optional(nullable(boolean)),
1267
+ images: record(string, object({
1268
+ width: number,
1269
+ height: number
1270
+ }))
1271
+ });
1272
+ const parseGradedGroupWidget = parseWidget(constant("graded-group"), parseGradedGroupWidgetOptions);
1273
+
1274
+ const parseGradedGroupSetWidget = parseWidget(constant("graded-group-set"), object({
1275
+ gradedGroups: array(parseGradedGroupWidgetOptions)
1276
+ }));
1277
+
1278
+ /**
1279
+ * discriminatedUnion() should be preferred over union() when parsing a
1280
+ * discriminated union type, because discriminatedUnion() produces more
1281
+ * understandable failure messages. It takes the discriminant as the source of
1282
+ * truth for which variant is to be parsed, and expects the other data to match
1283
+ * that variant.
1284
+ */
1285
+ function discriminatedUnionOn(discriminantKey) {
1286
+ const noMoreBranches = (raw, ctx) => {
1287
+ if (!isObject(raw)) {
1288
+ return ctx.failure("object", raw);
1289
+ }
1290
+ return ctx.forSubtree(discriminantKey).failure("a valid value", raw[discriminantKey]);
1291
+ };
1292
+ return new DiscriminatedUnionBuilder(discriminantKey, noMoreBranches);
1293
+ }
1294
+ class DiscriminatedUnionBuilder {
1295
+ constructor(discriminantKey, parser) {
1296
+ this.discriminantKey = discriminantKey;
1297
+ this.parser = parser;
1298
+ }
1299
+ withBranch(discriminantValue, parseNewVariant) {
1300
+ const parseNewBranch = discriminatedUnionBranch(this.discriminantKey, discriminantValue, parseNewVariant, this.parser);
1301
+ return new DiscriminatedUnionBuilder(this.discriminantKey, parseNewBranch);
1302
+ }
1303
+ }
1304
+ function discriminatedUnionBranch(discriminantKey, discriminantValue, parseVariant, parseOtherBranches) {
1305
+ return (raw, ctx) => {
1306
+ if (!isObject(raw)) {
1307
+ return ctx.failure("object", raw);
1308
+ }
1309
+ if (raw[discriminantKey] === discriminantValue) {
1310
+ return parseVariant(raw, ctx);
1311
+ }
1312
+ return parseOtherBranches(raw, ctx);
1313
+ };
1314
+ }
1315
+
1316
+ const pairOfNumbers$3 = pair(number, number);
1317
+ const pairOfPoints = pair(pairOfNumbers$3, pairOfNumbers$3);
1318
+ const parseGrapherWidget = parseWidget(constant("grapher"), object({
1319
+ availableTypes: array(enumeration("absolute_value", "exponential", "linear", "logarithm", "quadratic", "sinusoid", "tangent")),
1320
+ correct: discriminatedUnionOn("type").withBranch("absolute_value", object({
1321
+ type: constant("absolute_value"),
1322
+ coords: nullable(pairOfPoints)
1323
+ })).withBranch("exponential", object({
1324
+ type: constant("exponential"),
1325
+ asymptote: pairOfPoints,
1326
+ coords: nullable(pairOfPoints)
1327
+ })).withBranch("linear", object({
1328
+ type: constant("linear"),
1329
+ coords: nullable(pairOfPoints)
1330
+ })).withBranch("logarithm", object({
1331
+ type: constant("logarithm"),
1332
+ asymptote: pairOfPoints,
1333
+ coords: nullable(pairOfPoints)
1334
+ })).withBranch("quadratic", object({
1335
+ type: constant("quadratic"),
1336
+ coords: nullable(pairOfPoints)
1337
+ })).withBranch("sinusoid", object({
1338
+ type: constant("sinusoid"),
1339
+ coords: nullable(pairOfPoints)
1340
+ })).withBranch("tangent", object({
1341
+ type: constant("tangent"),
1342
+ coords: nullable(pairOfPoints)
1343
+ })).parser,
1344
+ graph: object({
1345
+ backgroundImage: object({
1346
+ bottom: optional(number),
1347
+ height: optional(number),
1348
+ left: optional(number),
1349
+ scale: optional(number),
1350
+ url: optional(nullable(string)),
1351
+ width: optional(number)
1352
+ }),
1353
+ box: optional(pairOfNumbers$3),
1354
+ editableSettings: optional(array(enumeration("graph", "snap", "image", "measure"))),
1355
+ gridStep: optional(pairOfNumbers$3),
1356
+ labels: pair(string, string),
1357
+ markings: enumeration("graph", "none", "grid"),
1358
+ range: pair(pairOfNumbers$3, pairOfNumbers$3),
1359
+ rulerLabel: constant(""),
1360
+ rulerTicks: number,
1361
+ showProtractor: optional(boolean),
1362
+ showRuler: optional(boolean),
1363
+ showTooltips: optional(boolean),
1364
+ snapStep: optional(pairOfNumbers$3),
1365
+ step: pairOfNumbers$3,
1366
+ valid: optional(union(boolean).or(string).parser)
1367
+ })
1368
+ }));
1369
+
1370
+ const parseGroupWidget = parseWidget(constant("group"),
1371
+ // This module has an import cycle with parsePerseusRenderer.
1372
+ // The anonymous function below ensures that we don't try to access
1373
+ // parsePerseusRenderer before it's defined.
1374
+ (rawVal, ctx) => parsePerseusRenderer(rawVal, ctx));
1375
+
1376
+ const parseIframeWidget = parseWidget(constant("iframe"), object({
1377
+ url: string,
1378
+ settings: optional(array(object({
1379
+ name: string,
1380
+ value: string
1381
+ }))),
1382
+ width: union(number).or(string).parser,
1383
+ height: union(number).or(string).parser,
1384
+ allowFullScreen: defaulted(boolean, () => false),
1385
+ allowTopNavigation: optional(boolean),
1386
+ static: defaulted(boolean, () => false)
1387
+ }));
1388
+
1389
+ const stringToNumber = (rawValue, ctx) => {
1390
+ if (typeof rawValue === "number") {
1391
+ return ctx.success(rawValue);
1392
+ }
1393
+ const parsedNumber = +rawValue;
1394
+ if (rawValue === "" || isNaN(parsedNumber)) {
1395
+ return ctx.failure("a number or numeric string", rawValue);
1396
+ }
1397
+ return ctx.success(parsedNumber);
1398
+ };
1399
+
1400
+ function emptyToZero(x) {
1401
+ return x === "" ? 0 : x;
1402
+ }
1403
+ const imageDimensionToNumber = pipeParsers(union(number).or(string).parser)
1404
+ // In this specific case, empty string is equivalent to zero. An empty
1405
+ // string parses to either NaN (using parseInt) or 0 (using unary +) and
1406
+ // CSS will treat NaN as invalid and default to 0 instead.
1407
+ .then(convert(emptyToZero)).then(stringToNumber).parser;
1408
+ const dimensionOrUndefined = defaulted(imageDimensionToNumber, () => undefined);
1409
+ const parsePerseusImageBackground = object({
1410
+ url: optional(nullable(string)),
1411
+ width: dimensionOrUndefined,
1412
+ height: dimensionOrUndefined,
1413
+ top: dimensionOrUndefined,
1414
+ left: dimensionOrUndefined,
1415
+ bottom: dimensionOrUndefined,
1416
+ scale: dimensionOrUndefined
1417
+ });
1418
+
1419
+ const pairOfNumbers$2 = pair(number, number);
1420
+ const parseImageWidget = parseWidget(constant("image"), object({
1421
+ title: optional(string),
1422
+ caption: optional(string),
1423
+ alt: optional(string),
1424
+ backgroundImage: parsePerseusImageBackground,
1425
+ static: optional(boolean),
1426
+ labels: optional(array(object({
1427
+ content: string,
1428
+ alignment: string,
1429
+ coordinates: array(number)
1430
+ }))),
1431
+ range: optional(pair(pairOfNumbers$2, pairOfNumbers$2)),
1432
+ box: optional(pairOfNumbers$2)
1433
+ }));
1434
+
1435
+ const booleanToString = (rawValue, ctx) => {
1436
+ if (typeof rawValue === "boolean") {
1437
+ return ctx.success(String(rawValue));
1438
+ }
1439
+ return ctx.failure("boolean", rawValue);
1440
+ };
1441
+ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
1442
+ answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
1443
+ inexact: optional(boolean),
1444
+ maxError: optional(union(number).or(string).parser),
1445
+ rightAlign: optional(boolean),
1446
+ simplify: enumeration("required", "optional", "enforced"),
1447
+ size: enumeration("normal", "small"),
1448
+ // TODO(benchristel): there are some content items where value is a
1449
+ // boolean, even though that makes no sense. We should figure out if
1450
+ // those content items are actually published anywhere, and consider
1451
+ // updating them.
1452
+ value: union(number).or(string).or(booleanToString).parser,
1453
+ customKeypad: optional(boolean)
1454
+ }));
1455
+
1456
+ const pairOfNumbers$1 = pair(number, number);
1457
+ const stringOrEmpty = defaulted(string, () => "");
1458
+ const parseKey = pipeParsers(optional(string)).then(convert(String)).parser;
1459
+ const parseFunctionElement = object({
1460
+ type: constant("function"),
1461
+ key: parseKey,
1462
+ options: object({
1463
+ value: string,
1464
+ funcName: string,
1465
+ rangeMin: string,
1466
+ rangeMax: string,
1467
+ color: string,
1468
+ strokeDasharray: string,
1469
+ strokeWidth: number
1470
+ })
1471
+ });
1472
+ const parseLabelElement = object({
1473
+ type: constant("label"),
1474
+ key: parseKey,
1475
+ options: object({
1476
+ label: string,
1477
+ color: string,
1478
+ coordX: string,
1479
+ coordY: string
1480
+ })
1481
+ });
1482
+ const parseLineElement = object({
1483
+ type: constant("line"),
1484
+ key: parseKey,
1485
+ options: object({
1486
+ color: string,
1487
+ startX: string,
1488
+ startY: string,
1489
+ endX: string,
1490
+ endY: string,
1491
+ strokeDasharray: string,
1492
+ strokeWidth: number,
1493
+ arrows: string
1494
+ })
1495
+ });
1496
+ const parseMovableLineElement = object({
1497
+ type: constant("movable-line"),
1498
+ key: parseKey,
1499
+ options: object({
1500
+ startX: string,
1501
+ startY: string,
1502
+ startSubscript: number,
1503
+ endX: string,
1504
+ endY: string,
1505
+ endSubscript: number,
1506
+ constraint: string,
1507
+ snap: number,
1508
+ constraintFn: string,
1509
+ constraintXMin: string,
1510
+ constraintXMax: string,
1511
+ constraintYMin: string,
1512
+ constraintYMax: string
1513
+ })
1514
+ });
1515
+ const parseMovablePointElement = object({
1516
+ type: constant("movable-point"),
1517
+ key: parseKey,
1518
+ options: object({
1519
+ startX: string,
1520
+ startY: string,
1521
+ varSubscript: number,
1522
+ constraint: string,
1523
+ snap: number,
1524
+ constraintFn: string,
1525
+ constraintXMin: stringOrEmpty,
1526
+ constraintXMax: stringOrEmpty,
1527
+ constraintYMin: stringOrEmpty,
1528
+ constraintYMax: stringOrEmpty
1529
+ })
1530
+ });
1531
+ const parseParametricElement = object({
1532
+ type: constant("parametric"),
1533
+ key: parseKey,
1534
+ options: object({
1535
+ x: string,
1536
+ y: string,
1537
+ rangeMin: string,
1538
+ rangeMax: string,
1539
+ color: string,
1540
+ strokeDasharray: string,
1541
+ strokeWidth: number
1542
+ })
1543
+ });
1544
+ const parsePointElement = object({
1545
+ type: constant("point"),
1546
+ key: parseKey,
1547
+ options: object({
1548
+ color: string,
1549
+ coordX: string,
1550
+ coordY: string
1551
+ })
1552
+ });
1553
+ const parseRectangleElement = object({
1554
+ type: constant("rectangle"),
1555
+ key: parseKey,
1556
+ options: object({
1557
+ color: string,
1558
+ coordX: string,
1559
+ coordY: string,
1560
+ width: string,
1561
+ height: string
1562
+ })
1563
+ });
1564
+ const parseInteractionWidget = parseWidget(constant("interaction"), object({
1565
+ static: defaulted(boolean, () => false),
1566
+ graph: object({
1567
+ editableSettings: optional(array(enumeration("canvas", "graph"))),
1568
+ box: pairOfNumbers$1,
1569
+ labels: array(string),
1570
+ range: pair(pairOfNumbers$1, pairOfNumbers$1),
1571
+ gridStep: pairOfNumbers$1,
1572
+ markings: enumeration("graph", "grid", "none"),
1573
+ snapStep: optional(pairOfNumbers$1),
1574
+ valid: optional(union(boolean).or(string).parser),
1575
+ backgroundImage: optional(parsePerseusImageBackground),
1576
+ showProtractor: optional(boolean),
1577
+ showRuler: optional(boolean),
1578
+ rulerLabel: optional(string),
1579
+ rulerTicks: optional(number),
1580
+ tickStep: pairOfNumbers$1
1581
+ }),
1582
+ 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)
1583
+ }));
1584
+
1585
+ /**
1586
+ * The Perseus "data schema" file.
1587
+ *
1588
+ * This file, and the types in it, represents the "data schema" that Perseus
1589
+ * uses. The @khanacademy/perseus-editor package edits and produces objects
1590
+ * that conform to the types in this file. Similarly, the top-level renderers
1591
+ * in @khanacademy/perseus, consume objects that conform to these types.
1592
+ *
1593
+ * WARNING: This file should not import any types from elsewhere so that it is
1594
+ * easy to reason about changes that alter the Perseus schema. This helps
1595
+ * ensure that it is not changed accidentally when upgrading a dependant
1596
+ * package or other part of Perseus code. Note that TypeScript does type
1597
+ * checking via something called "structural typing". This means that as long
1598
+ * as the shape of a type matches, the name it goes by doesn't matter. As a
1599
+ * result, a `Coord` type that looks like this `[x: number, y: number]` is
1600
+ * _identical_, in TypeScript's eyes, to this `Vector2` type `[x: number, y:
1601
+ * number]`. Also, with tuples, the labels for each entry is ignored, so `[x:
1602
+ * number, y: number]` is compatible with `[min: number, max: number]`. The
1603
+ * labels are for humans, not TypeScript. :)
1604
+ *
1605
+ * If you make changes to types in this file, be very sure that:
1606
+ *
1607
+ * a) the changes are backwards compatible. If they are not, old data from
1608
+ * previous versions of the "schema" could become unrenderable, or worse,
1609
+ * introduce hard-to-diagnose bugs.
1610
+ * b) the parsing code (`util/parse-perseus-json/`) is updated to handle
1611
+ * the new format _as well as_ the old format.
1612
+ */
1613
+
1614
+ // TODO(FEI-4010): Remove `Perseus` prefix for all types here
1615
+
1616
+ // Same name as Mafs
1617
+
1618
+ /**
1619
+ * A utility type that constructs a widget map from a "registry interface".
1620
+ * The keys of the registry should be the widget type (aka, "categorizer" or
1621
+ * "radio", etc) and the value should be the option type stored in the value
1622
+ * of the map.
1623
+ *
1624
+ * You can think of this as a type that generates another type. We use
1625
+ * "registry interfaces" as a way to keep a set of widget types to their data
1626
+ * type in several places in Perseus. This type then allows us to generate a
1627
+ * map type that maps a widget id to its data type and keep strong typing by
1628
+ * widget id.
1629
+ *
1630
+ * For example, given a fictitious registry such as this:
1631
+ *
1632
+ * ```
1633
+ * interface DummyRegistry {
1634
+ * categorizer: { categories: ReadonlyArray<string> };
1635
+ * dropdown: { choices: ReadonlyArray<string> }:
1636
+ * }
1637
+ * ```
1638
+ *
1639
+ * If we create a DummyMap using this helper:
1640
+ *
1641
+ * ```
1642
+ * type DummyMap = MakeWidgetMap<DummyRegistry>;
1643
+ * ```
1644
+ *
1645
+ * We'll get a map that looks like this:
1646
+ *
1647
+ * ```
1648
+ * type DummyMap = {
1649
+ * `categorizer ${number}`: { categories: ReadonlyArray<string> };
1650
+ * `dropdown ${number}`: { choices: ReadonlyArray<string> };
1651
+ * }
1652
+ * ```
1653
+ *
1654
+ * We use interfaces for the registries so that they can be extended in cases
1655
+ * where the consuming app brings along their own widgets. Interfaces in
1656
+ * TypeScript are always open (ie. you can extend them) whereas types aren't.
1657
+ */
1658
+
1659
+ /**
1660
+ * Our core set of Perseus widgets.
1661
+ *
1662
+ * This interface is the basis for "registering" all Perseus widget types.
1663
+ * There should be one key/value pair for each supported widget. If you create
1664
+ * a new widget, an entry should be added to this interface. Note that this
1665
+ * only registers the widget options type, you'll also need to register the
1666
+ * widget so that it's available at runtime (@see
1667
+ * {@link file://./widgets.ts#registerWidget}).
1668
+ *
1669
+ * Importantly, the key should be the name that is used in widget IDs. For most
1670
+ * widgets that is the same as the widget option's `type` field. In cases where
1671
+ * a widget has been deprecated and replaced with the deprecated-standin
1672
+ * widget, it should be the original widget type!
1673
+ *
1674
+ * If you define the widget outside of this package, you can still add the new
1675
+ * widget to this interface by writing the following in that package that
1676
+ * contains the widget. TypeScript will merge that definition of the
1677
+ * `PerseusWidgets` with the one defined below.
1678
+ *
1679
+ * ```typescript
1680
+ * declare module "@khanacademy/perseus-core" {
1681
+ * interface PerseusWidgetTypes {
1682
+ * // A new widget
1683
+ * "new-awesomeness": MyAwesomeNewWidget;
1684
+ *
1685
+ * // A deprecated widget
1686
+ * "super-old-widget": DeprecatedStandinWidget;
1687
+ * }
1688
+ * }
1689
+ *
1690
+ * // The new widget's options definition
1691
+ * type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>;
1692
+ *
1693
+ * // The deprecated widget's options definition
1694
+ * type SuperOldWidget = WidgetOptions<'super-old-widget', object>;
1695
+ * ```
1696
+ *
1697
+ * This interface can be extended through the magic of TypeScript "Declaration
1698
+ * merging". Specifically, we augment this module and extend this interface.
1699
+ *
1700
+ * @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
1701
+ */
1702
+
1703
+ /**
1704
+ * A map of widget IDs to widget options. This is most often used as the type
1705
+ * for a set of widgets defined in a `PerseusItem` but can also be useful to
1706
+ * represent a function parameter where only `widgets` from a `PerseusItem` are
1707
+ * needed. Today Widget IDs are made up of the widget type and an incrementing
1708
+ * integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid
1709
+ * reading/parsing the widget id to derive any information from it, except in
1710
+ * the case of this map.
1711
+ *
1712
+ * @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
1713
+ * by augmenting the PerseusWidgetTypes with new widget types!
1714
+ */
1715
+
1716
+ /**
1717
+ * PerseusWidget is a union of all the different types of widget options that
1718
+ * Perseus knows about.
1719
+ *
1720
+ * Thanks to it being based on PerseusWidgetTypes interface, this union is
1721
+ * automatically extended to include widgets used in tests without those widget
1722
+ * option types seeping into our production types.
1723
+ *
1724
+ * @see MockWidget for an example
1725
+ */
1726
+
1727
+ /**
1728
+ * A "PerseusItem" is a classic Perseus item. It is rendered by the
1729
+ * `ServerItemRenderer` and the layout is pre-set.
1730
+ *
1731
+ * To render more complex Perseus items, see the `Item` type in the multi item
1732
+ * area.
1733
+ */
1734
+
1735
+ /**
1736
+ * A "PerseusArticle" is an item that is meant to be rendered as an article.
1737
+ * This item is never scored and is rendered by the `ArticleRenderer`.
1738
+ */
1739
+
1740
+ const ItemExtras = [
1741
+ // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
1742
+ "calculator",
1743
+ // The user might benefit from using a statistics Chi Squared Table like https://people.richland.edu/james/lecture/m170/tbl-chi.html
1744
+ "chi2Table",
1745
+ // The user might benefit from a monthly payments calculator. Provided on Khan Academy when true
1746
+ "financialCalculatorMonthlyPayment",
1747
+ // The user might benefit from a total amount calculator. Provided on Khan Academy when true
1748
+ "financialCalculatorTotalAmount",
1749
+ // The user might benefit from a time to pay off calculator. Provided on Khan Academy when true
1750
+ "financialCalculatorTimeToPayOff",
1751
+ // The user might benefit from using a Periodic Table of Elements. Provided on Khan Academy when true
1752
+ "periodicTable",
1753
+ // The user might benefit from using a Periodic Table of Elements with key. Provided on Khan Academy when true
1754
+ "periodicTableWithKey",
1755
+ // The user might benefit from using a statistics T Table like https://www.statisticshowto.com/tables/t-distribution-table/
1756
+ "tTable",
1757
+ // The user might benefit from using a statistics Z Table like https://www.ztable.net/
1758
+ "zTable"];
1759
+
1760
+ /**
1761
+ * The type representing the common structure of all widget's options. The
1762
+ * `Options` generic type represents the widget-specific option data.
1763
+ */
1764
+
1765
+ // prettier-ignore
1766
+
1767
+ // prettier-ignore
1768
+
1769
+ // prettier-ignore
1770
+
1771
+ // prettier-ignore
1772
+
1773
+ // prettier-ignore
1774
+
1775
+ // prettier-ignore
1776
+
1777
+ // prettier-ignore
1778
+
1779
+ // prettier-ignore
1780
+
1781
+ // prettier-ignore
1782
+
1783
+ // prettier-ignore
1784
+
1785
+ // prettier-ignore
1786
+
1787
+ // prettier-ignore
1788
+
1789
+ // prettier-ignore
1790
+
1791
+ // prettier-ignore
1792
+
1793
+ // prettier-ignore
1794
+
1795
+ // prettier-ignore
1796
+
1797
+ // prettier-ignore
1798
+
1799
+ // prettier-ignore
1800
+
1801
+ // prettier-ignore
1802
+
1803
+ // prettier-ignore
1804
+
1805
+ // prettier-ignore
1806
+
1807
+ // prettier-ignore
1808
+
1809
+ // prettier-ignore
1810
+
1811
+ // prettier-ignore
1812
+
1813
+ // prettier-ignore
1814
+
1815
+ // prettier-ignore
1816
+
1817
+ // prettier-ignore
1818
+
1819
+ // prettier-ignore
1820
+
1821
+ // prettier-ignore
1822
+
1823
+ // prettier-ignore
1824
+
1825
+ // prettier-ignore
1826
+
1827
+ // prettier-ignore
1828
+
1829
+ // prettier-ignore
1830
+
1831
+ //prettier-ignore
1832
+
1833
+ /**
1834
+ * A background image applied to various widgets.
1835
+ */
1836
+
1837
+ /**
1838
+ * The type of markings to display on the graph.
1839
+ * - axes: shows the axes without the gride lines
1840
+ * - graph: shows the axes and the grid lines
1841
+ * - grid: shows only the grid lines
1842
+ * - none: shows no markings
1843
+ */
1844
+
1845
+ const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
1846
+
1847
+ // 2D range: xMin, xMax, yMin, yMax
1848
+
1849
+ const lockedFigureColorNames = ["blue", "green", "grayH", "purple", "pink", "orange", "red"];
1850
+ const lockedFigureColors = {
1851
+ blue: "#3D7586",
1852
+ green: "#447A53",
1853
+ grayH: "#3B3D45",
1854
+ purple: "#594094",
1855
+ pink: "#B25071",
1856
+ red: "#D92916",
1857
+ orange: "#946700"
1858
+ };
1859
+ const lockedFigureFillStyles = {
1860
+ none: 0,
1861
+ white: 1,
1862
+ translucent: 0.4,
1863
+ solid: 1
1864
+ };
1865
+
1866
+ // Not associated with a specific figure
1867
+
1868
+ const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
1869
+
1870
+ // Used to represent 2-D points and ranges
1871
+ const pairOfNumbers = pair(number, number);
1872
+ const parsePerseusGraphTypeAngle = object({
1873
+ type: constant("angle"),
1874
+ showAngles: optional(boolean),
1875
+ allowReflexAngles: optional(boolean),
1876
+ angleOffsetDeg: optional(number),
1877
+ snapDegrees: optional(number),
1878
+ match: optional(constant("congruent")),
1879
+ coords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
1880
+ startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))
1881
+ });
1882
+ const parsePerseusGraphTypeCircle = object({
1883
+ type: constant("circle"),
1884
+ center: optional(pairOfNumbers),
1885
+ radius: optional(number),
1886
+ startCoords: optional(object({
1887
+ center: pairOfNumbers,
1888
+ radius: number
1889
+ })),
1890
+ // TODO: remove coord? it's legacy.
1891
+ coord: optional(pairOfNumbers)
1892
+ });
1893
+ const parsePerseusGraphTypeLinear = object({
1894
+ type: constant("linear"),
1895
+ coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
1896
+ startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
1897
+ // TODO: remove coord? it's legacy.
1898
+ coord: optional(pairOfNumbers)
1899
+ });
1900
+ const parsePerseusGraphTypeLinearSystem = object({
1901
+ type: constant("linear-system"),
1902
+ // TODO(benchristel): default coords to empty array?
1903
+ coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
1904
+ startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
1905
+ // TODO: remove coord? it's legacy.
1906
+ coord: optional(pairOfNumbers)
1907
+ });
1908
+ const parsePerseusGraphTypeNone = object({
1909
+ type: constant("none")
1910
+ });
1911
+ const parsePerseusGraphTypePoint = object({
1912
+ type: constant("point"),
1913
+ numPoints: optional(union(number).or(constant("unlimited")).parser),
1914
+ coords: optional(nullable(array(pairOfNumbers))),
1915
+ startCoords: optional(array(pairOfNumbers)),
1916
+ // TODO: remove coord? it's legacy.
1917
+ coord: optional(pairOfNumbers)
1918
+ });
1919
+ const parsePerseusGraphTypePolygon = object({
1920
+ type: constant("polygon"),
1921
+ numSides: optional(union(number).or(constant("unlimited")).parser),
1922
+ showAngles: optional(boolean),
1923
+ showSides: optional(boolean),
1924
+ snapTo: optional(enumeration("grid", "angles", "sides")),
1925
+ match: optional(enumeration("similar", "congruent", "approx", "exact")),
1926
+ startCoords: optional(array(pairOfNumbers)),
1927
+ // TODO: remove coord? it's legacy.
1928
+ coord: optional(pairOfNumbers)
1929
+ });
1930
+ const parsePerseusGraphTypeQuadratic = object({
1931
+ type: constant("quadratic"),
1932
+ coords: optional(nullable(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))),
1933
+ startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
1934
+ // TODO: remove coord? it's legacy.
1935
+ coord: optional(pairOfNumbers)
1936
+ });
1937
+ const parsePerseusGraphTypeRay = object({
1938
+ type: constant("ray"),
1939
+ coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
1940
+ startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
1941
+ // TODO: remove coord? it's legacy.
1942
+ coord: optional(pairOfNumbers)
1943
+ });
1944
+ const parsePerseusGraphTypeSegment = object({
1945
+ type: constant("segment"),
1946
+ // TODO(benchristel): default numSegments?
1947
+ numSegments: optional(number),
1948
+ coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
1949
+ startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
1950
+ // TODO: remove coord? it's legacy.
1951
+ coord: optional(pairOfNumbers)
1952
+ });
1953
+ const parsePerseusGraphTypeSinusoid = object({
1954
+ type: constant("sinusoid"),
1955
+ coords: optional(nullable(array(pairOfNumbers))),
1956
+ startCoords: optional(array(pairOfNumbers)),
1957
+ // TODO: remove coord? it's legacy.
1958
+ coord: optional(pairOfNumbers)
1959
+ });
1960
+ 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;
1961
+ const parseLockedFigureColor = enumeration(...lockedFigureColorNames);
1962
+ const parseLockedFigureFillType = enumeration("none", "white", "translucent", "solid");
1963
+ const parseLockedLineStyle = enumeration("solid", "dashed");
1964
+ const parseLockedLabelType = object({
1965
+ type: constant("label"),
1966
+ coord: pairOfNumbers,
1967
+ text: string,
1968
+ color: parseLockedFigureColor,
1969
+ size: enumeration("small", "medium", "large")
1970
+ });
1971
+ const parseLockedPointType = object({
1972
+ type: constant("point"),
1973
+ coord: pairOfNumbers,
1974
+ color: parseLockedFigureColor,
1975
+ filled: boolean,
1976
+ // TODO(benchristel): default labels to empty array?
1977
+ labels: optional(array(parseLockedLabelType)),
1978
+ ariaLabel: optional(string)
1979
+ });
1980
+ const parseLockedLineType = object({
1981
+ type: constant("line"),
1982
+ kind: enumeration("line", "ray", "segment"),
1983
+ points: pair(parseLockedPointType, parseLockedPointType),
1984
+ color: parseLockedFigureColor,
1985
+ lineStyle: parseLockedLineStyle,
1986
+ showPoint1: defaulted(boolean, () => false),
1987
+ showPoint2: defaulted(boolean, () => false),
1988
+ // TODO(benchristel): default labels to empty array?
1989
+ labels: optional(array(parseLockedLabelType)),
1990
+ ariaLabel: optional(string)
1991
+ });
1992
+ const parseLockedVectorType = object({
1993
+ type: constant("vector"),
1994
+ points: pair(pairOfNumbers, pairOfNumbers),
1995
+ color: parseLockedFigureColor,
1996
+ // TODO(benchristel): default labels to empty array?
1997
+ labels: optional(array(parseLockedLabelType)),
1998
+ ariaLabel: optional(string)
1999
+ });
2000
+ const parseLockedEllipseType = object({
2001
+ type: constant("ellipse"),
2002
+ center: pairOfNumbers,
2003
+ radius: pairOfNumbers,
2004
+ angle: number,
2005
+ color: parseLockedFigureColor,
2006
+ fillStyle: parseLockedFigureFillType,
2007
+ strokeStyle: parseLockedLineStyle,
2008
+ // TODO(benchristel): default labels to empty array?
2009
+ labels: optional(array(parseLockedLabelType)),
2010
+ ariaLabel: optional(string)
2011
+ });
2012
+ const parseLockedPolygonType = object({
2013
+ type: constant("polygon"),
2014
+ points: array(pairOfNumbers),
2015
+ color: parseLockedFigureColor,
2016
+ showVertices: boolean,
2017
+ fillStyle: parseLockedFigureFillType,
2018
+ strokeStyle: parseLockedLineStyle,
2019
+ // TODO(benchristel): default labels to empty array?
2020
+ labels: optional(array(parseLockedLabelType)),
2021
+ ariaLabel: optional(string)
2022
+ });
2023
+ const parseLockedFunctionType = object({
2024
+ type: constant("function"),
2025
+ color: parseLockedFigureColor,
2026
+ strokeStyle: parseLockedLineStyle,
2027
+ equation: string,
2028
+ directionalAxis: enumeration("x", "y"),
2029
+ domain: optional(pairOfNumbers),
2030
+ // TODO(benchristel): default labels to empty array?
2031
+ labels: optional(array(parseLockedLabelType)),
2032
+ ariaLabel: optional(string)
2033
+ });
2034
+ 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;
2035
+ const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
2036
+ step: pairOfNumbers,
2037
+ // TODO(benchristel): rather than making gridStep and snapStep
2038
+ // optional, we should duplicate the defaulting logic from the
2039
+ // InteractiveGraph component. See parse-perseus-json/README.md for
2040
+ // why.
2041
+ gridStep: optional(pairOfNumbers),
2042
+ snapStep: optional(pairOfNumbers),
2043
+ backgroundImage: optional(parsePerseusImageBackground),
2044
+ markings: enumeration("graph", "grid", "none"),
2045
+ labels: optional(array(string)),
2046
+ showProtractor: boolean,
2047
+ showRuler: optional(boolean),
2048
+ showTooltips: optional(boolean),
2049
+ rulerLabel: optional(string),
2050
+ rulerTicks: optional(number),
2051
+ range: pair(pairOfNumbers, pairOfNumbers),
2052
+ // NOTE(benchristel): I copied the default graph from
2053
+ // interactive-graph.tsx. See the parse-perseus-json/README.md for
2054
+ // an explanation of why we want to duplicate the default here.
2055
+ graph: defaulted(parsePerseusGraphType, () => ({
2056
+ type: "linear"
2057
+ })),
2058
+ correct: parsePerseusGraphType,
2059
+ // TODO(benchristel): default lockedFigures to empty array
2060
+ lockedFigures: optional(array(parseLockedFigure)),
2061
+ fullGraphLabel: optional(string),
2062
+ fullGraphAriaDescription: optional(string)
2063
+ }));
2064
+
2065
+ const parseLabelImageWidget = parseWidget(constant("label-image"), object({
2066
+ choices: array(string),
2067
+ imageUrl: string,
2068
+ imageAlt: string,
2069
+ imageHeight: number,
2070
+ imageWidth: number,
2071
+ markers: array(object({
2072
+ answers: array(string),
2073
+ label: string,
2074
+ x: number,
2075
+ y: number
2076
+ })),
2077
+ hideChoicesFromInstructions: boolean,
2078
+ multipleAnswers: boolean,
2079
+ static: defaulted(boolean, () => false)
2080
+ }));
2081
+
2082
+ const parseMatcherWidget = parseWidget(constant("matcher"), object({
2083
+ labels: array(string),
2084
+ left: array(string),
2085
+ right: array(string),
2086
+ orderMatters: boolean,
2087
+ padding: boolean
2088
+ }));
2089
+
2090
+ const numberOrString = union(number).or(string).parser;
2091
+ const numeric = pipeParsers(defaulted(numberOrString, () => NaN)).then(stringToNumber).parser;
2092
+ const parseMatrixWidget = parseWidget(defaulted(constant("matrix"), () => "matrix"), object({
2093
+ prefix: optional(string),
2094
+ suffix: optional(string),
2095
+ answers: array(array(numeric)),
2096
+ cursorPosition: optional(array(number)),
2097
+ matrixBoardSize: array(number),
2098
+ static: optional(boolean)
2099
+ }));
2100
+
2101
+ const parseMeasurerWidget = parseWidget(constant("measurer"), object({
2102
+ // The default value for image comes from measurer.tsx.
2103
+ // See parse-perseus-json/README.md for why we want to duplicate the
2104
+ // defaults here.
2105
+ image: defaulted(parsePerseusImageBackground, () => ({
2106
+ url: null,
2107
+ top: 0,
2108
+ left: 0
2109
+ })),
2110
+ showProtractor: boolean,
2111
+ showRuler: boolean,
2112
+ rulerLabel: string,
2113
+ rulerTicks: number,
2114
+ rulerPixels: number,
2115
+ rulerLength: number,
2116
+ box: pair(number, number),
2117
+ // TODO(benchristel): static is not used. Remove it?
2118
+ static: defaulted(boolean, () => false)
2119
+ }));
2120
+
2121
+ const parseMoleculeRendererWidget = parseWidget(constant("molecule-renderer"), object({
2122
+ widgetId: string,
2123
+ rotationAngle: optional(number),
2124
+ smiles: optional(string)
2125
+ }));
2126
+
2127
+ const emptyStringToNull = pipeParsers(constant("")).then(convert(() => null)).parser;
2128
+ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
2129
+ range: array(number),
2130
+ labelRange: array(nullable(union(number).or(emptyStringToNull).parser)),
2131
+ labelStyle: string,
2132
+ labelTicks: boolean,
2133
+ isTickCtrl: optional(nullable(boolean)),
2134
+ divisionRange: array(number),
2135
+ numDivisions: optional(nullable(number)),
2136
+ // NOTE(benchristel): I copied the default snapDivisions from
2137
+ // number-line.tsx. See the parse-perseus-json/README.md for
2138
+ // an explanation of why we want to duplicate the default here.
2139
+ snapDivisions: defaulted(number, () => 2),
2140
+ tickStep: optional(nullable(number)),
2141
+ correctRel: optional(nullable(string)),
2142
+ correctX: nullable(number),
2143
+ initialX: optional(nullable(number)),
2144
+ showTooltips: optional(boolean),
2145
+ static: defaulted(boolean, () => false)
2146
+ }));
2147
+
2148
+ const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "decimal", "percent", "pi");
2149
+ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
2150
+ answers: array(object({
2151
+ message: string,
2152
+ // TODO(benchristel): value should never be null or undefined,
2153
+ // but we have some content where it is anyway. If we backfill
2154
+ // the data, simplify this.
2155
+ value: optional(nullable(number)),
2156
+ status: string,
2157
+ answerForms: defaulted(array(parseMathFormat), () => undefined),
2158
+ strict: boolean,
2159
+ maxError: optional(nullable(number)),
2160
+ // TODO(benchristel): simplify should never be a boolean, but we
2161
+ // have some content where it is anyway. If we ever backfill
2162
+ // the data, we should simplify `simplify`.
2163
+ simplify: optional(nullable(union(string).or(pipeParsers(boolean).then(convert(String)).parser).parser))
2164
+ })),
2165
+ labelText: optional(string),
2166
+ size: string,
2167
+ coefficient: defaulted(boolean, () => false),
2168
+ rightAlign: optional(boolean),
2169
+ static: defaulted(boolean, () => false),
2170
+ answerForms: optional(array(object({
2171
+ name: parseMathFormat,
2172
+ simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
2173
+ })))
2174
+ }));
2175
+
2176
+ // There is an import cycle between orderer-widget.ts and perseus-renderer.ts.
2177
+ // This wrapper ensures that we don't refer to parsePerseusRenderer before
2178
+ // it's defined.
2179
+ function parseRenderer(rawValue, ctx) {
2180
+ return parsePerseusRenderer(rawValue, ctx);
2181
+ }
2182
+ const largeToAuto = (height, ctx) => {
2183
+ if (height === "large") {
2184
+ return ctx.success("auto");
2185
+ }
2186
+ return ctx.success(height);
2187
+ };
2188
+ const parseOrdererWidget = parseWidget(constant("orderer"), object({
2189
+ options: defaulted(array(parseRenderer), () => []),
2190
+ correctOptions: array(parseRenderer),
2191
+ otherOptions: array(parseRenderer),
2192
+ height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
2193
+ layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
2194
+ }));
2195
+
2196
+ const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
2197
+ passageNumber: number,
2198
+ referenceNumber: number,
2199
+ summaryText: optional(string)
2200
+ }));
2201
+
2202
+ const parsePassageWidget = parseWidget(constant("passage"), object({
2203
+ footnotes: defaulted(string, () => ""),
2204
+ passageText: string,
2205
+ passageTitle: defaulted(string, () => ""),
2206
+ showLineNumbers: boolean,
2207
+ static: defaulted(boolean, () => false)
2208
+ }));
2209
+
2210
+ const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
2211
+ url: string,
2212
+ description: string
2213
+ }));
2214
+
2215
+ const parsePlotterWidget = parseWidget(constant("plotter"), object({
2216
+ labels: array(string),
2217
+ categories: array(string),
2218
+ type: enumeration(...plotterPlotTypes),
2219
+ maxY: number,
2220
+ // The default value for scaleY comes from plotter.tsx.
2221
+ // See parse-perseus-json/README.md for why we want to duplicate the
2222
+ // defaults here.
2223
+ scaleY: defaulted(number, () => 1),
2224
+ labelInterval: optional(nullable(number)),
2225
+ // The default value for snapsPerLine comes from plotter.tsx.
2226
+ // See parse-perseus-json/README.md for why we want to duplicate the
2227
+ // defaults here.
2228
+ snapsPerLine: defaulted(number, () => 2),
2229
+ starting: array(number),
2230
+ correct: array(number),
2231
+ picUrl: optional(nullable(string)),
2232
+ picSize: optional(nullable(number)),
2233
+ picBoxHeight: optional(nullable(number)),
2234
+ // NOTE(benchristel): I copied the default plotDimensions from
2235
+ // plotter.tsx. See the parse-perseus-json/README.md for an explanation
2236
+ // of why we want to duplicate the defaults here.
2237
+ plotDimensions: defaulted(array(number), () => [380, 300])
2238
+ }));
2239
+
2240
+ const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
2241
+ programID: string,
2242
+ height: number
2243
+ }));
2244
+
2245
+ const parseRadioWidget = parseWidget(constant("radio"), object({
2246
+ choices: array(object({
2247
+ content: defaulted(string, () => ""),
2248
+ clue: optional(string),
2249
+ correct: optional(boolean),
2250
+ isNoneOfTheAbove: optional(boolean),
2251
+ // deprecated
2252
+ // There is an import cycle between radio-widget.ts and
2253
+ // widgets-map.ts. The anonymous function below ensures that we
2254
+ // don't refer to parseWidgetsMap before it's defined.
2255
+ widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => undefined)
2256
+ })),
2257
+ hasNoneOfTheAbove: optional(boolean),
2258
+ countChoices: optional(boolean),
2259
+ randomize: optional(boolean),
2260
+ multipleSelect: optional(boolean),
2261
+ deselectEnabled: optional(boolean),
2262
+ // deprecated
2263
+ onePerLine: optional(boolean),
2264
+ // deprecated
2265
+ displayCount: optional(any),
2266
+ // v0 props
2267
+ // `noneOfTheAbove` is still in use (but only set to `false`).
2268
+ noneOfTheAbove: optional(constant(false))
2269
+ }));
2270
+
2271
+ const parseSorterWidget = parseWidget(constant("sorter"), object({
2272
+ correct: array(string),
2273
+ padding: boolean,
2274
+ layout: enumeration("horizontal", "vertical")
2275
+ }));
2276
+
2277
+ const parseTableWidget = parseWidget(constant("table"), object({
2278
+ headers: array(string),
2279
+ rows: number,
2280
+ columns: number,
2281
+ answers: array(array(string))
2282
+ }));
2283
+
2284
+ const parseVideoWidget = parseWidget(constant("video"), object({
2285
+ location: string,
2286
+ static: optional(boolean)
2287
+ }));
2288
+
2289
+ const parseWidgetsMap = (rawValue, ctx) => {
2290
+ if (!isObject(rawValue)) {
2291
+ return ctx.failure("PerseusWidgetsMap", rawValue);
2292
+ }
2293
+ const widgetsMap = {};
2294
+ for (const key of Object.keys(rawValue)) {
2295
+ // parseWidgetsMapEntry modifies the widgetsMap. This is kind of gross,
2296
+ // but it's the only way I could find to make TypeScript check the key
2297
+ // against the widget type.
2298
+ const entryResult = parseWidgetsMapEntry([key, rawValue[key]], widgetsMap, ctx.forSubtree(key));
2299
+ if (isFailure(entryResult)) {
2300
+ return entryResult;
2301
+ }
2302
+ }
2303
+ return ctx.success(widgetsMap);
2304
+ };
2305
+ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
2306
+ let [id, widget] = _ref;
2307
+ const idComponentsResult = parseWidgetIdComponents(id.split(" "), ctx.forSubtree("(widget ID)"));
2308
+ if (isFailure(idComponentsResult)) {
2309
+ return idComponentsResult;
2310
+ }
2311
+ const [type, n] = idComponentsResult.value;
2312
+ function parseAndAssign(key, parse) {
2313
+ const widgetResult = parse(widget, ctx);
2314
+ if (isFailure(widgetResult)) {
2315
+ return widgetResult;
2316
+ }
2317
+ widgetMap[key] = widgetResult.value;
2318
+ return ctx.success(undefined);
2319
+ }
2320
+ switch (type) {
2321
+ case "categorizer":
2322
+ return parseAndAssign(`categorizer ${n}`, parseCategorizerWidget);
2323
+ case "cs-program":
2324
+ return parseAndAssign(`cs-program ${n}`, parseCSProgramWidget);
2325
+ case "definition":
2326
+ return parseAndAssign(`definition ${n}`, parseDefinitionWidget);
2327
+ case "dropdown":
2328
+ return parseAndAssign(`dropdown ${n}`, parseDropdownWidget);
2329
+ case "explanation":
2330
+ return parseAndAssign(`explanation ${n}`, parseExplanationWidget);
2331
+ case "expression":
2332
+ return parseAndAssign(`expression ${n}`, parseExpressionWidget);
2333
+ case "grapher":
2334
+ return parseAndAssign(`grapher ${n}`, parseGrapherWidget);
2335
+ case "group":
2336
+ return parseAndAssign(`group ${n}`, parseGroupWidget);
2337
+ case "graded-group":
2338
+ return parseAndAssign(`graded-group ${n}`, parseGradedGroupWidget);
2339
+ case "graded-group-set":
2340
+ return parseAndAssign(`graded-group-set ${n}`, parseGradedGroupSetWidget);
2341
+ case "iframe":
2342
+ return parseAndAssign(`iframe ${n}`, parseIframeWidget);
2343
+ case "image":
2344
+ return parseAndAssign(`image ${n}`, parseImageWidget);
2345
+ case "input-number":
2346
+ return parseAndAssign(`input-number ${n}`, parseInputNumberWidget);
2347
+ case "interaction":
2348
+ return parseAndAssign(`interaction ${n}`, parseInteractionWidget);
2349
+ case "interactive-graph":
2350
+ return parseAndAssign(`interactive-graph ${n}`, parseInteractiveGraphWidget);
2351
+ case "label-image":
2352
+ return parseAndAssign(`label-image ${n}`, parseLabelImageWidget);
2353
+ case "matcher":
2354
+ return parseAndAssign(`matcher ${n}`, parseMatcherWidget);
2355
+ case "matrix":
2356
+ return parseAndAssign(`matrix ${n}`, parseMatrixWidget);
2357
+ case "measurer":
2358
+ return parseAndAssign(`measurer ${n}`, parseMeasurerWidget);
2359
+ case "molecule-renderer":
2360
+ return parseAndAssign(`molecule-renderer ${n}`, parseMoleculeRendererWidget);
2361
+ case "number-line":
2362
+ return parseAndAssign(`number-line ${n}`, parseNumberLineWidget);
2363
+ case "numeric-input":
2364
+ return parseAndAssign(`numeric-input ${n}`, parseNumericInputWidget);
2365
+ case "orderer":
2366
+ return parseAndAssign(`orderer ${n}`, parseOrdererWidget);
2367
+ case "passage":
2368
+ return parseAndAssign(`passage ${n}`, parsePassageWidget);
2369
+ case "passage-ref":
2370
+ return parseAndAssign(`passage-ref ${n}`, parsePassageRefWidget);
2371
+ case "passage-ref-target":
2372
+ // NOTE(benchristel): as of 2024-11-12, passage-ref-target is only
2373
+ // used in test content. See:
2374
+ // https://www.khanacademy.org/devadmin/content/search?query=widget:passage-ref-target
2375
+ return parseAndAssign(`passage-ref-target ${n}`, any);
2376
+ case "phet-simulation":
2377
+ return parseAndAssign(`phet-simulation ${n}`, parsePhetSimulationWidget);
2378
+ case "plotter":
2379
+ return parseAndAssign(`plotter ${n}`, parsePlotterWidget);
2380
+ case "python-program":
2381
+ return parseAndAssign(`python-program ${n}`, parsePythonProgramWidget);
2382
+ case "radio":
2383
+ return parseAndAssign(`radio ${n}`, parseRadioWidget);
2384
+ case "sorter":
2385
+ return parseAndAssign(`sorter ${n}`, parseSorterWidget);
2386
+ case "table":
2387
+ return parseAndAssign(`table ${n}`, parseTableWidget);
2388
+ case "video":
2389
+ return parseAndAssign(`video ${n}`, parseVideoWidget);
2390
+ case "sequence":
2391
+ // sequence is a deprecated widget type, and the corresponding
2392
+ // widget component no longer exists.
2393
+ return parseAndAssign(`sequence ${n}`, parseDeprecatedWidget);
2394
+ case "lights-puzzle":
2395
+ return parseAndAssign(`lights-puzzle ${n}`, parseDeprecatedWidget);
2396
+ case "simulator":
2397
+ return parseAndAssign(`simulator ${n}`, parseDeprecatedWidget);
2398
+ case "transformer":
2399
+ return parseAndAssign(`transformer ${n}`, parseDeprecatedWidget);
2400
+ default:
2401
+ return parseAndAssign(`${type} ${n}`, parseWidget(constant(type), any));
2402
+ }
2403
+ };
2404
+ const parseDeprecatedWidget = parseWidget(
2405
+ // Ignore the incoming widget type and hardcode "deprecated-standin"
2406
+ (_, ctx) => ctx.success("deprecated-standin"),
2407
+ // Allow any widget options
2408
+ object({}));
2409
+ const parseStringToNonNegativeInt = (rawValue, ctx) => {
2410
+ // The article renderer seems to allow the numeric part of a widget ID to
2411
+ // be 0, at least for image widgets. However, if widget IDs in an exercise
2412
+ // contain 0, the exercise renderer will blow up. We allow 0 here for
2413
+ // compatibility with articles.
2414
+ if (typeof rawValue !== "string" || !/^(0|[1-9][0-9]*)$/.test(rawValue)) {
2415
+ return ctx.failure("a string representing a non-negative integer", rawValue);
2416
+ }
2417
+ return ctx.success(+rawValue);
2418
+ };
2419
+ const parseWidgetIdComponents = pair(string, parseStringToNonNegativeInt);
2420
+
2421
+ const parsePerseusRenderer = defaulted(object({
2422
+ // TODO(benchristel): content is also defaulted to empty string in
2423
+ // renderer.tsx. See if we can remove one default or the other.
2424
+ content: defaulted(string, () => ""),
2425
+ // This module has an import cycle with parseWidgetsMap, because the
2426
+ // `group` widget can contain another renderer.
2427
+ // The anonymous function below ensures that we don't try to access
2428
+ // parseWidgetsMap before it's defined.
2429
+ widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
2430
+ images: parseImages,
2431
+ // deprecated
2432
+ metadata: any
2433
+ }),
2434
+ // Default value
2435
+ () => ({
2436
+ content: "",
2437
+ widgets: {},
2438
+ images: {}
2439
+ }));
2440
+
2441
+ const parsePerseusArticle = union(parsePerseusRenderer).or(array(parsePerseusRenderer)).parser;
2442
+
2443
+ const parseHint = object({
2444
+ replace: optional(boolean),
2445
+ content: string,
2446
+ widgets: defaulted(parseWidgetsMap, () => ({})),
2447
+ images: parseImages,
2448
+ // deprecated
2449
+ metadata: any
2450
+ });
2451
+
2452
+ const parsePerseusAnswerArea = pipeParsers(defaulted(object({}), () => ({}))).then(convert(toAnswerArea)).parser;
2453
+
2454
+ // Some answerAreas have extra, bogus fields, like:
2455
+ //
2456
+ // "answerArea": {
2457
+ // "type": "multiple",
2458
+ // "options": {},
2459
+ // "version": null,
2460
+ // "static": false,
2461
+ // "graded": false,
2462
+ // "alignment": "",
2463
+ // }
2464
+ //
2465
+ // This function filters the fields of an answerArea object, keeping only the
2466
+ // known ones, and converts `undefined` and `null` values to `false`.
2467
+ function toAnswerArea(raw) {
2468
+ return {
2469
+ zTable: !!raw.zTable,
2470
+ calculator: !!raw.calculator,
2471
+ chi2Table: !!raw.chi2Table,
2472
+ financialCalculatorMonthlyPayment: !!raw.financialCalculatorMonthlyPayment,
2473
+ financialCalculatorTotalAmount: !!raw.financialCalculatorTotalAmount,
2474
+ financialCalculatorTimeToPayOff: !!raw.financialCalculatorTimeToPayOff,
2475
+ periodicTable: !!raw.periodicTable,
2476
+ periodicTableWithKey: !!raw.periodicTableWithKey,
2477
+ tTable: !!raw.tTable
2478
+ };
2479
+ }
2480
+
2481
+ const parsePerseusItem$1 = object({
2482
+ question: parsePerseusRenderer,
2483
+ hints: defaulted(array(parseHint), () => []),
2484
+ answerArea: parsePerseusAnswerArea,
2485
+ itemDataVersion: optional(object({
2486
+ major: number,
2487
+ minor: number
2488
+ })),
2489
+ // Deprecated field
2490
+ answer: any
2491
+ });
2492
+
2493
+ /**
2494
+ * Helper to parse PerseusItem JSON
2495
+ * Why not just use JSON.parse? We want:
2496
+ * - To make sure types are correct
2497
+ * - To give us a central place to validate/transform output if needed
2498
+ * @deprecated - use parseAndMigratePerseusItem instead
2499
+ * @param {string} json - the stringified PerseusItem JSON
2500
+ * @returns {PerseusItem} the parsed PerseusItem object
2501
+ */
2502
+ function parsePerseusItem(json) {
2503
+ // Try to block a cheating vector which relies on monkey-patching
2504
+ // JSON.parse
2505
+ if (isRealJSONParse(JSON.parse)) {
2506
+ return JSON.parse(json);
2507
+ }
2508
+ throw new Error("Something went wrong.");
2509
+ }
2510
+ /**
2511
+ * Parses a PerseusItem from a JSON string, migrates old formats to the latest
2512
+ * schema, and runtime-typechecks the result. Use this to parse assessmentItem
2513
+ * data.
2514
+ *
2515
+ * @returns a {@link Result} of the parsed PerseusItem. If the result is a
2516
+ * failure, it will contain an error message describing where in the tree
2517
+ * parsing failed.
2518
+ * @throws SyntaxError if the argument is not well-formed JSON.
2519
+ */
2520
+ function parseAndMigratePerseusItem(json) {
2521
+ throwErrorIfCheatingDetected();
2522
+ const object = JSON.parse(json);
2523
+ const result = parse(object, parsePerseusItem$1);
2524
+ if (isFailure(result)) {
2525
+ return failure({
2526
+ message: result.detail,
2527
+ invalidObject: object
2528
+ });
2529
+ }
2530
+ return result;
2531
+ }
2532
+
2533
+ /**
2534
+ * Parses a PerseusArticle from a JSON string, migrates old formats to the
2535
+ * latest schema, and runtime-typechecks the result.
2536
+ *
2537
+ * @returns a {@link Result} of the parsed PerseusArticle. If the result is a
2538
+ * failure, it will contain an error message describing where in the tree
2539
+ * parsing failed.
2540
+ * @throws SyntaxError if the argument is not well-formed JSON.
2541
+ */
2542
+ function parseAndMigratePerseusArticle(json) {
2543
+ throwErrorIfCheatingDetected();
2544
+ const object = JSON.parse(json);
2545
+ const result = parse(object, parsePerseusArticle);
2546
+ if (isFailure(result)) {
2547
+ return failure({
2548
+ message: result.detail,
2549
+ invalidObject: object
2550
+ });
2551
+ }
2552
+ return result;
2553
+ }
2554
+
2555
+ /**
2556
+ * Tries to block a cheating vector that relies on monkey-patching JSON.parse.
2557
+ */
2558
+ // TODO(LEMS-2331): delete this function once server-side scoring is done.
2559
+ function throwErrorIfCheatingDetected() {
2560
+ if (!isRealJSONParse(JSON.parse)) {
2561
+ throw new Error("Something went wrong.");
2562
+ }
2563
+ }
2564
+
2565
+ /**
2566
+ * Adds the given perseus library version information to the __perseus_debug__
2567
+ * object and ensures that the object is attached to `globalThis` (`window` in
2568
+ * browser environments).
2569
+ *
2570
+ * This allows each library to provide runtime version information to assist in
2571
+ * debugging in production environments.
2572
+ */
2573
+ const addLibraryVersionToPerseusDebug = (libraryName, libraryVersion) => {
2574
+ // If the library version is the default value, then we don't want to
2575
+ // prefix it with a "v" to indicate that it is a version number.
2576
+ let prefix = "v";
2577
+ if (libraryVersion === "__lib_version__") {
2578
+ prefix = "";
2579
+ }
2580
+ const formattedVersion = `${prefix}${libraryVersion}`;
2581
+ if (typeof globalThis !== "undefined") {
2582
+ globalThis.__perseus_debug__ = globalThis.__perseus_debug__ ?? {};
2583
+ const existingVersionEntry = globalThis.__perseus_debug__[libraryName];
2584
+ if (existingVersionEntry) {
2585
+ // If we already have an entry and it doesn't match the registered
2586
+ // version, we morph the entry into an array and log a warning.
2587
+ if (existingVersionEntry !== formattedVersion) {
2588
+ // Existing entry might be an array already (oops, at least 2
2589
+ // versions of the library already loaded!).
2590
+ const allVersions = Array.isArray(existingVersionEntry) ? existingVersionEntry : [existingVersionEntry];
2591
+ allVersions.push(formattedVersion);
2592
+ globalThis.__perseus_debug__[libraryName] = allVersions;
2593
+
2594
+ // eslint-disable-next-line no-console
2595
+ console.warn(`Multiple versions of ${libraryName} loaded on this page: ${allVersions.sort().join(", ")}`);
2596
+ }
2597
+ } else {
2598
+ globalThis.__perseus_debug__[libraryName] = formattedVersion;
2599
+ }
2600
+ } else {
2601
+ // eslint-disable-next-line no-console
2602
+ console.warn(`globalThis not found found (${formattedVersion})`);
2603
+ }
2604
+ };
2605
+
2606
+ // This file is processed by a Rollup plugin (replace) to inject the production
2607
+ const libName = "@khanacademy/perseus-core";
2608
+ const libVersion = "3.7.0";
2609
+ addLibraryVersionToPerseusDebug(libName, libVersion);
2610
+
2611
+ /**
2612
+ * @typedef {Object} Errors utility for referencing the Perseus error taxonomy.
2613
+ */
2614
+ const Errors = Object.freeze({
2615
+ /**
2616
+ * @property {ErrorKind} Unknown The kind of error is not known.
2617
+ */
2618
+ Unknown: "Unknown",
2619
+ /**
2620
+ * @property {ErrorKind} Internal The error is internal to the executing code.
2621
+ */
2622
+ Internal: "Internal",
2623
+ /**
2624
+ * @property {ErrorKind} InvalidInput There was a problem with the provided
2625
+ * input, such as the wrong format or a null value.
2626
+ */
2627
+ InvalidInput: "InvalidInput",
2628
+ /**
2629
+ * @property {ErrorKind} NotAllowed There was a problem due to the state of
2630
+ * the system not matching the requested operation or input. For example,
2631
+ * trying to create a username that is valid, but is already taken by
2632
+ * another user. Use {@link InvalidInput} instead when the input isn't
2633
+ * valid regardless of the state of the system. Use {@link NotFound} when
2634
+ * the failure is due to not being able to find a resource.
2635
+ */
2636
+ NotAllowed: "NotAllowed",
2637
+ /**
2638
+ * @property {ErrorKind} TransientService There was a problem when making a
2639
+ * request to a service.
2640
+ */
2641
+ TransientService: "TransientService",
2642
+ /**
2643
+ * @property {ErrorKind} Service There was a non-transient problem when
2644
+ * making a request to service.
2645
+ */
2646
+ Service: "Service"
2647
+ });
2648
+
2649
+ /**
2650
+ * @type {ErrorKind} The kind of error being reported
2651
+ */
2652
+
2653
+ class PerseusError extends Error {
2654
+ kind;
2655
+ metadata;
2656
+ constructor(message, kind, options) {
2657
+ super(message);
2658
+ this.kind = kind;
2659
+ this.metadata = options?.metadata;
2660
+ }
2661
+ }
2662
+
2663
+ /**
2664
+ * _ utilities for objects
2665
+ */
2666
+
2667
+ /**
2668
+ * Does a pluck on keys inside objects in an object
2669
+ *
2670
+ * Ex:
2671
+ * tools = {
2672
+ * translation: {
2673
+ * enabled: true
2674
+ * },
2675
+ * rotation: {
2676
+ * enabled: false
2677
+ * }
2678
+ * };
2679
+ * pluckObject(tools, "enabled") returns {
2680
+ * translation: true
2681
+ * rotation: false
2682
+ * }
2683
+ */
2684
+ const pluck = function (table, subKey) {
2685
+ return ___default["default"].object(___default["default"].map(table, function (value, key) {
2686
+ return [key, value[subKey]];
2687
+ }));
2688
+ };
2689
+
2690
+ /**
2691
+ * Maps an object to an object
2692
+ *
2693
+ * > mapObject({a: '1', b: '2'}, (value, key) => {
2694
+ * return value + 1;
2695
+ * });
2696
+ * {a: 2, b: 3}
2697
+ */
2698
+ const mapObject = function (obj, lambda) {
2699
+ const result = {};
2700
+ Object.keys(obj).forEach(key => {
2701
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
2702
+ result[key] = lambda(obj[key], key);
2703
+ });
2704
+ return result;
2705
+ };
2706
+
2707
+ /**
2708
+ * For details on the individual options, see the
2709
+ * PerseusCategorizerWidgetOptions type
2710
+ */
2711
+
2712
+ /**
2713
+ * Given a PerseusCategorizerWidgetOptions object, return a new object with only
2714
+ * the public options that should be exposed to the client.
2715
+ */
2716
+ function getCategorizerPublicWidgetOptions(options) {
2717
+ return {
2718
+ items: options.items,
2719
+ categories: options.categories,
2720
+ randomizeItems: options.randomizeItems,
2721
+ static: options.static
2722
+ };
2723
+ }
2724
+
2725
+ const defaultWidgetOptions$v = {
2726
+ items: [],
2727
+ categories: [],
2728
+ values: [],
2729
+ randomizeItems: false
2730
+ };
2731
+ const categorizerWidgetLogic = {
2732
+ name: "categorizer",
2733
+ defaultWidgetOptions: defaultWidgetOptions$v,
2734
+ getPublicWidgetOptions: getCategorizerPublicWidgetOptions
2735
+ };
2736
+
2737
+ function getCSProgramPublicWidgetOptions(options) {
2738
+ return options;
2739
+ }
2740
+
2741
+ const DEFAULT_HEIGHT = 400;
2742
+ const defaultWidgetOptions$u = {
2743
+ programID: "",
2744
+ programType: null,
2745
+ settings: [{
2746
+ name: "",
2747
+ value: ""
2748
+ }],
2749
+ showEditor: false,
2750
+ showButtons: false,
2751
+ height: DEFAULT_HEIGHT
2752
+ };
2753
+ const csProgramWidgetLogic = {
2754
+ name: "cs-program",
2755
+ defaultWidgetOptions: defaultWidgetOptions$u,
2756
+ supportedAlignments: ["block", "full-width"],
2757
+ getPublicWidgetOptions: getCSProgramPublicWidgetOptions
2758
+ };
2759
+
2760
+ const defaultWidgetOptions$t = {
2761
+ togglePrompt: "",
2762
+ definition: ""
2763
+ };
2764
+ const definitionWidgetLogic = {
2765
+ name: "definition",
2766
+ defaultWidgetOptions: defaultWidgetOptions$t,
2767
+ defaultAlignment: "inline"
2768
+ };
2769
+
2770
+ /**
2771
+ * For details on the individual options, see the
2772
+ * PerseusDropdownWidgetOptions type
2773
+ */
2774
+
2775
+ /**
2776
+ * Given a PerseusDropdownWidgetOptions object, return a new object with only
2777
+ * the public options that should be exposed to the client.
2778
+ */
2779
+ function getDropdownPublicWidgetOptions(options) {
2780
+ return {
2781
+ choices: options.choices.map(choice => ({
2782
+ content: choice.content
2783
+ })),
2784
+ placeholder: options.placeholder,
2785
+ static: options.static,
2786
+ visibleLabel: options.visibleLabel,
2787
+ ariaLabel: options.ariaLabel
2788
+ };
2789
+ }
2790
+
2791
+ const defaultWidgetOptions$s = {
2792
+ placeholder: "",
2793
+ choices: [{
2794
+ content: "",
2795
+ correct: false
2796
+ }]
2797
+ };
2798
+ const dropdownWidgetLogic = {
2799
+ name: "definition",
2800
+ defaultWidgetOptions: defaultWidgetOptions$s,
2801
+ defaultAlignment: "inline-block",
2802
+ getPublicWidgetOptions: getDropdownPublicWidgetOptions
2803
+ };
2804
+
2805
+ const defaultWidgetOptions$r = {
2806
+ showPrompt: "Explain",
2807
+ hidePrompt: "Hide explanation",
2808
+ explanation: "explanation goes here\n\nmore explanation",
2809
+ widgets: {}
2810
+ };
2811
+ const explanationWidgetLogic = {
2812
+ name: "explanation",
2813
+ defaultWidgetOptions: defaultWidgetOptions$r,
2814
+ defaultAlignment: "inline"
2815
+ };
2816
+
2817
+ const currentVersion$3 = {
2818
+ major: 1,
2819
+ minor: 0
2820
+ };
2821
+ const widgetOptionsUpgrades$2 = {
2822
+ "1": v0options => ({
2823
+ times: v0options.times,
2824
+ buttonSets: v0options.buttonSets,
2825
+ functions: v0options.functions,
2826
+ buttonsVisible: v0options.buttonsVisible,
2827
+ visibleLabel: v0options.visibleLabel,
2828
+ ariaLabel: v0options.ariaLabel,
2829
+ answerForms: [{
2830
+ considered: "correct",
2831
+ form: v0options.form,
2832
+ simplify: v0options.simplify,
2833
+ value: v0options.value
2834
+ }]
2835
+ })
2836
+ };
2837
+ const defaultWidgetOptions$q = {
2838
+ answerForms: [],
2839
+ times: false,
2840
+ buttonSets: ["basic"],
2841
+ functions: ["f", "g", "h"]
2842
+ };
2843
+
2844
+ /**
2845
+ * For details on the individual options, see the
2846
+ * PerseusExpressionWidgetOptions type
2847
+ */
2848
+
2849
+ /**
2850
+ * Given a PerseusExpressionWidgetOptions object, return a new object with only
2851
+ * the public options that should be exposed to the client.
2852
+ */
2853
+ function getExpressionPublicWidgetOptions(options) {
2854
+ return {
2855
+ buttonSets: options.buttonSets,
2856
+ functions: options.functions,
2857
+ times: options.times,
2858
+ visibleLabel: options.visibleLabel,
2859
+ ariaLabel: options.ariaLabel,
2860
+ buttonsVisible: options.buttonsVisible
2861
+ };
2862
+ }
2863
+
2864
+ const expressionWidgetLogic = {
2865
+ name: "expression",
2866
+ version: currentVersion$3,
2867
+ widgetOptionsUpgrades: widgetOptionsUpgrades$2,
2868
+ defaultWidgetOptions: defaultWidgetOptions$q,
2869
+ defaultAlignment: "inline-block",
2870
+ getPublicWidgetOptions: getExpressionPublicWidgetOptions
2871
+ };
2872
+
2873
+ const defaultWidgetOptions$p = {
2874
+ title: "",
2875
+ content: "",
2876
+ widgets: {},
2877
+ images: {},
2878
+ hint: null
2879
+ };
2880
+ const gradedGroupWidgetLogic = {
2881
+ name: "graded-group",
2882
+ defaultWidgetOptions: defaultWidgetOptions$p
2883
+ };
2884
+
2885
+ const defaultWidgetOptions$o = {
2886
+ gradedGroups: []
2887
+ };
2888
+ const gradedGroupSetWidgetLogic = {
2889
+ name: "graded-group-set",
2890
+ defaultWidgetOptions: defaultWidgetOptions$o
2891
+ };
2892
+
2893
+ function getGrapherPublicWidgetOptions(options) {
2894
+ const {
2895
+ correct: _,
2896
+ ...publicOptions
2897
+ } = options;
2898
+ return publicOptions;
2899
+ }
2900
+
2901
+ const defaultWidgetOptions$n = {
2902
+ graph: {
2903
+ labels: ["x", "y"],
2904
+ range: [[-10, 10], [-10, 10]],
2905
+ step: [1, 1],
2906
+ backgroundImage: {
2907
+ url: null
2908
+ },
2909
+ markings: "graph",
2910
+ rulerLabel: "",
2911
+ rulerTicks: 10,
2912
+ valid: true,
2913
+ showTooltips: false
2914
+ },
2915
+ correct: {
2916
+ type: "linear",
2917
+ coords: null
2918
+ },
2919
+ availableTypes: ["linear"]
2920
+ };
2921
+ const grapherWidgetLogic = {
2922
+ name: "grapher",
2923
+ defaultWidgetOptions: defaultWidgetOptions$n,
2924
+ getPublicWidgetOptions: getGrapherPublicWidgetOptions
2925
+ };
2926
+
2927
+ const defaultWidgetOptions$m = {
2928
+ content: "",
2929
+ widgets: {},
2930
+ images: {}
2931
+ };
2932
+ const groupWidgetLogic = {
2933
+ name: "group",
2934
+ defaultWidgetOptions: defaultWidgetOptions$m
2935
+ };
2936
+
2937
+ function getIFramePublicWidgetOptions(options) {
2938
+ return options;
2939
+ }
2940
+
2941
+ const defaultWidgetOptions$l = {
2942
+ url: "",
2943
+ settings: [{
2944
+ name: "",
2945
+ value: ""
2946
+ }],
2947
+ width: "400",
2948
+ height: "400",
2949
+ allowFullScreen: false,
2950
+ allowTopNavigation: false
2951
+ };
2952
+ const iframeWidgetLogic = {
2953
+ name: "iframe",
2954
+ defaultWidgetOptions: defaultWidgetOptions$l,
2955
+ getPublicWidgetOptions: getIFramePublicWidgetOptions
2956
+ };
2957
+
2958
+ const defaultWidgetOptions$k = {
2959
+ title: "",
2960
+ range: [[0, 10], [0, 10]],
2961
+ box: [400, 400],
2962
+ backgroundImage: {
2963
+ url: null,
2964
+ width: 0,
2965
+ height: 0
2966
+ },
2967
+ labels: [],
2968
+ alt: "",
2969
+ caption: ""
2970
+ };
2971
+ const imageWidgetLogic = {
2972
+ name: "image",
2973
+ defaultWidgetOptions: defaultWidgetOptions$k,
2974
+ supportedAlignments: ["block", "full-width"],
2975
+ defaultAlignment: "block"
2976
+ };
2977
+
2978
+ const defaultWidgetOptions$j = {
2979
+ value: 0,
2980
+ simplify: "required",
2981
+ size: "normal",
2982
+ inexact: false,
2983
+ maxError: 0.1,
2984
+ answerType: "number",
2985
+ rightAlign: false
2986
+ };
2987
+ const inputNumberWidgetLogic = {
2988
+ name: "input-number",
2989
+ defaultWidgetOptions: defaultWidgetOptions$j,
2990
+ defaultAlignment: "inline-block"
2991
+ };
2992
+
2993
+ const defaultWidgetOptions$i = {
2994
+ graph: {
2995
+ box: [400, 400],
2996
+ labels: ["x", "y"],
2997
+ range: [[-10, 10], [-10, 10]],
2998
+ tickStep: [1, 1],
2999
+ gridStep: [1, 1],
3000
+ markings: "graph"
3001
+ },
3002
+ elements: []
3003
+ };
3004
+ const interactionWidgetLogic = {
3005
+ name: "interaction",
3006
+ defaultWidgetOptions: defaultWidgetOptions$i
3007
+ };
3008
+
3009
+ function getInteractiveGraphPublicWidgetOptions(options) {
3010
+ const {
3011
+ correct: _,
3012
+ ...publicOptions
3013
+ } = options;
3014
+ return publicOptions;
3015
+ }
3016
+
3017
+ const defaultWidgetOptions$h = {
3018
+ labels: ["x", "y"],
3019
+ range: [[-10, 10], [-10, 10]],
3020
+ step: [1, 1],
3021
+ backgroundImage: {
3022
+ url: null
3023
+ },
3024
+ markings: "graph",
3025
+ showTooltips: false,
3026
+ showProtractor: false,
3027
+ graph: {
3028
+ type: "linear"
3029
+ },
3030
+ correct: {
3031
+ type: "linear",
3032
+ coords: null
3033
+ }
3034
+ };
3035
+ const interactiveGraphWidgetLogic = {
3036
+ name: "interactive-graph",
3037
+ defaultWidgetOptions: defaultWidgetOptions$h,
3038
+ getPublicWidgetOptions: getInteractiveGraphPublicWidgetOptions
3039
+ };
3040
+
3041
+ /**
3042
+ * For details on the individual options, see the
3043
+ * PerseusLabelImageWidgetOptions type
3044
+ */
3045
+
3046
+ function getLabelImagePublicWidgetOptions(options) {
3047
+ return {
3048
+ ...options,
3049
+ markers: options.markers.map(getLabelImageMarkerPublicData)
3050
+ };
3051
+ }
3052
+ function getLabelImageMarkerPublicData(marker) {
3053
+ const {
3054
+ answers: _,
3055
+ ...publicData
3056
+ } = marker;
3057
+ return publicData;
3058
+ }
3059
+
3060
+ const defaultWidgetOptions$g = {
3061
+ choices: [],
3062
+ imageAlt: "",
3063
+ imageUrl: "",
3064
+ imageWidth: 0,
3065
+ imageHeight: 0,
3066
+ markers: [],
3067
+ multipleAnswers: false,
3068
+ hideChoicesFromInstructions: false
3069
+ };
3070
+ const labelImageWidgetLogic = {
3071
+ name: "label-image",
3072
+ defaultWidgetOptions: defaultWidgetOptions$g,
3073
+ getPublicWidgetOptions: getLabelImagePublicWidgetOptions
3074
+ };
3075
+
3076
+ // TODO(LEMS-2841): Should be able to remove once getPublicWidgetOptions is hooked up
3077
+
3078
+ // TODO(LEMS-2841): Should be able to remove once getPublicWidgetOptions is hooked up
3079
+ const shuffleMatcher = props => {
3080
+ // Use the same random() function to shuffle both columns sequentially
3081
+ const rng = perseusCore.seededRNG(props.problemNum);
3082
+ let left;
3083
+ if (!props.orderMatters) {
3084
+ // If the order doesn't matter, don't shuffle the left column
3085
+ left = props.left;
3086
+ } else {
3087
+ left = perseusCore.shuffle(props.left, rng, /* ensurePermuted */true);
3088
+ }
3089
+ const right = perseusCore.shuffle(props.right, rng, /* ensurePermuted */true);
3090
+ return {
3091
+ left,
3092
+ right
3093
+ };
3094
+ };
3095
+
3096
+ // TODO(LEMS-2841): Can shorten to shuffleMatcher after above function removed
3097
+ function shuffleMatcherWithRandom(data) {
3098
+ // Use the same random() function to shuffle both columns sequentially
3099
+ let left;
3100
+ if (!data.orderMatters) {
3101
+ // If the order doesn't matter, don't shuffle the left column
3102
+ left = data.left;
3103
+ } else {
3104
+ left = perseusCore.shuffle(data.left, Math.random, /* ensurePermuted */true);
3105
+ }
3106
+ const right = perseusCore.shuffle(data.right, Math.random, /* ensurePermuted */true);
3107
+ return {
3108
+ left,
3109
+ right
3110
+ };
3111
+ }
3112
+
3113
+ /**
3114
+ * For details on the individual options, see the
3115
+ * PerseusMatcherWidgetOptions type
3116
+ */
3117
+
3118
+ /**
3119
+ * Given a PerseusMatcherWidgetOptions object, return a new object with only
3120
+ * the public options that should be exposed to the client.
3121
+ */
3122
+ function getMatcherPublicWidgetOptions(options) {
3123
+ const {
3124
+ left,
3125
+ right
3126
+ } = shuffleMatcherWithRandom(options);
3127
+ return {
3128
+ ...options,
3129
+ left: left,
3130
+ right: right
3131
+ };
3132
+ }
3133
+
3134
+ const defaultWidgetOptions$f = {
3135
+ left: ["$x$", "$y$", "$z$"],
3136
+ right: ["$1$", "$2$", "$3$"],
3137
+ labels: ["test", "label"],
3138
+ orderMatters: false,
3139
+ padding: true
3140
+ };
3141
+ const matcherWidgetLogic = {
3142
+ name: "matcher",
3143
+ defaultWidgetOptions: defaultWidgetOptions$f,
3144
+ getPublicWidgetOptions: getMatcherPublicWidgetOptions
3145
+ };
3146
+
3147
+ function getMatrixPublicWidgetOptions(options) {
3148
+ const {
3149
+ answers: _,
3150
+ ...publicOptions
3151
+ } = options;
3152
+ return publicOptions;
3153
+ }
3154
+
3155
+ const defaultWidgetOptions$e = {
3156
+ matrixBoardSize: [3, 3],
3157
+ answers: [[]],
3158
+ prefix: "",
3159
+ suffix: "",
3160
+ cursorPosition: [0, 0]
3161
+ };
3162
+ const matrixWidgetLogic = {
3163
+ name: "matrix",
3164
+ defaultWidgetOptions: defaultWidgetOptions$e,
3165
+ getPublicWidgetOptions: getMatrixPublicWidgetOptions
3166
+ };
3167
+
3168
+ const currentVersion$2 = {
3169
+ major: 1,
3170
+ minor: 0
3171
+ };
3172
+ const widgetOptionsUpgrades$1 = {
3173
+ "1": v0options => {
3174
+ const {
3175
+ imageUrl,
3176
+ imageTop,
3177
+ imageLeft,
3178
+ ...rest
3179
+ } = v0options;
3180
+ return {
3181
+ ...rest,
3182
+ image: {
3183
+ url: imageUrl,
3184
+ top: imageTop,
3185
+ left: imageLeft
3186
+ }
3187
+ };
3188
+ }
3189
+ };
3190
+ const defaultWidgetOptions$d = {
3191
+ box: [480, 480],
3192
+ image: {},
3193
+ showProtractor: true,
3194
+ showRuler: false,
3195
+ rulerLabel: "",
3196
+ rulerTicks: 10,
3197
+ rulerPixels: 40,
3198
+ rulerLength: 10
3199
+ };
3200
+
3201
+ const measurerWidgetLogic = {
3202
+ name: "measurer",
3203
+ version: currentVersion$2,
3204
+ widgetOptionsUpgrades: widgetOptionsUpgrades$1,
3205
+ defaultWidgetOptions: defaultWidgetOptions$d
3206
+ };
3207
+
3208
+ function getNumberLinePublicWidgetOptions(options) {
3209
+ const {
3210
+ correctX: _,
3211
+ correctRel: __,
3212
+ ...publicOptions
3213
+ } = options;
3214
+ return publicOptions;
3215
+ }
3216
+
3217
+ const defaultWidgetOptions$c = {
3218
+ range: [0, 10],
3219
+ labelRange: [null, null],
3220
+ labelStyle: "decimal",
3221
+ labelTicks: true,
3222
+ divisionRange: [1, 12],
3223
+ numDivisions: 5,
3224
+ snapDivisions: 2,
3225
+ tickStep: null,
3226
+ correctRel: "eq",
3227
+ correctX: null,
3228
+ initialX: null,
3229
+ showTooltips: false
3230
+ };
3231
+ const numberLineWidgetLogic = {
3232
+ name: "number-line",
3233
+ defaultWidgetOptions: defaultWidgetOptions$c,
3234
+ getPublicWidgetOptions: getNumberLinePublicWidgetOptions
3235
+ };
3236
+
3237
+ /**
3238
+ * For details on the individual options, see the
3239
+ * PerseusNumericInputWidgetOptions type
3240
+ */
3241
+
3242
+ /**
3243
+ * Given a PerseusNumericInputWidgetOptions object, return a new object with only
3244
+ * the public options that should be exposed to the client.
3245
+ */
3246
+ function getNumericInputPublicWidgetOptions(options) {
3247
+ const {
3248
+ answers: _,
3249
+ ...publicWidgetOptions
3250
+ } = options;
3251
+ return publicWidgetOptions;
3252
+ }
3253
+
3254
+ const defaultWidgetOptions$b = {
3255
+ answers: [{
3256
+ value: null,
3257
+ status: "correct",
3258
+ message: "",
3259
+ simplify: "required",
3260
+ answerForms: [],
3261
+ strict: false,
3262
+ maxError: null
3263
+ }],
3264
+ size: "normal",
3265
+ coefficient: false,
3266
+ labelText: "",
3267
+ rightAlign: false
3268
+ };
3269
+ const numericInputWidgetLogic = {
3270
+ name: "numeric-input",
3271
+ defaultWidgetOptions: defaultWidgetOptions$b,
3272
+ defaultAlignment: "inline-block",
3273
+ getPublicWidgetOptions: getNumericInputPublicWidgetOptions
3274
+ };
3275
+
3276
+ /**
3277
+ * For details on the individual options, see the
3278
+ * PerseusOrdererWidgetOptions type
3279
+ */
3280
+
3281
+ /**
3282
+ * Given a PerseusOrdererWidgetOptions object, return a new object with only
3283
+ * the public options that should be exposed to the client.
3284
+ */
3285
+ function getOrdererPublicWidgetOptions(options) {
3286
+ return {
3287
+ options: options.options,
3288
+ height: options.height,
3289
+ layout: options.layout
3290
+ };
3291
+ }
3292
+
3293
+ const defaultWidgetOptions$a = {
3294
+ correctOptions: [{
3295
+ content: "$x$"
3296
+ }],
3297
+ otherOptions: [{
3298
+ content: "$y$"
3299
+ }],
3300
+ height: "normal",
3301
+ layout: "horizontal"
3302
+ };
3303
+ const ordererWidgetLogic = {
3304
+ name: "orderer",
3305
+ defaultWidgetOptions: defaultWidgetOptions$a,
3306
+ getPublicWidgetOptions: getOrdererPublicWidgetOptions
3307
+ };
3308
+
3309
+ const defaultWidgetOptions$9 = {
3310
+ passageTitle: "",
3311
+ passageText: "",
3312
+ footnotes: "",
3313
+ showLineNumbers: true
3314
+ };
3315
+ const passageWidgetLogic = {
3316
+ name: "passage",
3317
+ defaultWidgetOptions: defaultWidgetOptions$9
3318
+ };
3319
+
3320
+ const currentVersion$1 = {
3321
+ major: 0,
3322
+ minor: 1
3323
+ };
3324
+ const defaultWidgetOptions$8 = {
3325
+ passageNumber: 1,
3326
+ referenceNumber: 1,
3327
+ summaryText: ""
3328
+ };
3329
+
3330
+ const passageRefWidgetLogic = {
3331
+ name: "passageRef",
3332
+ version: currentVersion$1,
3333
+ defaultWidgetOptions: defaultWidgetOptions$8,
3334
+ defaultAlignment: "inline"
3335
+ };
3336
+
3337
+ const defaultWidgetOptions$7 = {
3338
+ content: ""
3339
+ };
3340
+ const passageRefTargetWidgetLogic = {
3341
+ name: "passageRefTarget",
3342
+ defaultWidgetOptions: defaultWidgetOptions$7,
3343
+ defaultAlignment: "inline"
3344
+ };
3345
+
3346
+ const defaultWidgetOptions$6 = {
3347
+ url: "",
3348
+ description: ""
3349
+ };
3350
+ const phetSimulationWidgetLogic = {
3351
+ name: "phet-simulation",
3352
+ defaultWidgetOptions: defaultWidgetOptions$6
3353
+ };
3354
+
3355
+ /**
3356
+ * For details on the individual options, see the
3357
+ * PerseusPlotterWidgetOptions type
3358
+ */
3359
+
3360
+ /**
3361
+ * Given a PerseusPlotterWidgetOptions object, return a new object with only
3362
+ * the public options that should be exposed to the client.
3363
+ */
3364
+ function getPlotterPublicWidgetOptions(options) {
3365
+ const {
3366
+ correct: _,
3367
+ ...publicOptions
3368
+ } = options;
3369
+ return publicOptions;
3370
+ }
3371
+
3372
+ const defaultWidgetOptions$5 = {
3373
+ scaleY: 1,
3374
+ maxY: 10,
3375
+ snapsPerLine: 2,
3376
+ correct: [1],
3377
+ starting: [1],
3378
+ type: "bar",
3379
+ labels: ["", ""],
3380
+ categories: [""],
3381
+ picSize: 30,
3382
+ picBoxHeight: 36,
3383
+ plotDimensions: [275, 200],
3384
+ labelInterval: 1,
3385
+ picUrl: null
3386
+ };
3387
+ const plotterWidgetLogic = {
3388
+ name: "plotter",
3389
+ defaultWidgetOptions: defaultWidgetOptions$5,
3390
+ getPublicWidgetOptions: getPlotterPublicWidgetOptions
3391
+ };
3392
+
3393
+ const defaultWidgetOptions$4 = {
3394
+ programID: "",
3395
+ height: 400
3396
+ };
3397
+ const pythonProgramWidgetLogic = {
3398
+ name: "python-program",
3399
+ defaultWidgetOptions: defaultWidgetOptions$4
3400
+ };
3401
+
3402
+ const currentVersion = {
3403
+ major: 1,
3404
+ minor: 0
3405
+ };
3406
+ const widgetOptionsUpgrades = {
3407
+ "1": v0props => {
3408
+ const {
3409
+ noneOfTheAbove,
3410
+ ...rest
3411
+ } = v0props;
3412
+ if (noneOfTheAbove) {
3413
+ throw new Error("radio widget v0 no longer supports auto noneOfTheAbove");
3414
+ }
3415
+ return {
3416
+ ...rest,
3417
+ hasNoneOfTheAbove: false
3418
+ };
3419
+ }
3420
+ };
3421
+ const defaultWidgetOptions$3 = {
3422
+ choices: [{}, {}, {}, {}],
3423
+ displayCount: null,
3424
+ randomize: false,
3425
+ hasNoneOfTheAbove: false,
3426
+ multipleSelect: false,
3427
+ countChoices: false,
3428
+ deselectEnabled: false
3429
+ };
3430
+
3431
+ /**
3432
+ * For details on the individual options, see the
3433
+ * PerseusRadioWidgetOptions type
3434
+ */
3435
+
3436
+ /**
3437
+ * Only the options from each Radio choice that should be exposed to the client.
3438
+ */
3439
+
3440
+ /**
3441
+ * Given a PerseusRadioChoice object, return a new object with only the public
3442
+ * data that should be included in the Radio public widget options.
3443
+ */
3444
+ function getRadioChoicePublicData(choice) {
3445
+ const {
3446
+ content,
3447
+ isNoneOfTheAbove,
3448
+ widgets
3449
+ } = choice;
3450
+ return {
3451
+ content,
3452
+ isNoneOfTheAbove,
3453
+ widgets
3454
+ };
3455
+ }
3456
+
3457
+ /**
3458
+ * Given a PerseusRadioWidgetOptions object, return a new object with only
3459
+ * the public options that should be exposed to the client.
3460
+ */
3461
+ function getRadioPublicWidgetOptions(options) {
3462
+ return {
3463
+ ...options,
3464
+ choices: options.choices.map(getRadioChoicePublicData)
3465
+ };
3466
+ }
3467
+
3468
+ const radioWidgetLogic = {
3469
+ name: "radio",
3470
+ version: currentVersion,
3471
+ widgetOptionsUpgrades: widgetOptionsUpgrades,
3472
+ defaultWidgetOptions: defaultWidgetOptions$3,
3473
+ getPublicWidgetOptions: getRadioPublicWidgetOptions
3474
+ };
3475
+
3476
+ /**
3477
+ * For details on the individual options, see the
3478
+ * PerseusSorterWidgetOptions type
3479
+ */
3480
+
3481
+ /**
3482
+ * Given a PerseusSorterWidgetOptions object, return a new object with only
3483
+ * the public options that should be exposed to the client.
3484
+ */
3485
+ function getSorterPublicWidgetOptions(options) {
3486
+ const shuffledCorrect = perseusCore.shuffle(options.correct, Math.random, /* ensurePermuted */true);
3487
+ return {
3488
+ ...options,
3489
+ // Note(Tamara): This does not provide correct answer information any longer.
3490
+ // To maintain compatibility with the original widget options, we are
3491
+ // keeping the key the same. Represents initial state of the cards here.
3492
+ correct: shuffledCorrect,
3493
+ // Note(Tamara): This new key is only added here with "true". There isn't
3494
+ // a place where it is set to false. It indicates that the correct field
3495
+ // has been shuffled and no longer contains correct answer info.
3496
+ isCorrectShuffled: true
3497
+ };
3498
+ }
3499
+
3500
+ const defaultWidgetOptions$2 = {
3501
+ correct: ["$x$", "$y$", "$z$"],
3502
+ layout: "horizontal",
3503
+ padding: true
3504
+ };
3505
+ const sorterWidgetLogic = {
3506
+ name: "sorter",
3507
+ defaultWidgetOptions: defaultWidgetOptions$2,
3508
+ getPublicWidgetOptions: getSorterPublicWidgetOptions
3509
+ };
3510
+
3511
+ function getTablePublicWidgetOptions(options) {
3512
+ const {
3513
+ answers: _,
3514
+ ...publicOptions
3515
+ } = options;
3516
+ return publicOptions;
3517
+ }
3518
+
3519
+ const defaultRows = 4;
3520
+ const defaultColumns = 1;
3521
+
3522
+ // initialize a 2D array
3523
+ // (defaultRows x defaultColumns) of empty strings
3524
+ const answers = new Array(defaultRows).fill(0).map(() => new Array(defaultColumns).fill(""));
3525
+ const defaultWidgetOptions$1 = {
3526
+ headers: [""],
3527
+ rows: defaultRows,
3528
+ columns: defaultColumns,
3529
+ answers: answers
3530
+ };
3531
+ const tableWidgetLogic = {
3532
+ name: "table",
3533
+ defaultWidgetOptions: defaultWidgetOptions$1,
3534
+ getPublicWidgetOptions: getTablePublicWidgetOptions
3535
+ };
3536
+
3537
+ const defaultWidgetOptions = {
3538
+ location: ""
3539
+ };
3540
+ const videoWidgetLogic = {
3541
+ name: "video",
3542
+ defaultWidgetOptions,
3543
+ supportedAlignments: ["block", "float-left", "float-right", "full-width"],
3544
+ defaultAlignment: "block"
3545
+ };
3546
+
3547
+ const widgets = {};
3548
+ function registerWidget(type, logic) {
3549
+ widgets[type] = logic;
3550
+ }
3551
+ function isWidgetRegistered(type) {
3552
+ const widgetLogic = widgets[type];
3553
+ return !!widgetLogic;
3554
+ }
3555
+ function getCurrentVersion(type) {
3556
+ const widgetLogic = widgets[type];
3557
+ return widgetLogic?.version || {
3558
+ major: 0,
3559
+ minor: 0
3560
+ };
3561
+ }
3562
+
3563
+ // TODO(LEMS-2870): getPublicWidgetOptionsFunction/PublicWidgetOptionsFunction
3564
+ // need better types
3565
+ const getPublicWidgetOptionsFunction = name => {
3566
+ return widgets[name]?.getPublicWidgetOptions ?? (i => i);
3567
+ };
3568
+ function getWidgetOptionsUpgrades(type) {
3569
+ const widgetLogic = widgets[type];
3570
+ return widgetLogic?.widgetOptionsUpgrades || {};
3571
+ }
3572
+ function getDefaultWidgetOptions(type) {
3573
+ const widgetLogic = widgets[type];
3574
+ return widgetLogic?.defaultWidgetOptions || {};
3575
+ }
3576
+
3577
+ /**
3578
+ * Handling for the optional alignments for widgets
3579
+ * See widget-container.jsx for details on how alignments are implemented.
3580
+ */
3581
+
3582
+ /**
3583
+ * Returns the list of supported alignments for the given (string) widget
3584
+ * type. This is used primarily at editing time to display the choices
3585
+ * for the user.
3586
+ *
3587
+ * Supported alignments are given as an array of strings in the exports of
3588
+ * a widget's module.
3589
+ */
3590
+ const getSupportedAlignments = type => {
3591
+ const widgetLogic = widgets[type];
3592
+ if (!widgetLogic?.supportedAlignments?.[0]) {
3593
+ // default alignments
3594
+ return ["default"];
3595
+ }
3596
+ return widgetLogic?.supportedAlignments;
3597
+ };
3598
+
3599
+ /**
3600
+ * For the given (string) widget type, determine the default alignment for
3601
+ * the widget. This is used at rendering time to go from "default" alignment
3602
+ * to the actual alignment displayed on the screen.
3603
+ *
3604
+ * The default alignment is given either as a string (called
3605
+ * `defaultAlignment`) or a function (called `getDefaultAlignment`) on
3606
+ * the exports of a widget's module.
3607
+ */
3608
+ const getDefaultAlignment = type => {
3609
+ const widgetLogic = widgets[type];
3610
+ if (!widgetLogic?.defaultAlignment) {
3611
+ return "block";
3612
+ }
3613
+ return widgetLogic.defaultAlignment;
3614
+ };
3615
+ registerWidget("categorizer", categorizerWidgetLogic);
3616
+ registerWidget("cs-program", csProgramWidgetLogic);
3617
+ registerWidget("definition", definitionWidgetLogic);
3618
+ registerWidget("dropdown", dropdownWidgetLogic);
3619
+ registerWidget("explanation", explanationWidgetLogic);
3620
+ registerWidget("expression", expressionWidgetLogic);
3621
+ registerWidget("graded-group", gradedGroupWidgetLogic);
3622
+ registerWidget("graded-group-set", gradedGroupSetWidgetLogic);
3623
+ registerWidget("grapher", grapherWidgetLogic);
3624
+ registerWidget("group", groupWidgetLogic);
3625
+ registerWidget("iframe", iframeWidgetLogic);
3626
+ registerWidget("image", imageWidgetLogic);
3627
+ registerWidget("input-number", inputNumberWidgetLogic);
3628
+ registerWidget("interaction", interactionWidgetLogic);
3629
+ registerWidget("interactive-graph", interactiveGraphWidgetLogic);
3630
+ registerWidget("label-image", labelImageWidgetLogic);
3631
+ registerWidget("matcher", matcherWidgetLogic);
3632
+ registerWidget("matrix", matrixWidgetLogic);
3633
+ registerWidget("measurer", measurerWidgetLogic);
3634
+ registerWidget("number-line", numberLineWidgetLogic);
3635
+ registerWidget("numeric-input", numericInputWidgetLogic);
3636
+ registerWidget("orderer", ordererWidgetLogic);
3637
+ registerWidget("passage", passageWidgetLogic);
3638
+ registerWidget("passage-ref", passageRefWidgetLogic);
3639
+ registerWidget("passage-ref-target", passageRefTargetWidgetLogic);
3640
+ registerWidget("phet-simulation", phetSimulationWidgetLogic);
3641
+ registerWidget("plotter", plotterWidgetLogic);
3642
+ registerWidget("python-program", pythonProgramWidgetLogic);
3643
+ registerWidget("radio", radioWidgetLogic);
3644
+ registerWidget("sorter", sorterWidgetLogic);
3645
+ registerWidget("table", tableWidgetLogic);
3646
+ registerWidget("video", videoWidgetLogic);
3647
+
3648
+ var coreWidgetRegistry = /*#__PURE__*/Object.freeze({
3649
+ __proto__: null,
3650
+ isWidgetRegistered: isWidgetRegistered,
3651
+ getCurrentVersion: getCurrentVersion,
3652
+ getPublicWidgetOptionsFunction: getPublicWidgetOptionsFunction,
3653
+ getWidgetOptionsUpgrades: getWidgetOptionsUpgrades,
3654
+ getDefaultWidgetOptions: getDefaultWidgetOptions,
3655
+ getSupportedAlignments: getSupportedAlignments,
3656
+ getDefaultAlignment: getDefaultAlignment
3657
+ });
3658
+
3659
+ const DEFAULT_STATIC = false;
3660
+ const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
3661
+ const type = oldWidgetInfo.type;
3662
+ // NOTE(jeremy): This looks like it could be replaced by fixing types so
3663
+ // that `type` is non-optional. But we're seeing this in Sentry today so I
3664
+ // suspect we have legacy data (potentially unpublished) and we should
3665
+ // figure that out before depending solely on types.
3666
+ if (!___default["default"].isString(type)) {
3667
+ throw new PerseusError("widget type must be a string, but was: " + type, Errors.Internal);
3668
+ }
3669
+ if (!isWidgetRegistered(type)) {
3670
+ // If we have a widget that isn't registered, we can't upgrade it
3671
+ // TODO(aria): Figure out what the best thing to do here would be
3672
+ return oldWidgetInfo;
3673
+ }
3674
+
3675
+ // Unversioned widgets (pre-July 2014) are all implicitly 0.0
3676
+ const initialVersion = oldWidgetInfo.version || {
3677
+ major: 0,
3678
+ minor: 0
3679
+ };
3680
+ const latestVersion = getCurrentVersion(type);
3681
+
3682
+ // If the widget version is later than what we understand (major
3683
+ // version is higher than latest, or major versions are equal and minor
3684
+ // version is higher than latest), don't perform any upgrades.
3685
+ if (initialVersion.major > latestVersion.major || initialVersion.major === latestVersion.major && initialVersion.minor > latestVersion.minor) {
3686
+ return oldWidgetInfo;
3687
+ }
3688
+
3689
+ // We do a clone here so that it's safe to mutate the input parameter
3690
+ // in propUpgrades functions (which I will probably accidentally do at
3691
+ // some point, and we would like to not break when that happens).
3692
+ let newEditorOptions = ___default["default"].clone(oldWidgetInfo.options) || {};
3693
+ const upgradePropsMap = getWidgetOptionsUpgrades(type);
3694
+
3695
+ // Empty props usually mean a newly created widget by the editor,
3696
+ // and are always considerered up-to-date.
3697
+ // Mostly, we'd rather not run upgrade functions on props that are
3698
+ // not complete.
3699
+ if (___default["default"].keys(newEditorOptions).length !== 0) {
3700
+ // We loop through all the versions after the current version of
3701
+ // the loaded widget, up to and including the latest version of the
3702
+ // loaded widget, and run the upgrade function to bring our loaded
3703
+ // widget's props up to that version.
3704
+ // There is a little subtlety here in that we call
3705
+ // upgradePropsMap[1] to upgrade *to* version 1,
3706
+ // (not from version 1).
3707
+ for (let nextVersion = initialVersion.major + 1; nextVersion <= latestVersion.major; nextVersion++) {
3708
+ if (upgradePropsMap[String(nextVersion)]) {
3709
+ newEditorOptions = upgradePropsMap[String(nextVersion)](newEditorOptions);
3710
+ } else {
3711
+ throw new PerseusError("No upgrade found for widget. Cannot render.", Errors.Internal, {
3712
+ metadata: {
3713
+ type,
3714
+ fromMajorVersion: nextVersion - 1,
3715
+ toMajorVersion: nextVersion,
3716
+ oldWidgetInfo: JSON.stringify(oldWidgetInfo)
3717
+ }
3718
+ });
3719
+ }
3720
+ }
3721
+ }
3722
+
3723
+ // Minor version upgrades (eg. new optional props) don't have
3724
+ // transform functions. Instead, we fill in the new props with their
3725
+ // defaults.
3726
+ const defaultOptions = getDefaultWidgetOptions(type);
3727
+ newEditorOptions = {
3728
+ ...defaultOptions,
3729
+ ...newEditorOptions
3730
+ };
3731
+ let alignment = oldWidgetInfo.alignment;
3732
+
3733
+ // Widgets that support multiple alignments will "lock in" the
3734
+ // alignment to the alignment that would be listed first in the
3735
+ // select box. If the widget only supports one alignment, the
3736
+ // alignment value will likely just end up as "default".
3737
+ if (alignment == null || alignment === "default") {
3738
+ alignment = getSupportedAlignments(type)?.[0];
3739
+ if (!alignment) {
3740
+ throw new PerseusError("No default alignment found when upgrading widget", Errors.Internal, {
3741
+ metadata: {
3742
+ widgetType: type
3743
+ }
3744
+ });
3745
+ }
3746
+ }
3747
+ let widgetStatic = oldWidgetInfo.static;
3748
+ if (widgetStatic == null) {
3749
+ widgetStatic = DEFAULT_STATIC;
3750
+ }
3751
+ return {
3752
+ ...oldWidgetInfo,
3753
+ // maintain other info, like type
3754
+ // After upgrading we guarantee that the version is up-to-date
3755
+ version: latestVersion,
3756
+ // Default graded to true (so null/undefined becomes true):
3757
+ graded: oldWidgetInfo.graded != null ? oldWidgetInfo.graded : true,
3758
+ alignment: alignment,
3759
+ static: widgetStatic,
3760
+ options: newEditorOptions
3761
+ };
3762
+ };
3763
+ function getUpgradedWidgetOptions(oldWidgetOptions) {
3764
+ return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
3765
+ if (!widgetInfo.type || !widgetInfo.alignment) {
3766
+ const newValues = {};
3767
+ if (!widgetInfo.type) {
3768
+ // TODO: why does widget have no type?
3769
+ // We don't want to derive type from widget ID
3770
+ // see: LEMS-1845
3771
+ newValues.type = widgetId.split(" ")[0];
3772
+ }
3773
+ if (!widgetInfo.alignment) {
3774
+ newValues.alignment = "default";
3775
+ }
3776
+ widgetInfo = {
3777
+ ...widgetInfo,
3778
+ ...newValues
3779
+ };
3780
+ }
3781
+ return upgradeWidgetInfoToLatestVersion(widgetInfo);
3782
+ });
3783
+ }
3784
+
3785
+ function splitPerseusItem(originalItem) {
3786
+ const item = ___default["default"].clone(originalItem);
3787
+ const originalWidgets = item.widgets ?? {};
3788
+ const upgradedWidgets = getUpgradedWidgetOptions(originalWidgets);
3789
+ const splitWidgets = {};
3790
+ for (const [id, widget] of Object.entries(upgradedWidgets)) {
3791
+ const publicWidgetOptionsFun = getPublicWidgetOptionsFunction(widget.type);
3792
+ splitWidgets[id] = {
3793
+ ...widget,
3794
+ options: publicWidgetOptionsFun(widget.options)
3795
+ };
3796
+ }
3797
+ return {
3798
+ ...item,
3799
+ widgets: splitWidgets
3800
+ };
3801
+ }
3802
+
3803
+ /* Note(tamara): Brought over from the perseus package packages/perseus/src/util.ts file.
3804
+ May be useful to bring other perseus package utilities here. Contains utility functions
3805
+ and types used across multiple widgets for randomization and shuffling. */
3806
+ const seededRNG = function (seed) {
3807
+ let randomSeed = seed;
3808
+ return function () {
3809
+ // Robert Jenkins' 32 bit integer hash function.
3810
+ let seed = randomSeed;
3811
+ seed = seed + 0x7ed55d16 + (seed << 12) & 0xffffffff;
3812
+ seed = (seed ^ 0xc761c23c ^ seed >>> 19) & 0xffffffff;
3813
+ seed = seed + 0x165667b1 + (seed << 5) & 0xffffffff;
3814
+ seed = (seed + 0xd3a2646c ^ seed << 9) & 0xffffffff;
3815
+ seed = seed + 0xfd7046c5 + (seed << 3) & 0xffffffff;
3816
+ seed = (seed ^ 0xb55a4f09 ^ seed >>> 16) & 0xffffffff;
3817
+ return (randomSeed = seed & 0xfffffff) / 0x10000000;
3818
+ };
3819
+ };
3820
+
3821
+ // Shuffle an array using a given random seed or function.
3822
+ // If `ensurePermuted` is true, the input and output are guaranteed to be
3823
+ // distinct permutations.
3824
+ function shuffle(array, randomSeed) {
3825
+ let ensurePermuted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
3826
+ // Always return a copy of the input array
3827
+ const shuffled = ___default["default"].clone(array);
3828
+
3829
+ // Handle edge cases (input array is empty or uniform)
3830
+ if (!shuffled.length || ___default["default"].all(shuffled, function (value) {
3831
+ return ___default["default"].isEqual(value, shuffled[0]);
3832
+ })) {
3833
+ return shuffled;
3834
+ }
3835
+ let random;
3836
+ if (typeof randomSeed === "function") {
3837
+ random = randomSeed;
3838
+ } else {
3839
+ random = seededRNG(randomSeed);
3840
+ }
3841
+ do {
3842
+ // Fischer-Yates shuffle
3843
+ for (let top = shuffled.length; top > 0; top--) {
3844
+ const newEnd = Math.floor(random() * top);
3845
+ const temp = shuffled[newEnd];
3846
+
3847
+ // @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
3848
+ shuffled[newEnd] = shuffled[top - 1];
3849
+ // @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
3850
+ shuffled[top - 1] = temp;
3851
+ }
3852
+ } while (ensurePermuted && ___default["default"].isEqual(array, shuffled));
3853
+ return shuffled;
3854
+ }
3855
+ const random = seededRNG(new Date().getTime() & 0xffffffff);
50
3856
 
51
- exports.addLibraryVersionToPerseusDebug = addLibraryVersionToPerseusDebug;
3857
+ exports.CoreWidgetRegistry = coreWidgetRegistry;
3858
+ exports.Errors = Errors;
3859
+ exports.GrapherUtil = grapherUtil;
3860
+ exports.ItemExtras = ItemExtras;
3861
+ exports.PerseusError = PerseusError;
3862
+ exports.PerseusExpressionAnswerFormConsidered = PerseusExpressionAnswerFormConsidered;
3863
+ exports.addWidget = addWidget;
3864
+ exports.approximateDeepEqual = approximateDeepEqual;
3865
+ exports.approximateEqual = approximateEqual;
3866
+ exports.categorizerLogic = categorizerWidgetLogic;
3867
+ exports.csProgramLogic = csProgramWidgetLogic;
3868
+ exports.deepClone = deepClone;
3869
+ exports.definitionLogic = definitionWidgetLogic;
3870
+ exports.dropdownLogic = dropdownWidgetLogic;
3871
+ exports.explanationLogic = explanationWidgetLogic;
3872
+ exports.expressionLogic = expressionWidgetLogic;
3873
+ exports.getCSProgramPublicWidgetOptions = getCSProgramPublicWidgetOptions;
3874
+ exports.getCategorizerPublicWidgetOptions = getCategorizerPublicWidgetOptions;
3875
+ exports.getDecimalSeparator = getDecimalSeparator;
3876
+ exports.getDropdownPublicWidgetOptions = getDropdownPublicWidgetOptions;
3877
+ exports.getExpressionPublicWidgetOptions = getExpressionPublicWidgetOptions;
3878
+ exports.getGrapherPublicWidgetOptions = getGrapherPublicWidgetOptions;
3879
+ exports.getIFramePublicWidgetOptions = getIFramePublicWidgetOptions;
3880
+ exports.getInteractiveGraphPublicWidgetOptions = getInteractiveGraphPublicWidgetOptions;
3881
+ exports.getLabelImagePublicWidgetOptions = getLabelImagePublicWidgetOptions;
3882
+ exports.getMatcherPublicWidgetOptions = getMatcherPublicWidgetOptions;
3883
+ exports.getMatrixPublicWidgetOptions = getMatrixPublicWidgetOptions;
3884
+ exports.getMatrixSize = getMatrixSize;
3885
+ exports.getNumberLinePublicWidgetOptions = getNumberLinePublicWidgetOptions;
3886
+ exports.getNumericInputPublicWidgetOptions = getNumericInputPublicWidgetOptions;
3887
+ exports.getOrdererPublicWidgetOptions = getOrdererPublicWidgetOptions;
3888
+ exports.getPlotterPublicWidgetOptions = getPlotterPublicWidgetOptions;
3889
+ exports.getRadioPublicWidgetOptions = getRadioPublicWidgetOptions;
3890
+ exports.getSorterPublicWidgetOptions = getSorterPublicWidgetOptions;
3891
+ exports.getTablePublicWidgetOptions = getTablePublicWidgetOptions;
3892
+ exports.getUpgradedWidgetOptions = getUpgradedWidgetOptions;
3893
+ exports.getWidgetIdsFromContent = getWidgetIdsFromContent;
3894
+ exports.getWidgetIdsFromContentByType = getWidgetIdsFromContentByType;
3895
+ exports.gradedGroupLogic = gradedGroupWidgetLogic;
3896
+ exports.gradedGroupSetLogic = gradedGroupSetWidgetLogic;
3897
+ exports.grapherLogic = grapherWidgetLogic;
3898
+ exports.groupLogic = groupWidgetLogic;
3899
+ exports.iframeLogic = iframeWidgetLogic;
3900
+ exports.imageLogic = imageWidgetLogic;
3901
+ exports.inputNumberLogic = inputNumberWidgetLogic;
3902
+ exports.interactionLogic = interactionWidgetLogic;
3903
+ exports.interactiveGraphLogic = interactiveGraphWidgetLogic;
3904
+ exports.isFailure = isFailure;
3905
+ exports.isSuccess = isSuccess;
3906
+ exports.labelImageLogic = labelImageWidgetLogic;
52
3907
  exports.libVersion = libVersion;
3908
+ exports.lockedFigureColorNames = lockedFigureColorNames;
3909
+ exports.lockedFigureColors = lockedFigureColors;
3910
+ exports.lockedFigureFillStyles = lockedFigureFillStyles;
3911
+ exports.mapObject = mapObject;
3912
+ exports.matcherLogic = matcherWidgetLogic;
3913
+ exports.matrixLogic = matrixWidgetLogic;
3914
+ exports.measurerLogic = measurerWidgetLogic;
3915
+ exports.numberLineLogic = numberLineWidgetLogic;
3916
+ exports.numericInputLogic = numericInputWidgetLogic;
3917
+ exports.ordererLogic = ordererWidgetLogic;
3918
+ exports.parseAndMigratePerseusArticle = parseAndMigratePerseusArticle;
3919
+ exports.parseAndMigratePerseusItem = parseAndMigratePerseusItem;
3920
+ exports.parsePerseusItem = parsePerseusItem;
3921
+ exports.passageLogic = passageWidgetLogic;
3922
+ exports.passageRefLogic = passageRefWidgetLogic;
3923
+ exports.passageRefTargetLogic = passageRefTargetWidgetLogic;
3924
+ exports.phetSimulationLogic = phetSimulationWidgetLogic;
3925
+ exports.plotterLogic = plotterWidgetLogic;
3926
+ exports.plotterPlotTypes = plotterPlotTypes;
3927
+ exports.pluck = pluck;
3928
+ exports.pythonProgramLogic = pythonProgramWidgetLogic;
3929
+ exports.radioLogic = radioWidgetLogic;
3930
+ exports.random = random;
3931
+ exports.seededRNG = seededRNG;
3932
+ exports.shuffle = shuffle;
3933
+ exports.shuffleMatcher = shuffleMatcher;
3934
+ exports.sorterLogic = sorterWidgetLogic;
3935
+ exports.splitPerseusItem = splitPerseusItem;
3936
+ exports.tableLogic = tableWidgetLogic;
3937
+ exports.upgradeWidgetInfoToLatestVersion = upgradeWidgetInfoToLatestVersion;
3938
+ exports.videoLogic = videoWidgetLogic;
53
3939
  //# sourceMappingURL=index.js.map