@khanacademy/perseus-core 6.0.0 → 7.0.0

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