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