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

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