@khanacademy/perseus-core 6.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/index.js +4090 -0
- package/dist/es/index.js.map +1 -0
- package/dist/index.js +1253 -155
- package/dist/index.js.map +1 -1
- package/package.json +6 -13
package/dist/index.js
CHANGED
|
@@ -1,19 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var _ = require('underscore');
|
|
6
|
+
var KAS = require('@khanacademy/kas');
|
|
7
|
+
var perseusUtils = require('@khanacademy/perseus-utils');
|
|
8
|
+
|
|
9
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
function _interopNamespaceCompat(e) {
|
|
12
|
+
if (e && typeof e === 'object' && 'default' in e) return e;
|
|
13
|
+
var n = Object.create(null);
|
|
14
|
+
if (e) {
|
|
15
|
+
Object.keys(e).forEach(function (k) {
|
|
16
|
+
if (k !== 'default') {
|
|
17
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
18
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () { return e[k]; }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
n.default = e;
|
|
26
|
+
return Object.freeze(n);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var ___default = /*#__PURE__*/_interopDefaultCompat(_);
|
|
30
|
+
var KAS__namespace = /*#__PURE__*/_interopNamespaceCompat(KAS);
|
|
6
31
|
|
|
7
32
|
function getMatrixSize(matrix) {
|
|
8
33
|
const matrixSize = [1, 1];
|
|
9
|
-
|
|
34
|
+
|
|
35
|
+
// We need to find the widest row and tallest column to get the correct
|
|
36
|
+
// matrix size.
|
|
37
|
+
___default.default(matrix).each((matrixRow, row) => {
|
|
10
38
|
let rowWidth = 0;
|
|
11
|
-
|
|
39
|
+
___default.default(matrixRow).each((matrixCol, col) => {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
12
41
|
if (matrixCol != null && matrixCol.toString().length) {
|
|
13
42
|
rowWidth = col + 1;
|
|
14
43
|
}
|
|
15
44
|
});
|
|
45
|
+
|
|
46
|
+
// Matrix width:
|
|
16
47
|
matrixSize[1] = Math.max(matrixSize[1], rowWidth);
|
|
48
|
+
|
|
49
|
+
// Matrix height:
|
|
17
50
|
if (rowWidth > 0) {
|
|
18
51
|
matrixSize[0] = Math.max(matrixSize[0], row + 1);
|
|
19
52
|
}
|
|
@@ -21,24 +54,47 @@ function getMatrixSize(matrix) {
|
|
|
21
54
|
return matrixSize;
|
|
22
55
|
}
|
|
23
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Get the character used for separating decimals.
|
|
59
|
+
*/
|
|
24
60
|
const getDecimalSeparator = locale => {
|
|
25
|
-
var _match$;
|
|
26
61
|
switch (locale) {
|
|
62
|
+
// TODO(somewhatabstract): Remove this when Chrome supports the `ka`
|
|
63
|
+
// locale properly.
|
|
64
|
+
// https://github.com/formatjs/formatjs/issues/1526#issuecomment-559891201
|
|
65
|
+
//
|
|
66
|
+
// Supported locales in Chrome:
|
|
67
|
+
// https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/scripts/chrome_ui_languages.list
|
|
27
68
|
case "ka":
|
|
28
69
|
return ",";
|
|
29
70
|
default:
|
|
30
71
|
const numberWithDecimalSeparator = 1.1;
|
|
31
|
-
|
|
32
|
-
|
|
72
|
+
// TODO(FEI-3647): Update to use .formatToParts() once we no longer have to
|
|
73
|
+
// support Safari 12.
|
|
74
|
+
const match = new Intl.NumberFormat(locale).format(numberWithDecimalSeparator)
|
|
75
|
+
// 0x661 is ARABIC-INDIC DIGIT ONE
|
|
76
|
+
// 0x6F1 is EXTENDED ARABIC-INDIC DIGIT ONE
|
|
77
|
+
// 0x967 is DEVANAGARI DIGIT ONE
|
|
78
|
+
// 0x9e7 is BENGALI/BANGLA DIGIT ONE
|
|
79
|
+
.match(/[^\d\u0661\u06F1\u0967\u09e7]/);
|
|
80
|
+
return match?.[0] ?? ".";
|
|
33
81
|
}
|
|
34
82
|
};
|
|
35
83
|
|
|
84
|
+
/**
|
|
85
|
+
* APPROXIMATE equality on numbers and primitives.
|
|
86
|
+
*/
|
|
36
87
|
function approximateEqual(x, y) {
|
|
37
88
|
if (typeof x === "number" && typeof y === "number") {
|
|
38
89
|
return Math.abs(x - y) < 1e-9;
|
|
39
90
|
}
|
|
40
91
|
return x === y;
|
|
41
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Deep APPROXIMATE equality on primitives, numbers, arrays, and objects.
|
|
96
|
+
* Recursive.
|
|
97
|
+
*/
|
|
42
98
|
function approximateDeepEqual(x, y) {
|
|
43
99
|
if (Array.isArray(x) && Array.isArray(y)) {
|
|
44
100
|
if (x.length !== y.length) {
|
|
@@ -61,9 +117,11 @@ function approximateDeepEqual(x, y) {
|
|
|
61
117
|
return false;
|
|
62
118
|
}
|
|
63
119
|
if (typeof x === "object" && typeof y === "object" && !!x && !!y) {
|
|
64
|
-
return x === y ||
|
|
120
|
+
return x === y || ___default.default.all(x, function (v, k) {
|
|
121
|
+
// @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
|
|
65
122
|
return approximateDeepEqual(y[k], v);
|
|
66
|
-
}) &&
|
|
123
|
+
}) && ___default.default.all(y, function (v, k) {
|
|
124
|
+
// @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
|
|
67
125
|
return approximateDeepEqual(x[k], v);
|
|
68
126
|
});
|
|
69
127
|
}
|
|
@@ -73,12 +131,38 @@ function approximateDeepEqual(x, y) {
|
|
|
73
131
|
return approximateEqual(x, y);
|
|
74
132
|
}
|
|
75
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Add a widget placeholder using the widget ID.
|
|
136
|
+
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
137
|
+
*
|
|
138
|
+
* @param {string} id
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
76
141
|
function addWidget(id) {
|
|
77
142
|
return `[[☃ ${id}]]`;
|
|
78
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Regex for widget placeholders in a string.
|
|
147
|
+
*
|
|
148
|
+
* First capture group is the widget ID (ex. 'radio 1')
|
|
149
|
+
* Second capture group is the widget type (ex. "radio)
|
|
150
|
+
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
151
|
+
*/
|
|
79
152
|
function getWidgetRegex() {
|
|
80
153
|
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
81
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Extract all widget IDs, which includes the widget type and instance number.
|
|
158
|
+
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
159
|
+
*
|
|
160
|
+
* Content should contain Perseus widget placeholders,
|
|
161
|
+
* which look like: '[[☃ radio 1]]'.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} content
|
|
164
|
+
* @returns {ReadonlyArray<string>} widgetIds
|
|
165
|
+
*/
|
|
82
166
|
function getWidgetIdsFromContent(content) {
|
|
83
167
|
const widgets = [];
|
|
84
168
|
const localWidgetRegex = getWidgetRegex();
|
|
@@ -89,18 +173,31 @@ function getWidgetIdsFromContent(content) {
|
|
|
89
173
|
}
|
|
90
174
|
return widgets;
|
|
91
175
|
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get a list of widget IDs from content,
|
|
179
|
+
* but only for specific widget types
|
|
180
|
+
*
|
|
181
|
+
* @param {string} type the type of widget (ie "radio")
|
|
182
|
+
* @param {string} content the string to parse
|
|
183
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
184
|
+
* @returns {ReadonlyArray<string>} the widget type (ie "radio")
|
|
185
|
+
*/
|
|
92
186
|
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
93
187
|
const rv = [];
|
|
94
188
|
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
95
189
|
widgetIdsInContent.forEach(widgetId => {
|
|
96
190
|
const widget = widgetMap[widgetId];
|
|
97
|
-
if (
|
|
191
|
+
if (widget?.type === type) {
|
|
98
192
|
rv.push(widgetId);
|
|
99
193
|
}
|
|
100
194
|
});
|
|
101
195
|
return rv;
|
|
102
196
|
}
|
|
103
197
|
|
|
198
|
+
// TODO(benchristel): in the future, we may want to make deepClone work for
|
|
199
|
+
// Record<string, Cloneable> as well. Currently, it only does arrays.
|
|
200
|
+
|
|
104
201
|
function deepClone(obj) {
|
|
105
202
|
if (Array.isArray(obj)) {
|
|
106
203
|
return obj.map(deepClone);
|
|
@@ -113,22 +210,33 @@ const MOVABLES = {
|
|
|
113
210
|
PARABOLA: "PARABOLA",
|
|
114
211
|
SINUSOID: "SINUSOID"
|
|
115
212
|
};
|
|
213
|
+
|
|
214
|
+
// TODO(charlie): These really need to go into a utility file as they're being
|
|
215
|
+
// used by both interactive-graph and now grapher.
|
|
116
216
|
function canonicalSineCoefficients(coeffs) {
|
|
217
|
+
// For a curve of the form f(x) = a * Sin(b * x - c) + d,
|
|
218
|
+
// this function ensures that a, b > 0, and c is its
|
|
219
|
+
// smallest possible positive value.
|
|
117
220
|
let amplitude = coeffs[0];
|
|
118
221
|
let angularFrequency = coeffs[1];
|
|
119
222
|
let phase = coeffs[2];
|
|
120
223
|
const verticalOffset = coeffs[3];
|
|
224
|
+
|
|
225
|
+
// Guarantee a > 0
|
|
121
226
|
if (amplitude < 0) {
|
|
122
227
|
amplitude *= -1;
|
|
123
228
|
angularFrequency *= -1;
|
|
124
229
|
phase *= -1;
|
|
125
230
|
}
|
|
126
231
|
const period = 2 * Math.PI;
|
|
232
|
+
// Guarantee b > 0
|
|
127
233
|
if (angularFrequency < 0) {
|
|
128
234
|
angularFrequency *= -1;
|
|
129
235
|
phase *= -1;
|
|
130
236
|
phase += period / 2;
|
|
131
237
|
}
|
|
238
|
+
|
|
239
|
+
// Guarantee c is smallest possible positive value
|
|
132
240
|
while (phase > 0) {
|
|
133
241
|
phase -= period;
|
|
134
242
|
}
|
|
@@ -138,21 +246,29 @@ function canonicalSineCoefficients(coeffs) {
|
|
|
138
246
|
return [amplitude, angularFrequency, phase, verticalOffset];
|
|
139
247
|
}
|
|
140
248
|
function canonicalTangentCoefficients(coeffs) {
|
|
249
|
+
// For a curve of the form f(x) = a * Tan(b * x - c) + d,
|
|
250
|
+
// this function ensures that a, b > 0, and c is its
|
|
251
|
+
// smallest possible positive value.
|
|
141
252
|
let amplitude = coeffs[0];
|
|
142
253
|
let angularFrequency = coeffs[1];
|
|
143
254
|
let phase = coeffs[2];
|
|
144
255
|
const verticalOffset = coeffs[3];
|
|
256
|
+
|
|
257
|
+
// Guarantee a > 0
|
|
145
258
|
if (amplitude < 0) {
|
|
146
259
|
amplitude *= -1;
|
|
147
260
|
angularFrequency *= -1;
|
|
148
261
|
phase *= -1;
|
|
149
262
|
}
|
|
150
263
|
const period = Math.PI;
|
|
264
|
+
// Guarantee b > 0
|
|
151
265
|
if (angularFrequency < 0) {
|
|
152
266
|
angularFrequency *= -1;
|
|
153
267
|
phase *= -1;
|
|
154
268
|
phase += period / 2;
|
|
155
269
|
}
|
|
270
|
+
|
|
271
|
+
// Guarantee c is smallest possible positive value
|
|
156
272
|
while (phase > 0) {
|
|
157
273
|
phase -= period;
|
|
158
274
|
}
|
|
@@ -168,11 +284,12 @@ const PlotDefaults = {
|
|
|
168
284
|
movable: MOVABLES.PLOT,
|
|
169
285
|
getPropsForCoeffs: function (coeffs) {
|
|
170
286
|
return {
|
|
171
|
-
|
|
287
|
+
// @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; }'.
|
|
288
|
+
fn: ___default.default.partial(this.getFunctionForCoeffs, coeffs)
|
|
172
289
|
};
|
|
173
290
|
}
|
|
174
291
|
};
|
|
175
|
-
const Linear =
|
|
292
|
+
const Linear = ___default.default.extend({}, PlotDefaults, {
|
|
176
293
|
url: "https://ka-perseus-graphie.s3.amazonaws.com/67aaf581e6d9ef9038c10558a1f70ac21c11c9f8.png",
|
|
177
294
|
defaultCoords: [[0.25, 0.75], [0.75, 0.75]],
|
|
178
295
|
getCoefficients: function (coords) {
|
|
@@ -199,15 +316,19 @@ const Linear = _.extend({}, PlotDefaults, {
|
|
|
199
316
|
return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
|
|
200
317
|
}
|
|
201
318
|
});
|
|
202
|
-
const Quadratic =
|
|
319
|
+
const Quadratic = ___default.default.extend({}, PlotDefaults, {
|
|
203
320
|
url: "https://ka-perseus-graphie.s3.amazonaws.com/e23d36e6fc29ee37174e92c9daba2a66677128ab.png",
|
|
204
321
|
defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
|
|
205
322
|
movable: MOVABLES.PARABOLA,
|
|
206
323
|
getCoefficients: function (coords) {
|
|
207
324
|
const p1 = coords[0];
|
|
208
325
|
const p2 = coords[1];
|
|
326
|
+
|
|
327
|
+
// Parabola with vertex (h, k) has form: y = a * (h - k)^2 + k
|
|
209
328
|
const h = p1[0];
|
|
210
329
|
const k = p1[1];
|
|
330
|
+
|
|
331
|
+
// Use these to calculate familiar a, b, c
|
|
211
332
|
const a = (p2[1] - k) / ((p2[0] - h) * (p2[0] - h));
|
|
212
333
|
const b = -2 * h * a;
|
|
213
334
|
const c = a * h * h + k;
|
|
@@ -234,7 +355,7 @@ const Quadratic = _.extend({}, PlotDefaults, {
|
|
|
234
355
|
return "y = " + a.toFixed(3) + "x^2 + " + b.toFixed(3) + "x + " + c.toFixed(3);
|
|
235
356
|
}
|
|
236
357
|
});
|
|
237
|
-
const Sinusoid =
|
|
358
|
+
const Sinusoid = ___default.default.extend({}, PlotDefaults, {
|
|
238
359
|
url: "https://ka-perseus-graphie.s3.amazonaws.com/3d68e7718498475f53b206c2ab285626baf8857e.png",
|
|
239
360
|
defaultCoords: [[0.5, 0.5], [0.6, 0.6]],
|
|
240
361
|
movable: MOVABLES.SINUSOID,
|
|
@@ -274,7 +395,7 @@ const Sinusoid = _.extend({}, PlotDefaults, {
|
|
|
274
395
|
return approximateDeepEqual(canonicalSineCoefficients(coeffs1), canonicalSineCoefficients(coeffs2));
|
|
275
396
|
}
|
|
276
397
|
});
|
|
277
|
-
const Tangent =
|
|
398
|
+
const Tangent = ___default.default.extend({}, PlotDefaults, {
|
|
278
399
|
url: "https://ka-perseus-graphie.s3.amazonaws.com/7db80d23c35214f98659fe1cf0765811c1bbfbba.png",
|
|
279
400
|
defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
|
|
280
401
|
getCoefficients: function (coords) {
|
|
@@ -305,27 +426,46 @@ const Tangent = _.extend({}, PlotDefaults, {
|
|
|
305
426
|
return approximateDeepEqual(canonicalTangentCoefficients(coeffs1), canonicalTangentCoefficients(coeffs2));
|
|
306
427
|
}
|
|
307
428
|
});
|
|
308
|
-
const Exponential =
|
|
429
|
+
const Exponential = ___default.default.extend({}, PlotDefaults, {
|
|
309
430
|
url: "https://ka-perseus-graphie.s3.amazonaws.com/9cbfad55525e3ce755a31a631b074670a5dad611.png",
|
|
310
431
|
defaultCoords: [[0.5, 0.55], [0.75, 0.75]],
|
|
311
432
|
defaultAsymptote: [[0, 0.5], [1.0, 0.5]],
|
|
433
|
+
/**
|
|
434
|
+
* Add extra constraints for movement of the points or asymptote (below):
|
|
435
|
+
* newCoord: [x, y]
|
|
436
|
+
* The end position of the point or asymptote endpoint
|
|
437
|
+
* oldCoord: [x, y]
|
|
438
|
+
* The old position of the point or asymptote endpoint
|
|
439
|
+
* coords:
|
|
440
|
+
* An array of coordinates representing the proposed end configuration
|
|
441
|
+
* of the plot coordinates.
|
|
442
|
+
* asymptote:
|
|
443
|
+
* An array of coordinates representing the proposed end configuration
|
|
444
|
+
* of the asymptote.
|
|
445
|
+
*
|
|
446
|
+
* Return: either a coordinate (to be used as the resulting coordinate of
|
|
447
|
+
* the move) or a boolean, where `true` uses newCoord as the resulting
|
|
448
|
+
* coordinate, and `false` uses oldCoord as the resulting coordinate.
|
|
449
|
+
*/
|
|
312
450
|
extraCoordConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
|
|
313
451
|
const y = asymptote[0][1];
|
|
314
|
-
return
|
|
452
|
+
return ___default.default.all(coords, coord => coord[1] !== y);
|
|
315
453
|
},
|
|
316
454
|
extraAsymptoteConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
|
|
317
455
|
const y = newCoord[1];
|
|
318
|
-
const isValid =
|
|
456
|
+
const isValid = ___default.default.all(coords, coord => coord[1] > y) || ___default.default.all(coords, coord => coord[1] < y);
|
|
319
457
|
if (isValid) {
|
|
320
458
|
return [oldCoord[0], y];
|
|
321
459
|
}
|
|
460
|
+
// Snap the asymptote as close as possible, i.e., if the user moves
|
|
461
|
+
// the mouse really quickly into an invalid region
|
|
322
462
|
const oldY = oldCoord[1];
|
|
323
|
-
const wasBelow =
|
|
463
|
+
const wasBelow = ___default.default.all(coords, coord => coord[1] > oldY);
|
|
324
464
|
if (wasBelow) {
|
|
325
|
-
const bottomMost =
|
|
465
|
+
const bottomMost = ___default.default.min(___default.default.map(coords, coord => coord[1]));
|
|
326
466
|
return [oldCoord[0], bottomMost - graph.snapStep[1]];
|
|
327
467
|
}
|
|
328
|
-
const topMost =
|
|
468
|
+
const topMost = ___default.default.max(___default.default.map(coords, coord => coord[1]));
|
|
329
469
|
return [oldCoord[0], topMost + graph.snapStep[1]];
|
|
330
470
|
},
|
|
331
471
|
allowReflectOverAsymptote: true,
|
|
@@ -344,6 +484,7 @@ const Exponential = _.extend({}, PlotDefaults, {
|
|
|
344
484
|
return a * Math.exp(b * x) + c;
|
|
345
485
|
},
|
|
346
486
|
getEquationString: function (coords, asymptote) {
|
|
487
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
347
488
|
if (!asymptote) {
|
|
348
489
|
return null;
|
|
349
490
|
}
|
|
@@ -354,33 +495,39 @@ const Exponential = _.extend({}, PlotDefaults, {
|
|
|
354
495
|
return "y = " + a.toFixed(3) + "e^(" + b.toFixed(3) + "x) + " + c.toFixed(3);
|
|
355
496
|
}
|
|
356
497
|
});
|
|
357
|
-
const Logarithm =
|
|
498
|
+
const Logarithm = ___default.default.extend({}, PlotDefaults, {
|
|
358
499
|
url: "https://ka-perseus-graphie.s3.amazonaws.com/f6491e99d34af34d924bfe0231728ad912068dc3.png",
|
|
359
500
|
defaultCoords: [[0.55, 0.5], [0.75, 0.75]],
|
|
360
501
|
defaultAsymptote: [[0.5, 0], [0.5, 1.0]],
|
|
361
502
|
extraCoordConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
|
|
362
503
|
const x = asymptote[0][0];
|
|
363
|
-
return
|
|
504
|
+
return ___default.default.all(coords, coord => coord[0] !== x) && coords[0][1] !== coords[1][1];
|
|
364
505
|
},
|
|
365
506
|
extraAsymptoteConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
|
|
366
507
|
const x = newCoord[0];
|
|
367
|
-
const isValid =
|
|
508
|
+
const isValid = ___default.default.all(coords, coord => coord[0] > x) || ___default.default.all(coords, coord => coord[0] < x);
|
|
368
509
|
if (isValid) {
|
|
369
510
|
return [x, oldCoord[1]];
|
|
370
511
|
}
|
|
512
|
+
// Snap the asymptote as close as possible, i.e., if the user moves
|
|
513
|
+
// the mouse really quickly into an invalid region
|
|
371
514
|
const oldX = oldCoord[0];
|
|
372
|
-
const wasLeft =
|
|
515
|
+
const wasLeft = ___default.default.all(coords, coord => coord[0] > oldX);
|
|
373
516
|
if (wasLeft) {
|
|
374
|
-
const leftMost =
|
|
517
|
+
const leftMost = ___default.default.min(___default.default.map(coords, coord => coord[0]));
|
|
375
518
|
return [leftMost - graph.snapStep[0], oldCoord[1]];
|
|
376
519
|
}
|
|
377
|
-
const rightMost =
|
|
520
|
+
const rightMost = ___default.default.max(___default.default.map(coords, coord => coord[0]));
|
|
378
521
|
return [rightMost + graph.snapStep[0], oldCoord[1]];
|
|
379
522
|
},
|
|
380
523
|
allowReflectOverAsymptote: true,
|
|
381
524
|
getCoefficients: function (coords, asymptote) {
|
|
525
|
+
// It's easiest to calculate the logarithm's coefficients by thinking
|
|
526
|
+
// about it as the inverse of the exponential, so we flip x and y and
|
|
527
|
+
// perform some algebra on the coefficients. This also unifies the
|
|
528
|
+
// logic between the two 'models'.
|
|
382
529
|
const flip = coord => [coord[1], coord[0]];
|
|
383
|
-
const inverseCoeffs = Exponential.getCoefficients(
|
|
530
|
+
const inverseCoeffs = Exponential.getCoefficients(___default.default.map(coords, flip), ___default.default.map(asymptote, flip));
|
|
384
531
|
if (inverseCoeffs) {
|
|
385
532
|
const c = -inverseCoeffs[2] / inverseCoeffs[0];
|
|
386
533
|
const b = 1 / inverseCoeffs[0];
|
|
@@ -395,6 +542,7 @@ const Logarithm = _.extend({}, PlotDefaults, {
|
|
|
395
542
|
return a * Math.log(b * x + c);
|
|
396
543
|
},
|
|
397
544
|
getEquationString: function (coords, asymptote) {
|
|
545
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
398
546
|
if (!asymptote) {
|
|
399
547
|
return null;
|
|
400
548
|
}
|
|
@@ -405,7 +553,7 @@ const Logarithm = _.extend({}, PlotDefaults, {
|
|
|
405
553
|
return "y = ln(" + a.toFixed(3) + "x + " + b.toFixed(3) + ") + " + c.toFixed(3);
|
|
406
554
|
}
|
|
407
555
|
});
|
|
408
|
-
const AbsoluteValue =
|
|
556
|
+
const AbsoluteValue = ___default.default.extend({}, PlotDefaults, {
|
|
409
557
|
url: "https://ka-perseus-graphie.s3.amazonaws.com/8256a630175a0cb1d11de223d6de0266daf98721.png",
|
|
410
558
|
defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
|
|
411
559
|
getCoefficients: function (coords) {
|
|
@@ -438,6 +586,8 @@ const AbsoluteValue = _.extend({}, PlotDefaults, {
|
|
|
438
586
|
return "y = " + m.toFixed(3) + "| x - " + horizontalOffset.toFixed(3) + "| + " + verticalOffset.toFixed(3);
|
|
439
587
|
}
|
|
440
588
|
});
|
|
589
|
+
|
|
590
|
+
/* Utility functions for dealing with graphing interfaces. */
|
|
441
591
|
const functionTypeMapping = {
|
|
442
592
|
linear: Linear,
|
|
443
593
|
quadratic: Quadratic,
|
|
@@ -447,8 +597,10 @@ const functionTypeMapping = {
|
|
|
447
597
|
logarithm: Logarithm,
|
|
448
598
|
absolute_value: AbsoluteValue
|
|
449
599
|
};
|
|
450
|
-
const allTypes =
|
|
600
|
+
const allTypes = ___default.default.keys(functionTypeMapping);
|
|
451
601
|
function functionForType(type) {
|
|
602
|
+
// @ts-expect-error: TypeScript doesn't know how to use deal with generics
|
|
603
|
+
// and conditional types in this way.
|
|
452
604
|
return functionTypeMapping[type];
|
|
453
605
|
}
|
|
454
606
|
|
|
@@ -517,7 +669,8 @@ function isRealJSONParse(jsonParse) {
|
|
|
517
669
|
const parsedTestItemData = parsedTestJSON.data.assessmentItem.item.itemData;
|
|
518
670
|
return approximateDeepEqual(parsedTestItemData, testingObject);
|
|
519
671
|
}
|
|
520
|
-
function buildRandomString(
|
|
672
|
+
function buildRandomString() {
|
|
673
|
+
let capitalize = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
|
521
674
|
let randomString = "";
|
|
522
675
|
const randomLength = Math.floor(Math.random() * 8) + 3;
|
|
523
676
|
for (let i = 0; i < randomLength; i++) {
|
|
@@ -559,7 +712,11 @@ function isFailure(result) {
|
|
|
559
712
|
function isSuccess(result) {
|
|
560
713
|
return result.type === "success";
|
|
561
714
|
}
|
|
562
|
-
|
|
715
|
+
|
|
716
|
+
// Result's `all` function is similar to Promise.all: given an array of
|
|
717
|
+
// results, it returns success if all succeeded, and failure if any failed.
|
|
718
|
+
function all(results) {
|
|
719
|
+
let combineFailureDetails = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : a => a;
|
|
563
720
|
const values = [];
|
|
564
721
|
const failureDetails = [];
|
|
565
722
|
for (const result of results) {
|
|
@@ -616,6 +773,7 @@ function message(failure) {
|
|
|
616
773
|
}
|
|
617
774
|
function conjoin(items) {
|
|
618
775
|
switch (items.length) {
|
|
776
|
+
// TODO(benchristel): handle 0 if this is reused elsewhere.
|
|
619
777
|
case 1:
|
|
620
778
|
return items[0];
|
|
621
779
|
case 2:
|
|
@@ -668,7 +826,10 @@ function constant(acceptedValue) {
|
|
|
668
826
|
};
|
|
669
827
|
}
|
|
670
828
|
|
|
671
|
-
function enumeration(
|
|
829
|
+
function enumeration() {
|
|
830
|
+
for (var _len = arguments.length, acceptedValues = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
831
|
+
acceptedValues[_key] = arguments[_key];
|
|
832
|
+
}
|
|
672
833
|
return (rawValue, ctx) => {
|
|
673
834
|
if (typeof rawValue === "string") {
|
|
674
835
|
const index = acceptedValues.indexOf(rawValue);
|
|
@@ -706,7 +867,9 @@ function object(schema) {
|
|
|
706
867
|
if (!isObject(rawValue)) {
|
|
707
868
|
return ctx.failure("object", rawValue);
|
|
708
869
|
}
|
|
709
|
-
const ret =
|
|
870
|
+
const ret = {
|
|
871
|
+
...rawValue
|
|
872
|
+
};
|
|
710
873
|
const mismatches = [];
|
|
711
874
|
for (const [prop, propParser] of Object.entries(schema)) {
|
|
712
875
|
const result = propParser(rawValue[prop], ctx.forSubtree(prop));
|
|
@@ -944,26 +1107,54 @@ const parseExplanationWidget = parseWidget(constant("explanation"), object({
|
|
|
944
1107
|
showPrompt: string,
|
|
945
1108
|
hidePrompt: string,
|
|
946
1109
|
explanation: string,
|
|
1110
|
+
// We wrap parseWidgetsMap in a function here to make sure it is not
|
|
1111
|
+
// referenced before it is defined. There is an import cycle between
|
|
1112
|
+
// this file and widgets-map.ts that could cause it to be undefined.
|
|
947
1113
|
widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
|
|
948
1114
|
static: defaulted(boolean, () => false)
|
|
949
1115
|
}));
|
|
950
1116
|
|
|
951
|
-
const KeypadKeys = ["PLUS", "MINUS", "NEGATIVE", "TIMES", "DIVIDE", "DECIMAL", "PERIOD", "PERCENT", "CDOT", "EQUAL", "NEQ", "GT", "LT", "GEQ", "LEQ",
|
|
952
|
-
|
|
1117
|
+
const KeypadKeys = ["PLUS", "MINUS", "NEGATIVE", "TIMES", "DIVIDE", "DECIMAL", "PERIOD", "PERCENT", "CDOT", "EQUAL", "NEQ", "GT", "LT", "GEQ", "LEQ",
|
|
1118
|
+
// mobile native only
|
|
1119
|
+
"FRAC_INCLUSIVE",
|
|
1120
|
+
// mobile native only
|
|
1121
|
+
"FRAC_EXCLUSIVE",
|
|
1122
|
+
// mobile native only
|
|
1123
|
+
"FRAC", "EXP", "EXP_2", "EXP_3", "SQRT", "CUBE_ROOT", "RADICAL", "LEFT_PAREN", "RIGHT_PAREN", "LN", "LOG", "LOG_N", "SIN", "COS",
|
|
1124
|
+
// TODO(charlie): Add in additional Greek letters.,
|
|
1125
|
+
"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",
|
|
1126
|
+
// Multi-functional keys.
|
|
1127
|
+
"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"];
|
|
1128
|
+
|
|
1129
|
+
// Used by KeypadContext to pass around a renderer reference
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Scrape the answer forms for any variables or contants (like Pi)
|
|
1133
|
+
* that need to be included as keys on the keypad.
|
|
1134
|
+
*/
|
|
953
1135
|
function deriveExtraKeys(widgetOptions) {
|
|
954
1136
|
if (widgetOptions.extraKeys) {
|
|
955
1137
|
return widgetOptions.extraKeys;
|
|
956
1138
|
}
|
|
1139
|
+
|
|
1140
|
+
// If there are no extra symbols available, we include Pi anyway, so
|
|
1141
|
+
// that the "extra symbols" button doesn't appear empty.
|
|
957
1142
|
const defaultKeys = ["PI"];
|
|
958
1143
|
if (widgetOptions.answerForms == null) {
|
|
959
1144
|
return defaultKeys;
|
|
960
1145
|
}
|
|
1146
|
+
|
|
1147
|
+
// Extract any and all variables and constants from the answer forms.
|
|
961
1148
|
const uniqueExtraVariables = {};
|
|
962
1149
|
const uniqueExtraConstants = {};
|
|
963
1150
|
for (const answerForm of widgetOptions.answerForms) {
|
|
964
|
-
const maybeExpr =
|
|
1151
|
+
const maybeExpr = KAS__namespace.parse(answerForm.value, widgetOptions);
|
|
965
1152
|
if (maybeExpr.parsed) {
|
|
966
1153
|
const expr = maybeExpr.expr;
|
|
1154
|
+
|
|
1155
|
+
// The keypad expects Greek letters to be capitalized (e.g., it
|
|
1156
|
+
// requires `PI` instead of `pi`). Right now, it only supports Pi
|
|
1157
|
+
// and Theta, so we special-case.
|
|
967
1158
|
const isGreek = symbol => symbol === "pi" || symbol === "theta";
|
|
968
1159
|
const toKey = symbol => isGreek(symbol) ? symbol.toUpperCase() : symbol;
|
|
969
1160
|
const isKey = key => KeypadKeys.includes(key);
|
|
@@ -981,28 +1172,57 @@ function deriveExtraKeys(widgetOptions) {
|
|
|
981
1172
|
}
|
|
982
1173
|
}
|
|
983
1174
|
}
|
|
1175
|
+
|
|
1176
|
+
// TODO(charlie): Alert the keypad as to which of these symbols should be
|
|
1177
|
+
// treated as functions.
|
|
984
1178
|
const extraVariables = Object.keys(uniqueExtraVariables).sort();
|
|
985
1179
|
const extraConstants = Object.keys(uniqueExtraConstants).sort();
|
|
986
1180
|
const extraKeys = [...extraVariables, ...extraConstants];
|
|
1181
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
987
1182
|
if (!extraKeys.length) {
|
|
988
1183
|
return defaultKeys;
|
|
989
1184
|
}
|
|
990
1185
|
return extraKeys;
|
|
991
1186
|
}
|
|
992
1187
|
|
|
1188
|
+
// Given a function, creates a PartialParser that converts one type to another
|
|
1189
|
+
// using that function. The returned parser never fails.
|
|
993
1190
|
function convert(f) {
|
|
994
1191
|
return (rawValue, ctx) => ctx.success(f(rawValue));
|
|
995
1192
|
}
|
|
996
1193
|
|
|
997
1194
|
const parseLegacyButtonSet = enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations", "scientific");
|
|
998
|
-
const parseLegacyButtonSets = defaulted(array(parseLegacyButtonSet),
|
|
999
|
-
|
|
1195
|
+
const parseLegacyButtonSets = defaulted(array(parseLegacyButtonSet),
|
|
1196
|
+
// NOTE(benchristel): I copied the default buttonSets from
|
|
1197
|
+
// expression.tsx. See the parse-perseus-json/README.md for
|
|
1198
|
+
// an explanation of why we want to duplicate the default here.
|
|
1199
|
+
() => ["basic", "trig", "prealgebra", "logarithms"]);
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* Creates a parser for a widget options type with multiple major versions. Old
|
|
1203
|
+
* versions are migrated to the latest version. The parse fails if the input
|
|
1204
|
+
* data does not match any of the versions.
|
|
1205
|
+
*
|
|
1206
|
+
* @example
|
|
1207
|
+
* const parseOptions = versionedWidgetOptions(3, parseOptionsV3)
|
|
1208
|
+
* .withMigrationFrom(2, parseOptionsV2, migrateV2ToV3)
|
|
1209
|
+
* .withMigrationFrom(1, parseOptionsV1, migrateV1ToV2)
|
|
1210
|
+
* .withMigrationFrom(0, parseOptionsV0, migrateV0ToV1)
|
|
1211
|
+
* .parser;
|
|
1212
|
+
*
|
|
1213
|
+
* @param latestMajorVersion the latest major version of the widget options.
|
|
1214
|
+
* @param parseLatest a {@link Parser} for the latest version of the widget
|
|
1215
|
+
* options.
|
|
1216
|
+
* @returns a builder object, to which migrations from earlier versions can be
|
|
1217
|
+
* added. Migrations must be added in "reverse chronological" order as in the
|
|
1218
|
+
* example above.
|
|
1219
|
+
*/
|
|
1000
1220
|
function versionedWidgetOptions(latestMajorVersion, parseLatest) {
|
|
1001
1221
|
return new VersionedWidgetOptionsParserBuilder(latestMajorVersion, parseLatest, latest => latest, (raw, ctx) => ctx.failure("widget options with a known version number", raw));
|
|
1002
1222
|
}
|
|
1003
1223
|
class VersionedWidgetOptionsParserBuilder {
|
|
1224
|
+
parser;
|
|
1004
1225
|
constructor(majorVersion, parseThisVersion, migrateToLatest, parseOtherVersions) {
|
|
1005
|
-
this.parser = void 0;
|
|
1006
1226
|
this.migrateToLatest = migrateToLatest;
|
|
1007
1227
|
this.parseOtherVersions = parseOtherVersions;
|
|
1008
1228
|
const parseThisVersionAndMigrateToLatest = pipeParsers(parseThisVersion).then(convert(this.migrateToLatest)).parser;
|
|
@@ -1020,6 +1240,10 @@ class VersionedWidgetOptionsParserBuilder {
|
|
|
1020
1240
|
return parseThisVersionAndMigrateToLatest(raw, ctx);
|
|
1021
1241
|
};
|
|
1022
1242
|
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Add a migration from an old version of the widget options.
|
|
1246
|
+
*/
|
|
1023
1247
|
withMigrationFrom(majorVersion, parseOldVersion, migrateToNextVersion) {
|
|
1024
1248
|
const parseOtherVersions = this.parser;
|
|
1025
1249
|
const migrateToLatest = old => this.migrateToLatest(migrateToNextVersion(old));
|
|
@@ -1038,6 +1262,9 @@ const parseVersionedObject = object({
|
|
|
1038
1262
|
|
|
1039
1263
|
const stringOrNumberOrNullOrUndefined = union(string).or(number).or(constant(null)).or(constant(undefined)).parser;
|
|
1040
1264
|
const parsePossiblyInvalidAnswerForm = object({
|
|
1265
|
+
// `value` is the possibly invalid part of this. It should always be a
|
|
1266
|
+
// string, but some answer forms don't have it. The Expression widget
|
|
1267
|
+
// ignores invalid values, so we can safely filter them out during parsing.
|
|
1041
1268
|
value: optional(string),
|
|
1042
1269
|
form: defaulted(boolean, () => false),
|
|
1043
1270
|
simplify: defaulted(boolean, () => false),
|
|
@@ -1051,9 +1278,11 @@ function removeInvalidAnswerForms(possiblyInvalid) {
|
|
|
1051
1278
|
value
|
|
1052
1279
|
} = answerForm;
|
|
1053
1280
|
if (value != null) {
|
|
1054
|
-
|
|
1281
|
+
// Copying the object seems to be needed to make TypeScript happy
|
|
1282
|
+
valid.push({
|
|
1283
|
+
...answerForm,
|
|
1055
1284
|
value
|
|
1056
|
-
})
|
|
1285
|
+
});
|
|
1057
1286
|
}
|
|
1058
1287
|
}
|
|
1059
1288
|
return valid;
|
|
@@ -1089,7 +1318,8 @@ function migrateV1ToV2$1(widget) {
|
|
|
1089
1318
|
const {
|
|
1090
1319
|
options
|
|
1091
1320
|
} = widget;
|
|
1092
|
-
return
|
|
1321
|
+
return {
|
|
1322
|
+
...widget,
|
|
1093
1323
|
version: {
|
|
1094
1324
|
major: 2,
|
|
1095
1325
|
minor: 0
|
|
@@ -1104,7 +1334,7 @@ function migrateV1ToV2$1(widget) {
|
|
|
1104
1334
|
answerForms: options.answerForms,
|
|
1105
1335
|
extraKeys: deriveExtraKeys(options)
|
|
1106
1336
|
}
|
|
1107
|
-
}
|
|
1337
|
+
};
|
|
1108
1338
|
}
|
|
1109
1339
|
const version0$1 = optional(object({
|
|
1110
1340
|
major: constant(0),
|
|
@@ -1125,7 +1355,8 @@ function migrateV0ToV1$1(widget) {
|
|
|
1125
1355
|
const {
|
|
1126
1356
|
options
|
|
1127
1357
|
} = widget;
|
|
1128
|
-
return
|
|
1358
|
+
return {
|
|
1359
|
+
...widget,
|
|
1129
1360
|
version: {
|
|
1130
1361
|
major: 1,
|
|
1131
1362
|
minor: 0
|
|
@@ -1144,7 +1375,7 @@ function migrateV0ToV1$1(widget) {
|
|
|
1144
1375
|
value: options.value
|
|
1145
1376
|
}]
|
|
1146
1377
|
}
|
|
1147
|
-
}
|
|
1378
|
+
};
|
|
1148
1379
|
}
|
|
1149
1380
|
const parseExpressionWidget = versionedWidgetOptions(2, parseExpressionWidgetV2).withMigrationFrom(1, parseExpressionWidgetV1, migrateV1ToV2$1).withMigrationFrom(0, parseExpressionWidgetV0, migrateV0ToV1$1).parser;
|
|
1150
1381
|
|
|
@@ -1152,8 +1383,14 @@ const falseToNull = pipeParsers(constant(false)).then(convert(() => null)).parse
|
|
|
1152
1383
|
const parseGradedGroupWidgetOptions = object({
|
|
1153
1384
|
title: defaulted(string, () => ""),
|
|
1154
1385
|
hasHint: optional(nullable(boolean)),
|
|
1386
|
+
// This module has an import cycle with parsePerseusRenderer.
|
|
1387
|
+
// The anonymous function below ensures that we don't try to access
|
|
1388
|
+
// parsePerseusRenderer before it's defined.
|
|
1155
1389
|
hint: union(falseToNull).or(constant(null)).or(constant(undefined)).or((rawVal, ctx) => parsePerseusRenderer(rawVal, ctx)).parser,
|
|
1156
1390
|
content: string,
|
|
1391
|
+
// This module has an import cycle with parseWidgetsMap.
|
|
1392
|
+
// The anonymous function below ensures that we don't try to access
|
|
1393
|
+
// parseWidgetsMap before it's defined.
|
|
1157
1394
|
widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
|
|
1158
1395
|
widgetEnabled: optional(nullable(boolean)),
|
|
1159
1396
|
immutableWidgets: optional(nullable(boolean)),
|
|
@@ -1168,6 +1405,13 @@ const parseGradedGroupSetWidget = parseWidget(constant("graded-group-set"), obje
|
|
|
1168
1405
|
gradedGroups: array(parseGradedGroupWidgetOptions)
|
|
1169
1406
|
}));
|
|
1170
1407
|
|
|
1408
|
+
/**
|
|
1409
|
+
* discriminatedUnion() should be preferred over union() when parsing a
|
|
1410
|
+
* discriminated union type, because discriminatedUnion() produces more
|
|
1411
|
+
* understandable failure messages. It takes the discriminant as the source of
|
|
1412
|
+
* truth for which variant is to be parsed, and expects the other data to match
|
|
1413
|
+
* that variant.
|
|
1414
|
+
*/
|
|
1171
1415
|
function discriminatedUnionOn(discriminantKey) {
|
|
1172
1416
|
const noMoreBranches = (raw, ctx) => {
|
|
1173
1417
|
if (!isObject(raw)) {
|
|
@@ -1253,7 +1497,11 @@ const parseGrapherWidget = parseWidget(constant("grapher"), object({
|
|
|
1253
1497
|
})
|
|
1254
1498
|
}));
|
|
1255
1499
|
|
|
1256
|
-
const parseGroupWidget = parseWidget(constant("group"),
|
|
1500
|
+
const parseGroupWidget = parseWidget(constant("group"),
|
|
1501
|
+
// This module has an import cycle with parsePerseusRenderer.
|
|
1502
|
+
// The anonymous function below ensures that we don't try to access
|
|
1503
|
+
// parsePerseusRenderer before it's defined.
|
|
1504
|
+
(rawVal, ctx) => parsePerseusRenderer(rawVal, ctx));
|
|
1257
1505
|
|
|
1258
1506
|
const parseIframeWidget = parseWidget(constant("iframe"), object({
|
|
1259
1507
|
url: string,
|
|
@@ -1282,7 +1530,11 @@ const stringToNumber = (rawValue, ctx) => {
|
|
|
1282
1530
|
function emptyToZero(x) {
|
|
1283
1531
|
return x === "" ? 0 : x;
|
|
1284
1532
|
}
|
|
1285
|
-
const imageDimensionToNumber = pipeParsers(union(number).or(string).parser)
|
|
1533
|
+
const imageDimensionToNumber = pipeParsers(union(number).or(string).parser)
|
|
1534
|
+
// In this specific case, empty string is equivalent to zero. An empty
|
|
1535
|
+
// string parses to either NaN (using parseInt) or 0 (using unary +) and
|
|
1536
|
+
// CSS will treat NaN as invalid and default to 0 instead.
|
|
1537
|
+
.then(convert(emptyToZero)).then(stringToNumber).parser;
|
|
1286
1538
|
const dimensionOrUndefined = defaulted(imageDimensionToNumber, () => undefined);
|
|
1287
1539
|
const parsePerseusImageBackground = object({
|
|
1288
1540
|
url: optional(nullable(string)),
|
|
@@ -1323,6 +1575,10 @@ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
|
|
|
1323
1575
|
rightAlign: optional(boolean),
|
|
1324
1576
|
simplify: enumeration("required", "optional", "enforced"),
|
|
1325
1577
|
size: enumeration("normal", "small"),
|
|
1578
|
+
// TODO(benchristel): there are some content items where value is a
|
|
1579
|
+
// boolean, even though that makes no sense. We should figure out if
|
|
1580
|
+
// those content items are actually published anywhere, and consider
|
|
1581
|
+
// updating them.
|
|
1326
1582
|
value: union(number).or(string).or(booleanToString).parser,
|
|
1327
1583
|
customKeypad: optional(boolean)
|
|
1328
1584
|
}));
|
|
@@ -1456,8 +1712,270 @@ const parseInteractionWidget = parseWidget(constant("interaction"), object({
|
|
|
1456
1712
|
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)
|
|
1457
1713
|
}));
|
|
1458
1714
|
|
|
1459
|
-
|
|
1715
|
+
/**
|
|
1716
|
+
* The Perseus "data schema" file.
|
|
1717
|
+
*
|
|
1718
|
+
* This file, and the types in it, represents the "data schema" that Perseus
|
|
1719
|
+
* uses. The @khanacademy/perseus-editor package edits and produces objects
|
|
1720
|
+
* that conform to the types in this file. Similarly, the top-level renderers
|
|
1721
|
+
* in @khanacademy/perseus, consume objects that conform to these types.
|
|
1722
|
+
*
|
|
1723
|
+
* WARNING: This file should not import any types from elsewhere so that it is
|
|
1724
|
+
* easy to reason about changes that alter the Perseus schema. This helps
|
|
1725
|
+
* ensure that it is not changed accidentally when upgrading a dependant
|
|
1726
|
+
* package or other part of Perseus code. Note that TypeScript does type
|
|
1727
|
+
* checking via something called "structural typing". This means that as long
|
|
1728
|
+
* as the shape of a type matches, the name it goes by doesn't matter. As a
|
|
1729
|
+
* result, a `Coord` type that looks like this `[x: number, y: number]` is
|
|
1730
|
+
* _identical_, in TypeScript's eyes, to this `Vector2` type `[x: number, y:
|
|
1731
|
+
* number]`. Also, with tuples, the labels for each entry is ignored, so `[x:
|
|
1732
|
+
* number, y: number]` is compatible with `[min: number, max: number]`. The
|
|
1733
|
+
* labels are for humans, not TypeScript. :)
|
|
1734
|
+
*
|
|
1735
|
+
* If you make changes to types in this file, be very sure that:
|
|
1736
|
+
*
|
|
1737
|
+
* a) the changes are backwards compatible. If they are not, old data from
|
|
1738
|
+
* previous versions of the "schema" could become unrenderable, or worse,
|
|
1739
|
+
* introduce hard-to-diagnose bugs.
|
|
1740
|
+
* b) the parsing code (`util/parse-perseus-json/`) is updated to handle
|
|
1741
|
+
* the new format _as well as_ the old format.
|
|
1742
|
+
*/
|
|
1743
|
+
|
|
1744
|
+
// TODO(FEI-4010): Remove `Perseus` prefix for all types here
|
|
1745
|
+
|
|
1746
|
+
// Same name as Mafs
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* A utility type that constructs a widget map from a "registry interface".
|
|
1750
|
+
* The keys of the registry should be the widget type (aka, "categorizer" or
|
|
1751
|
+
* "radio", etc) and the value should be the option type stored in the value
|
|
1752
|
+
* of the map.
|
|
1753
|
+
*
|
|
1754
|
+
* You can think of this as a type that generates another type. We use
|
|
1755
|
+
* "registry interfaces" as a way to keep a set of widget types to their data
|
|
1756
|
+
* type in several places in Perseus. This type then allows us to generate a
|
|
1757
|
+
* map type that maps a widget id to its data type and keep strong typing by
|
|
1758
|
+
* widget id.
|
|
1759
|
+
*
|
|
1760
|
+
* For example, given a fictitious registry such as this:
|
|
1761
|
+
*
|
|
1762
|
+
* ```
|
|
1763
|
+
* interface DummyRegistry {
|
|
1764
|
+
* categorizer: { categories: ReadonlyArray<string> };
|
|
1765
|
+
* dropdown: { choices: ReadonlyArray<string> }:
|
|
1766
|
+
* }
|
|
1767
|
+
* ```
|
|
1768
|
+
*
|
|
1769
|
+
* If we create a DummyMap using this helper:
|
|
1770
|
+
*
|
|
1771
|
+
* ```
|
|
1772
|
+
* type DummyMap = MakeWidgetMap<DummyRegistry>;
|
|
1773
|
+
* ```
|
|
1774
|
+
*
|
|
1775
|
+
* We'll get a map that looks like this:
|
|
1776
|
+
*
|
|
1777
|
+
* ```
|
|
1778
|
+
* type DummyMap = {
|
|
1779
|
+
* `categorizer ${number}`: { categories: ReadonlyArray<string> };
|
|
1780
|
+
* `dropdown ${number}`: { choices: ReadonlyArray<string> };
|
|
1781
|
+
* }
|
|
1782
|
+
* ```
|
|
1783
|
+
*
|
|
1784
|
+
* We use interfaces for the registries so that they can be extended in cases
|
|
1785
|
+
* where the consuming app brings along their own widgets. Interfaces in
|
|
1786
|
+
* TypeScript are always open (ie. you can extend them) whereas types aren't.
|
|
1787
|
+
*/
|
|
1788
|
+
|
|
1789
|
+
/**
|
|
1790
|
+
* Our core set of Perseus widgets.
|
|
1791
|
+
*
|
|
1792
|
+
* This interface is the basis for "registering" all Perseus widget types.
|
|
1793
|
+
* There should be one key/value pair for each supported widget. If you create
|
|
1794
|
+
* a new widget, an entry should be added to this interface. Note that this
|
|
1795
|
+
* only registers the widget options type, you'll also need to register the
|
|
1796
|
+
* widget so that it's available at runtime (@see
|
|
1797
|
+
* {@link file://./widgets.ts#registerWidget}).
|
|
1798
|
+
*
|
|
1799
|
+
* Importantly, the key should be the name that is used in widget IDs. For most
|
|
1800
|
+
* widgets that is the same as the widget option's `type` field. In cases where
|
|
1801
|
+
* a widget has been deprecated and replaced with the deprecated-standin
|
|
1802
|
+
* widget, it should be the original widget type!
|
|
1803
|
+
*
|
|
1804
|
+
* If you define the widget outside of this package, you can still add the new
|
|
1805
|
+
* widget to this interface by writing the following in that package that
|
|
1806
|
+
* contains the widget. TypeScript will merge that definition of the
|
|
1807
|
+
* `PerseusWidgets` with the one defined below.
|
|
1808
|
+
*
|
|
1809
|
+
* ```typescript
|
|
1810
|
+
* declare module "@khanacademy/perseus-core" {
|
|
1811
|
+
* interface PerseusWidgetTypes {
|
|
1812
|
+
* // A new widget
|
|
1813
|
+
* "new-awesomeness": MyAwesomeNewWidget;
|
|
1814
|
+
*
|
|
1815
|
+
* // A deprecated widget
|
|
1816
|
+
* "super-old-widget": DeprecatedStandinWidget;
|
|
1817
|
+
* }
|
|
1818
|
+
* }
|
|
1819
|
+
*
|
|
1820
|
+
* // The new widget's options definition
|
|
1821
|
+
* type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>;
|
|
1822
|
+
*
|
|
1823
|
+
* // The deprecated widget's options definition
|
|
1824
|
+
* type SuperOldWidget = WidgetOptions<'super-old-widget', object>;
|
|
1825
|
+
* ```
|
|
1826
|
+
*
|
|
1827
|
+
* This interface can be extended through the magic of TypeScript "Declaration
|
|
1828
|
+
* merging". Specifically, we augment this module and extend this interface.
|
|
1829
|
+
*
|
|
1830
|
+
* @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
|
|
1831
|
+
*/
|
|
1832
|
+
|
|
1833
|
+
/**
|
|
1834
|
+
* A map of widget IDs to widget options. This is most often used as the type
|
|
1835
|
+
* for a set of widgets defined in a `PerseusItem` but can also be useful to
|
|
1836
|
+
* represent a function parameter where only `widgets` from a `PerseusItem` are
|
|
1837
|
+
* needed. Today Widget IDs are made up of the widget type and an incrementing
|
|
1838
|
+
* integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid
|
|
1839
|
+
* reading/parsing the widget id to derive any information from it, except in
|
|
1840
|
+
* the case of this map.
|
|
1841
|
+
*
|
|
1842
|
+
* @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
|
|
1843
|
+
* by augmenting the PerseusWidgetTypes with new widget types!
|
|
1844
|
+
*/
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* PerseusWidget is a union of all the different types of widget options that
|
|
1848
|
+
* Perseus knows about.
|
|
1849
|
+
*
|
|
1850
|
+
* Thanks to it being based on PerseusWidgetTypes interface, this union is
|
|
1851
|
+
* automatically extended to include widgets used in tests without those widget
|
|
1852
|
+
* option types seeping into our production types.
|
|
1853
|
+
*
|
|
1854
|
+
* @see MockWidget for an example
|
|
1855
|
+
*/
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* A "PerseusItem" is a classic Perseus item. It is rendered by the
|
|
1859
|
+
* `ServerItemRenderer` and the layout is pre-set.
|
|
1860
|
+
*
|
|
1861
|
+
* To render more complex Perseus items, see the `Item` type in the multi item
|
|
1862
|
+
* area.
|
|
1863
|
+
*/
|
|
1864
|
+
|
|
1865
|
+
/**
|
|
1866
|
+
* A "PerseusArticle" is an item that is meant to be rendered as an article.
|
|
1867
|
+
* This item is never scored and is rendered by the `ArticleRenderer`.
|
|
1868
|
+
*/
|
|
1869
|
+
|
|
1870
|
+
const ItemExtras = [
|
|
1871
|
+
// The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
|
|
1872
|
+
"calculator",
|
|
1873
|
+
// The user might benefit from using a statistics Chi Squared Table like https://people.richland.edu/james/lecture/m170/tbl-chi.html
|
|
1874
|
+
"chi2Table",
|
|
1875
|
+
// The user might benefit from a monthly payments calculator. Provided on Khan Academy when true
|
|
1876
|
+
"financialCalculatorMonthlyPayment",
|
|
1877
|
+
// The user might benefit from a total amount calculator. Provided on Khan Academy when true
|
|
1878
|
+
"financialCalculatorTotalAmount",
|
|
1879
|
+
// The user might benefit from a time to pay off calculator. Provided on Khan Academy when true
|
|
1880
|
+
"financialCalculatorTimeToPayOff",
|
|
1881
|
+
// The user might benefit from using a Periodic Table of Elements. Provided on Khan Academy when true
|
|
1882
|
+
"periodicTable",
|
|
1883
|
+
// The user might benefit from using a Periodic Table of Elements with key. Provided on Khan Academy when true
|
|
1884
|
+
"periodicTableWithKey",
|
|
1885
|
+
// The user might benefit from using a statistics T Table like https://www.statisticshowto.com/tables/t-distribution-table/
|
|
1886
|
+
"tTable",
|
|
1887
|
+
// The user might benefit from using a statistics Z Table like https://www.ztable.net/
|
|
1888
|
+
"zTable"];
|
|
1889
|
+
|
|
1890
|
+
/**
|
|
1891
|
+
* The type representing the common structure of all widget's options. The
|
|
1892
|
+
* `Options` generic type represents the widget-specific option data.
|
|
1893
|
+
*/
|
|
1894
|
+
|
|
1895
|
+
// prettier-ignore
|
|
1896
|
+
|
|
1897
|
+
// prettier-ignore
|
|
1898
|
+
|
|
1899
|
+
// prettier-ignore
|
|
1900
|
+
|
|
1901
|
+
// prettier-ignore
|
|
1902
|
+
|
|
1903
|
+
// prettier-ignore
|
|
1904
|
+
|
|
1905
|
+
// prettier-ignore
|
|
1906
|
+
|
|
1907
|
+
// prettier-ignore
|
|
1908
|
+
|
|
1909
|
+
// prettier-ignore
|
|
1910
|
+
|
|
1911
|
+
// prettier-ignore
|
|
1912
|
+
|
|
1913
|
+
// prettier-ignore
|
|
1914
|
+
|
|
1915
|
+
// prettier-ignore
|
|
1916
|
+
|
|
1917
|
+
// prettier-ignore
|
|
1918
|
+
|
|
1919
|
+
// prettier-ignore
|
|
1920
|
+
|
|
1921
|
+
// prettier-ignore
|
|
1922
|
+
|
|
1923
|
+
// prettier-ignore
|
|
1924
|
+
|
|
1925
|
+
// prettier-ignore
|
|
1926
|
+
|
|
1927
|
+
// prettier-ignore
|
|
1928
|
+
|
|
1929
|
+
// prettier-ignore
|
|
1930
|
+
|
|
1931
|
+
// prettier-ignore
|
|
1932
|
+
|
|
1933
|
+
// prettier-ignore
|
|
1934
|
+
|
|
1935
|
+
// prettier-ignore
|
|
1936
|
+
|
|
1937
|
+
// prettier-ignore
|
|
1938
|
+
|
|
1939
|
+
// prettier-ignore
|
|
1940
|
+
|
|
1941
|
+
// prettier-ignore
|
|
1942
|
+
|
|
1943
|
+
// prettier-ignore
|
|
1944
|
+
|
|
1945
|
+
// prettier-ignore
|
|
1946
|
+
|
|
1947
|
+
// prettier-ignore
|
|
1948
|
+
|
|
1949
|
+
// prettier-ignore
|
|
1950
|
+
|
|
1951
|
+
// prettier-ignore
|
|
1952
|
+
|
|
1953
|
+
// prettier-ignore
|
|
1954
|
+
|
|
1955
|
+
// prettier-ignore
|
|
1956
|
+
|
|
1957
|
+
// prettier-ignore
|
|
1958
|
+
|
|
1959
|
+
// prettier-ignore
|
|
1960
|
+
|
|
1961
|
+
//prettier-ignore
|
|
1962
|
+
|
|
1963
|
+
/**
|
|
1964
|
+
* A background image applied to various widgets.
|
|
1965
|
+
*/
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* The type of markings to display on the graph.
|
|
1969
|
+
* - axes: shows the axes without the gride lines
|
|
1970
|
+
* - graph: shows the axes and the grid lines
|
|
1971
|
+
* - grid: shows only the grid lines
|
|
1972
|
+
* - none: shows no markings
|
|
1973
|
+
*/
|
|
1974
|
+
|
|
1460
1975
|
const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
|
|
1976
|
+
|
|
1977
|
+
// 2D range: xMin, xMax, yMin, yMax
|
|
1978
|
+
|
|
1461
1979
|
const lockedFigureColorNames = ["blue", "green", "grayH", "purple", "pink", "orange", "red"];
|
|
1462
1980
|
const lockedFigureColors = {
|
|
1463
1981
|
blue: "#3D7586",
|
|
@@ -1474,8 +1992,21 @@ const lockedFigureFillStyles = {
|
|
|
1474
1992
|
translucent: 0.4,
|
|
1475
1993
|
solid: 1
|
|
1476
1994
|
};
|
|
1995
|
+
|
|
1996
|
+
// Not associated with a specific figure
|
|
1997
|
+
|
|
1998
|
+
/**
|
|
1999
|
+
* Determines how unsimplified fractions are scored.
|
|
2000
|
+
*
|
|
2001
|
+
* - "required" means unsimplified fractions are considered invalid input, and
|
|
2002
|
+
* the learner can try again.
|
|
2003
|
+
* - "enforced" means unsimplified fractions are marked incorrect.
|
|
2004
|
+
* - "optional" means unsimplified fractions are accepted.
|
|
2005
|
+
*/
|
|
2006
|
+
|
|
1477
2007
|
const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
|
|
1478
2008
|
|
|
2009
|
+
// Used to represent 2-D points and ranges
|
|
1479
2010
|
const pairOfNumbers = pair(number, number);
|
|
1480
2011
|
const parsePerseusGraphTypeAngle = object({
|
|
1481
2012
|
type: constant("angle"),
|
|
@@ -1495,18 +2026,22 @@ const parsePerseusGraphTypeCircle = object({
|
|
|
1495
2026
|
center: pairOfNumbers,
|
|
1496
2027
|
radius: number
|
|
1497
2028
|
})),
|
|
2029
|
+
// TODO: remove coord? it's legacy.
|
|
1498
2030
|
coord: optional(pairOfNumbers)
|
|
1499
2031
|
});
|
|
1500
2032
|
const parsePerseusGraphTypeLinear = object({
|
|
1501
2033
|
type: constant("linear"),
|
|
1502
2034
|
coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
|
|
1503
2035
|
startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
|
|
2036
|
+
// TODO: remove coord? it's legacy.
|
|
1504
2037
|
coord: optional(pairOfNumbers)
|
|
1505
2038
|
});
|
|
1506
2039
|
const parsePerseusGraphTypeLinearSystem = object({
|
|
1507
2040
|
type: constant("linear-system"),
|
|
2041
|
+
// TODO(benchristel): default coords to empty array?
|
|
1508
2042
|
coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
|
|
1509
2043
|
startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
|
|
2044
|
+
// TODO: remove coord? it's legacy.
|
|
1510
2045
|
coord: optional(pairOfNumbers)
|
|
1511
2046
|
});
|
|
1512
2047
|
const parsePerseusGraphTypeNone = object({
|
|
@@ -1517,6 +2052,7 @@ const parsePerseusGraphTypePoint = object({
|
|
|
1517
2052
|
numPoints: optional(union(number).or(constant("unlimited")).parser),
|
|
1518
2053
|
coords: optional(nullable(array(pairOfNumbers))),
|
|
1519
2054
|
startCoords: optional(array(pairOfNumbers)),
|
|
2055
|
+
// TODO: remove coord? it's legacy.
|
|
1520
2056
|
coord: optional(pairOfNumbers)
|
|
1521
2057
|
});
|
|
1522
2058
|
const parsePerseusGraphTypePolygon = object({
|
|
@@ -1527,31 +2063,37 @@ const parsePerseusGraphTypePolygon = object({
|
|
|
1527
2063
|
snapTo: optional(enumeration("grid", "angles", "sides")),
|
|
1528
2064
|
match: optional(enumeration("similar", "congruent", "approx", "exact")),
|
|
1529
2065
|
startCoords: optional(array(pairOfNumbers)),
|
|
2066
|
+
// TODO: remove coord? it's legacy.
|
|
1530
2067
|
coord: optional(pairOfNumbers)
|
|
1531
2068
|
});
|
|
1532
2069
|
const parsePerseusGraphTypeQuadratic = object({
|
|
1533
2070
|
type: constant("quadratic"),
|
|
1534
2071
|
coords: optional(nullable(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))),
|
|
1535
2072
|
startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
|
|
2073
|
+
// TODO: remove coord? it's legacy.
|
|
1536
2074
|
coord: optional(pairOfNumbers)
|
|
1537
2075
|
});
|
|
1538
2076
|
const parsePerseusGraphTypeRay = object({
|
|
1539
2077
|
type: constant("ray"),
|
|
1540
2078
|
coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
|
|
1541
2079
|
startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
|
|
2080
|
+
// TODO: remove coord? it's legacy.
|
|
1542
2081
|
coord: optional(pairOfNumbers)
|
|
1543
2082
|
});
|
|
1544
2083
|
const parsePerseusGraphTypeSegment = object({
|
|
1545
2084
|
type: constant("segment"),
|
|
2085
|
+
// TODO(benchristel): default numSegments?
|
|
1546
2086
|
numSegments: optional(number),
|
|
1547
2087
|
coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
|
|
1548
2088
|
startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
|
|
2089
|
+
// TODO: remove coord? it's legacy.
|
|
1549
2090
|
coord: optional(pairOfNumbers)
|
|
1550
2091
|
});
|
|
1551
2092
|
const parsePerseusGraphTypeSinusoid = object({
|
|
1552
2093
|
type: constant("sinusoid"),
|
|
1553
2094
|
coords: optional(nullable(array(pairOfNumbers))),
|
|
1554
2095
|
startCoords: optional(array(pairOfNumbers)),
|
|
2096
|
+
// TODO: remove coord? it's legacy.
|
|
1555
2097
|
coord: optional(pairOfNumbers)
|
|
1556
2098
|
});
|
|
1557
2099
|
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;
|
|
@@ -1570,6 +2112,7 @@ const parseLockedPointType = object({
|
|
|
1570
2112
|
coord: pairOfNumbers,
|
|
1571
2113
|
color: parseLockedFigureColor,
|
|
1572
2114
|
filled: boolean,
|
|
2115
|
+
// TODO(benchristel): default labels to empty array?
|
|
1573
2116
|
labels: optional(array(parseLockedLabelType)),
|
|
1574
2117
|
ariaLabel: optional(string)
|
|
1575
2118
|
});
|
|
@@ -1581,6 +2124,7 @@ const parseLockedLineType = object({
|
|
|
1581
2124
|
lineStyle: parseLockedLineStyle,
|
|
1582
2125
|
showPoint1: defaulted(boolean, () => false),
|
|
1583
2126
|
showPoint2: defaulted(boolean, () => false),
|
|
2127
|
+
// TODO(benchristel): default labels to empty array?
|
|
1584
2128
|
labels: optional(array(parseLockedLabelType)),
|
|
1585
2129
|
ariaLabel: optional(string)
|
|
1586
2130
|
});
|
|
@@ -1588,6 +2132,7 @@ const parseLockedVectorType = object({
|
|
|
1588
2132
|
type: constant("vector"),
|
|
1589
2133
|
points: pair(pairOfNumbers, pairOfNumbers),
|
|
1590
2134
|
color: parseLockedFigureColor,
|
|
2135
|
+
// TODO(benchristel): default labels to empty array?
|
|
1591
2136
|
labels: optional(array(parseLockedLabelType)),
|
|
1592
2137
|
ariaLabel: optional(string)
|
|
1593
2138
|
});
|
|
@@ -1599,6 +2144,7 @@ const parseLockedEllipseType = object({
|
|
|
1599
2144
|
color: parseLockedFigureColor,
|
|
1600
2145
|
fillStyle: parseLockedFigureFillType,
|
|
1601
2146
|
strokeStyle: parseLockedLineStyle,
|
|
2147
|
+
// TODO(benchristel): default labels to empty array?
|
|
1602
2148
|
labels: optional(array(parseLockedLabelType)),
|
|
1603
2149
|
ariaLabel: optional(string)
|
|
1604
2150
|
});
|
|
@@ -1609,9 +2155,12 @@ const parseLockedPolygonType = object({
|
|
|
1609
2155
|
showVertices: boolean,
|
|
1610
2156
|
fillStyle: parseLockedFigureFillType,
|
|
1611
2157
|
strokeStyle: parseLockedLineStyle,
|
|
2158
|
+
// TODO(benchristel): default labels to empty array?
|
|
1612
2159
|
labels: optional(array(parseLockedLabelType)),
|
|
1613
2160
|
ariaLabel: optional(string)
|
|
1614
2161
|
});
|
|
2162
|
+
|
|
2163
|
+
// Exported for testing.
|
|
1615
2164
|
const parseLockedFunctionDomain = defaulted(pair(defaulted(number, () => -Infinity), defaulted(number, () => Infinity)), () => [-Infinity, Infinity]);
|
|
1616
2165
|
const parseLockedFunctionType = object({
|
|
1617
2166
|
type: constant("function"),
|
|
@@ -1620,12 +2169,17 @@ const parseLockedFunctionType = object({
|
|
|
1620
2169
|
equation: string,
|
|
1621
2170
|
directionalAxis: enumeration("x", "y"),
|
|
1622
2171
|
domain: parseLockedFunctionDomain,
|
|
2172
|
+
// TODO(benchristel): default labels to empty array?
|
|
1623
2173
|
labels: optional(array(parseLockedLabelType)),
|
|
1624
2174
|
ariaLabel: optional(string)
|
|
1625
2175
|
});
|
|
1626
2176
|
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;
|
|
1627
2177
|
const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
|
|
1628
2178
|
step: pairOfNumbers,
|
|
2179
|
+
// TODO(benchristel): rather than making gridStep and snapStep
|
|
2180
|
+
// optional, we should duplicate the defaulting logic from the
|
|
2181
|
+
// InteractiveGraph component. See parse-perseus-json/README.md for
|
|
2182
|
+
// why.
|
|
1629
2183
|
gridStep: optional(pairOfNumbers),
|
|
1630
2184
|
snapStep: optional(pairOfNumbers),
|
|
1631
2185
|
backgroundImage: optional(parsePerseusImageBackground),
|
|
@@ -1637,10 +2191,14 @@ const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), o
|
|
|
1637
2191
|
rulerLabel: optional(string),
|
|
1638
2192
|
rulerTicks: optional(number),
|
|
1639
2193
|
range: pair(pairOfNumbers, pairOfNumbers),
|
|
2194
|
+
// NOTE(benchristel): I copied the default graph from
|
|
2195
|
+
// interactive-graph.tsx. See the parse-perseus-json/README.md for
|
|
2196
|
+
// an explanation of why we want to duplicate the default here.
|
|
1640
2197
|
graph: defaulted(parsePerseusGraphType, () => ({
|
|
1641
2198
|
type: "linear"
|
|
1642
2199
|
})),
|
|
1643
2200
|
correct: parsePerseusGraphType,
|
|
2201
|
+
// TODO(benchristel): default lockedFigures to empty array
|
|
1644
2202
|
lockedFigures: optional(array(parseLockedFigure)),
|
|
1645
2203
|
fullGraphLabel: optional(string),
|
|
1646
2204
|
fullGraphAriaDescription: optional(string)
|
|
@@ -1683,6 +2241,9 @@ const parseMatrixWidget = parseWidget(defaulted(constant("matrix"), () => "matri
|
|
|
1683
2241
|
}));
|
|
1684
2242
|
|
|
1685
2243
|
const parseMeasurerWidget = parseWidget(constant("measurer"), object({
|
|
2244
|
+
// The default value for image comes from measurer.tsx.
|
|
2245
|
+
// See parse-perseus-json/README.md for why we want to duplicate the
|
|
2246
|
+
// defaults here.
|
|
1686
2247
|
image: defaulted(parsePerseusImageBackground, () => ({
|
|
1687
2248
|
url: null,
|
|
1688
2249
|
top: 0,
|
|
@@ -1695,6 +2256,7 @@ const parseMeasurerWidget = parseWidget(constant("measurer"), object({
|
|
|
1695
2256
|
rulerPixels: number,
|
|
1696
2257
|
rulerLength: number,
|
|
1697
2258
|
box: pair(number, number),
|
|
2259
|
+
// TODO(benchristel): static is not used. Remove it?
|
|
1698
2260
|
static: defaulted(boolean, () => false)
|
|
1699
2261
|
}));
|
|
1700
2262
|
|
|
@@ -1713,6 +2275,9 @@ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
|
|
|
1713
2275
|
isTickCtrl: optional(nullable(boolean)),
|
|
1714
2276
|
divisionRange: array(number),
|
|
1715
2277
|
numDivisions: optional(nullable(number)),
|
|
2278
|
+
// NOTE(benchristel): I copied the default snapDivisions from
|
|
2279
|
+
// number-line.tsx. See the parse-perseus-json/README.md for
|
|
2280
|
+
// an explanation of why we want to duplicate the default here.
|
|
1716
2281
|
snapDivisions: defaulted(number, () => 2),
|
|
1717
2282
|
tickStep: optional(nullable(number)),
|
|
1718
2283
|
correctRel: optional(nullable(string)),
|
|
@@ -1730,6 +2295,10 @@ function deprecatedSimplifyValuesToRequired(simplify) {
|
|
|
1730
2295
|
case "required":
|
|
1731
2296
|
case "optional":
|
|
1732
2297
|
return simplify;
|
|
2298
|
+
// NOTE(benchristel): "accepted", "correct", true, false, undefined, and
|
|
2299
|
+
// null are all treated the same as "required" during scoring, so we
|
|
2300
|
+
// convert them to "required" here to preserve behavior. See the tests
|
|
2301
|
+
// in score-numeric-input.test.ts
|
|
1733
2302
|
default:
|
|
1734
2303
|
return "required";
|
|
1735
2304
|
}
|
|
@@ -1737,11 +2306,17 @@ function deprecatedSimplifyValuesToRequired(simplify) {
|
|
|
1737
2306
|
const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
|
|
1738
2307
|
answers: array(object({
|
|
1739
2308
|
message: string,
|
|
2309
|
+
// TODO(benchristel): value should never be null or undefined,
|
|
2310
|
+
// but we have some content where it is anyway. If we backfill
|
|
2311
|
+
// the data, simplify this.
|
|
1740
2312
|
value: optional(nullable(number)),
|
|
1741
2313
|
status: string,
|
|
1742
2314
|
answerForms: defaulted(array(parseMathFormat), () => undefined),
|
|
1743
2315
|
strict: boolean,
|
|
1744
2316
|
maxError: optional(nullable(number)),
|
|
2317
|
+
// TODO(benchristel): simplify should never be a boolean, but we
|
|
2318
|
+
// have some content where it is anyway. If we ever backfill
|
|
2319
|
+
// the data, we should simplify `simplify`.
|
|
1745
2320
|
simplify: parseSimplify
|
|
1746
2321
|
})),
|
|
1747
2322
|
labelText: optional(string),
|
|
@@ -1755,6 +2330,9 @@ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
|
|
|
1755
2330
|
})))
|
|
1756
2331
|
}));
|
|
1757
2332
|
|
|
2333
|
+
// There is an import cycle between orderer-widget.ts and perseus-renderer.ts.
|
|
2334
|
+
// This wrapper ensures that we don't refer to parsePerseusRenderer before
|
|
2335
|
+
// it's defined.
|
|
1758
2336
|
function parseRenderer(rawValue, ctx) {
|
|
1759
2337
|
return parsePerseusRenderer(rawValue, ctx);
|
|
1760
2338
|
}
|
|
@@ -1796,14 +2374,23 @@ const parsePlotterWidget = parseWidget(constant("plotter"), object({
|
|
|
1796
2374
|
categories: array(string),
|
|
1797
2375
|
type: enumeration(...plotterPlotTypes),
|
|
1798
2376
|
maxY: number,
|
|
2377
|
+
// The default value for scaleY comes from plotter.tsx.
|
|
2378
|
+
// See parse-perseus-json/README.md for why we want to duplicate the
|
|
2379
|
+
// defaults here.
|
|
1799
2380
|
scaleY: defaulted(number, () => 1),
|
|
1800
2381
|
labelInterval: optional(nullable(number)),
|
|
2382
|
+
// The default value for snapsPerLine comes from plotter.tsx.
|
|
2383
|
+
// See parse-perseus-json/README.md for why we want to duplicate the
|
|
2384
|
+
// defaults here.
|
|
1801
2385
|
snapsPerLine: defaulted(number, () => 2),
|
|
1802
2386
|
starting: array(number),
|
|
1803
2387
|
correct: array(number),
|
|
1804
2388
|
picUrl: optional(nullable(string)),
|
|
1805
2389
|
picSize: optional(nullable(number)),
|
|
1806
2390
|
picBoxHeight: optional(nullable(number)),
|
|
2391
|
+
// NOTE(benchristel): I copied the default plotDimensions from
|
|
2392
|
+
// plotter.tsx. See the parse-perseus-json/README.md for an explanation
|
|
2393
|
+
// of why we want to duplicate the defaults here.
|
|
1807
2394
|
plotDimensions: defaulted(array(number), () => [380, 300])
|
|
1808
2395
|
}));
|
|
1809
2396
|
|
|
@@ -1812,7 +2399,6 @@ const parsePythonProgramWidget = parseWidget(constant("python-program"), object(
|
|
|
1812
2399
|
height: number
|
|
1813
2400
|
}));
|
|
1814
2401
|
|
|
1815
|
-
const _excluded$a = ["noneOfTheAbove"];
|
|
1816
2402
|
const currentVersion$3 = {
|
|
1817
2403
|
major: 2,
|
|
1818
2404
|
minor: 0
|
|
@@ -1822,26 +2408,28 @@ function deriveNumCorrect(options) {
|
|
|
1822
2408
|
choices,
|
|
1823
2409
|
numCorrect
|
|
1824
2410
|
} = options;
|
|
1825
|
-
return numCorrect
|
|
2411
|
+
return numCorrect ?? choices.filter(c => c.correct).length;
|
|
1826
2412
|
}
|
|
1827
2413
|
const widgetOptionsUpgrades$2 = {
|
|
1828
2414
|
"2": v1props => {
|
|
1829
|
-
const upgraded =
|
|
2415
|
+
const upgraded = {
|
|
2416
|
+
...v1props,
|
|
1830
2417
|
numCorrect: deriveNumCorrect(v1props)
|
|
1831
|
-
}
|
|
2418
|
+
};
|
|
1832
2419
|
return upgraded;
|
|
1833
2420
|
},
|
|
1834
2421
|
"1": v0props => {
|
|
1835
2422
|
const {
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2423
|
+
noneOfTheAbove,
|
|
2424
|
+
...rest
|
|
2425
|
+
} = v0props;
|
|
1839
2426
|
if (noneOfTheAbove) {
|
|
1840
2427
|
throw new Error("radio widget v0 no longer supports auto noneOfTheAbove");
|
|
1841
2428
|
}
|
|
1842
|
-
return
|
|
2429
|
+
return {
|
|
2430
|
+
...rest,
|
|
1843
2431
|
hasNoneOfTheAbove: false
|
|
1844
|
-
}
|
|
2432
|
+
};
|
|
1845
2433
|
}
|
|
1846
2434
|
};
|
|
1847
2435
|
const defaultWidgetOptions$v = {
|
|
@@ -1854,7 +2442,6 @@ const defaultWidgetOptions$v = {
|
|
|
1854
2442
|
deselectEnabled: false
|
|
1855
2443
|
};
|
|
1856
2444
|
|
|
1857
|
-
const _excluded$9 = ["noneOfTheAbove"];
|
|
1858
2445
|
const version2 = optional(object({
|
|
1859
2446
|
major: constant(2),
|
|
1860
2447
|
minor: number
|
|
@@ -1866,6 +2453,10 @@ const parseRadioWidgetV2 = parseWidgetWithVersion(version2, constant("radio"), o
|
|
|
1866
2453
|
clue: optional(string),
|
|
1867
2454
|
correct: optional(boolean),
|
|
1868
2455
|
isNoneOfTheAbove: optional(boolean),
|
|
2456
|
+
// deprecated
|
|
2457
|
+
// There is an import cycle between radio-widget.ts and
|
|
2458
|
+
// widgets-map.ts. The anonymous function below ensures that we
|
|
2459
|
+
// don't refer to parseWidgetsMap before it's defined.
|
|
1869
2460
|
widgets: optional((rawVal, ctx) => parseWidgetsMap(rawVal, ctx))
|
|
1870
2461
|
})),
|
|
1871
2462
|
hasNoneOfTheAbove: optional(boolean),
|
|
@@ -1873,8 +2464,12 @@ const parseRadioWidgetV2 = parseWidgetWithVersion(version2, constant("radio"), o
|
|
|
1873
2464
|
randomize: optional(boolean),
|
|
1874
2465
|
multipleSelect: optional(boolean),
|
|
1875
2466
|
deselectEnabled: optional(boolean),
|
|
2467
|
+
// deprecated
|
|
1876
2468
|
onePerLine: optional(boolean),
|
|
2469
|
+
// deprecated
|
|
1877
2470
|
displayCount: optional(any),
|
|
2471
|
+
// v0 props
|
|
2472
|
+
// `noneOfTheAbove` is still in use (but only set to `false`).
|
|
1878
2473
|
noneOfTheAbove: optional(constant(false))
|
|
1879
2474
|
}));
|
|
1880
2475
|
const version1 = optional(object({
|
|
@@ -1887,6 +2482,10 @@ const parseRadioWidgetV1 = parseWidgetWithVersion(version1, constant("radio"), o
|
|
|
1887
2482
|
clue: optional(string),
|
|
1888
2483
|
correct: optional(boolean),
|
|
1889
2484
|
isNoneOfTheAbove: optional(boolean),
|
|
2485
|
+
// deprecated
|
|
2486
|
+
// There is an import cycle between radio-widget.ts and
|
|
2487
|
+
// widgets-map.ts. The anonymous function below ensures that we
|
|
2488
|
+
// don't refer to parseWidgetsMap before it's defined.
|
|
1890
2489
|
widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => undefined)
|
|
1891
2490
|
})),
|
|
1892
2491
|
hasNoneOfTheAbove: optional(boolean),
|
|
@@ -1894,23 +2493,29 @@ const parseRadioWidgetV1 = parseWidgetWithVersion(version1, constant("radio"), o
|
|
|
1894
2493
|
randomize: optional(boolean),
|
|
1895
2494
|
multipleSelect: optional(boolean),
|
|
1896
2495
|
deselectEnabled: optional(boolean),
|
|
2496
|
+
// deprecated
|
|
1897
2497
|
onePerLine: optional(boolean),
|
|
2498
|
+
// deprecated
|
|
1898
2499
|
displayCount: optional(any),
|
|
2500
|
+
// v0 props
|
|
2501
|
+
// `noneOfTheAbove` is still in use (but only set to `false`).
|
|
1899
2502
|
noneOfTheAbove: optional(constant(false))
|
|
1900
2503
|
}));
|
|
1901
2504
|
function migrateV1ToV2(widget) {
|
|
1902
2505
|
const {
|
|
1903
2506
|
options
|
|
1904
2507
|
} = widget;
|
|
1905
|
-
return
|
|
2508
|
+
return {
|
|
2509
|
+
...widget,
|
|
1906
2510
|
version: {
|
|
1907
2511
|
major: 2,
|
|
1908
2512
|
minor: 0
|
|
1909
2513
|
},
|
|
1910
|
-
options:
|
|
2514
|
+
options: {
|
|
2515
|
+
...options,
|
|
1911
2516
|
numCorrect: deriveNumCorrect(options)
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
2517
|
+
}
|
|
2518
|
+
};
|
|
1914
2519
|
}
|
|
1915
2520
|
const version0 = optional(object({
|
|
1916
2521
|
major: constant(0),
|
|
@@ -1922,6 +2527,10 @@ const parseRadioWidgetV0 = parseWidgetWithVersion(version0, constant("radio"), o
|
|
|
1922
2527
|
clue: optional(string),
|
|
1923
2528
|
correct: optional(boolean),
|
|
1924
2529
|
isNoneOfTheAbove: optional(boolean),
|
|
2530
|
+
// deprecated
|
|
2531
|
+
// There is an import cycle between radio-widget.ts and
|
|
2532
|
+
// widgets-map.ts. The anonymous function below ensures that we
|
|
2533
|
+
// don't refer to parseWidgetsMap before it's defined.
|
|
1925
2534
|
widgets: optional((rawVal, ctx) => parseWidgetsMap(rawVal, ctx))
|
|
1926
2535
|
})),
|
|
1927
2536
|
hasNoneOfTheAbove: optional(boolean),
|
|
@@ -1929,24 +2538,33 @@ const parseRadioWidgetV0 = parseWidgetWithVersion(version0, constant("radio"), o
|
|
|
1929
2538
|
randomize: optional(boolean),
|
|
1930
2539
|
multipleSelect: optional(boolean),
|
|
1931
2540
|
deselectEnabled: optional(boolean),
|
|
2541
|
+
// deprecated
|
|
1932
2542
|
onePerLine: optional(boolean),
|
|
2543
|
+
// deprecated
|
|
1933
2544
|
displayCount: optional(any),
|
|
2545
|
+
// v0 props
|
|
2546
|
+
// `noneOfTheAbove` is still in use (but only set to `false`).
|
|
1934
2547
|
noneOfTheAbove: optional(constant(false))
|
|
1935
2548
|
}));
|
|
1936
2549
|
function migrateV0ToV1(widget) {
|
|
1937
2550
|
const {
|
|
1938
2551
|
options
|
|
1939
2552
|
} = widget;
|
|
1940
|
-
const
|
|
1941
|
-
|
|
2553
|
+
const {
|
|
2554
|
+
noneOfTheAbove: _,
|
|
2555
|
+
...rest
|
|
2556
|
+
} = options;
|
|
2557
|
+
return {
|
|
2558
|
+
...widget,
|
|
1942
2559
|
version: {
|
|
1943
2560
|
major: 1,
|
|
1944
2561
|
minor: 0
|
|
1945
2562
|
},
|
|
1946
|
-
options:
|
|
2563
|
+
options: {
|
|
2564
|
+
...rest,
|
|
1947
2565
|
hasNoneOfTheAbove: false
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
2566
|
+
}
|
|
2567
|
+
};
|
|
1950
2568
|
}
|
|
1951
2569
|
const parseRadioWidget = versionedWidgetOptions(2, parseRadioWidgetV2).withMigrationFrom(1, parseRadioWidgetV1, migrateV1ToV2).withMigrationFrom(0, parseRadioWidgetV0, migrateV0ToV1).parser;
|
|
1952
2570
|
|
|
@@ -1974,6 +2592,9 @@ const parseWidgetsMap = (rawValue, ctx) => {
|
|
|
1974
2592
|
}
|
|
1975
2593
|
const widgetsMap = {};
|
|
1976
2594
|
for (const key of Object.keys(rawValue)) {
|
|
2595
|
+
// parseWidgetsMapEntry modifies the widgetsMap. This is kind of gross,
|
|
2596
|
+
// but it's the only way I could find to make TypeScript check the key
|
|
2597
|
+
// against the widget type.
|
|
1977
2598
|
const entryResult = parseWidgetsMapEntry([key, rawValue[key]], widgetsMap, ctx.forSubtree(key));
|
|
1978
2599
|
if (isFailure(entryResult)) {
|
|
1979
2600
|
return entryResult;
|
|
@@ -1981,7 +2602,8 @@ const parseWidgetsMap = (rawValue, ctx) => {
|
|
|
1981
2602
|
}
|
|
1982
2603
|
return ctx.success(widgetsMap);
|
|
1983
2604
|
};
|
|
1984
|
-
const parseWidgetsMapEntry = (
|
|
2605
|
+
const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
|
|
2606
|
+
let [id, widget] = _ref;
|
|
1985
2607
|
const idComponentsResult = parseWidgetIdComponents(id.split(" "), ctx.forSubtree("(widget ID)"));
|
|
1986
2608
|
if (isFailure(idComponentsResult)) {
|
|
1987
2609
|
return idComponentsResult;
|
|
@@ -2047,6 +2669,9 @@ const parseWidgetsMapEntry = ([id, widget], widgetMap, ctx) => {
|
|
|
2047
2669
|
case "passage-ref":
|
|
2048
2670
|
return parseAndAssign(`passage-ref ${n}`, parsePassageRefWidget);
|
|
2049
2671
|
case "passage-ref-target":
|
|
2672
|
+
// NOTE(benchristel): as of 2024-11-12, passage-ref-target is only
|
|
2673
|
+
// used in test content. See:
|
|
2674
|
+
// https://www.khanacademy.org/devadmin/content/search?query=widget:passage-ref-target
|
|
2050
2675
|
return parseAndAssign(`passage-ref-target ${n}`, any);
|
|
2051
2676
|
case "phet-simulation":
|
|
2052
2677
|
return parseAndAssign(`phet-simulation ${n}`, parsePhetSimulationWidget);
|
|
@@ -2063,6 +2688,8 @@ const parseWidgetsMapEntry = ([id, widget], widgetMap, ctx) => {
|
|
|
2063
2688
|
case "video":
|
|
2064
2689
|
return parseAndAssign(`video ${n}`, parseVideoWidget);
|
|
2065
2690
|
case "sequence":
|
|
2691
|
+
// sequence is a deprecated widget type, and the corresponding
|
|
2692
|
+
// widget component no longer exists.
|
|
2066
2693
|
return parseAndAssign(`sequence ${n}`, parseDeprecatedWidget);
|
|
2067
2694
|
case "lights-puzzle":
|
|
2068
2695
|
return parseAndAssign(`lights-puzzle ${n}`, parseDeprecatedWidget);
|
|
@@ -2074,8 +2701,16 @@ const parseWidgetsMapEntry = ([id, widget], widgetMap, ctx) => {
|
|
|
2074
2701
|
return parseAndAssign(`${type} ${n}`, parseWidget(constant(type), any));
|
|
2075
2702
|
}
|
|
2076
2703
|
};
|
|
2077
|
-
const parseDeprecatedWidget = parseWidget(
|
|
2704
|
+
const parseDeprecatedWidget = parseWidget(
|
|
2705
|
+
// Ignore the incoming widget type and hardcode "deprecated-standin"
|
|
2706
|
+
(_, ctx) => ctx.success("deprecated-standin"),
|
|
2707
|
+
// Allow any widget options
|
|
2708
|
+
object({}));
|
|
2078
2709
|
const parseStringToNonNegativeInt = (rawValue, ctx) => {
|
|
2710
|
+
// The article renderer seems to allow the numeric part of a widget ID to
|
|
2711
|
+
// be 0, at least for image widgets. However, if widget IDs in an exercise
|
|
2712
|
+
// contain 0, the exercise renderer will blow up. We allow 0 here for
|
|
2713
|
+
// compatibility with articles.
|
|
2079
2714
|
if (typeof rawValue !== "string" || !/^(0|[1-9][0-9]*)$/.test(rawValue)) {
|
|
2080
2715
|
return ctx.failure("a string representing a non-negative integer", rawValue);
|
|
2081
2716
|
}
|
|
@@ -2084,11 +2719,20 @@ const parseStringToNonNegativeInt = (rawValue, ctx) => {
|
|
|
2084
2719
|
const parseWidgetIdComponents = pair(string, parseStringToNonNegativeInt);
|
|
2085
2720
|
|
|
2086
2721
|
const parsePerseusRenderer = defaulted(object({
|
|
2722
|
+
// TODO(benchristel): content is also defaulted to empty string in
|
|
2723
|
+
// renderer.tsx. See if we can remove one default or the other.
|
|
2087
2724
|
content: defaulted(string, () => ""),
|
|
2725
|
+
// This module has an import cycle with parseWidgetsMap, because the
|
|
2726
|
+
// `group` widget can contain another renderer.
|
|
2727
|
+
// The anonymous function below ensures that we don't try to access
|
|
2728
|
+
// parseWidgetsMap before it's defined.
|
|
2088
2729
|
widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
|
|
2089
2730
|
images: parseImages,
|
|
2731
|
+
// deprecated
|
|
2090
2732
|
metadata: any
|
|
2091
|
-
}),
|
|
2733
|
+
}),
|
|
2734
|
+
// Default value
|
|
2735
|
+
() => ({
|
|
2092
2736
|
content: "",
|
|
2093
2737
|
widgets: {},
|
|
2094
2738
|
images: {}
|
|
@@ -2101,10 +2745,25 @@ const parseHint = object({
|
|
|
2101
2745
|
content: string,
|
|
2102
2746
|
widgets: defaulted(parseWidgetsMap, () => ({})),
|
|
2103
2747
|
images: parseImages,
|
|
2748
|
+
// deprecated
|
|
2104
2749
|
metadata: any
|
|
2105
2750
|
});
|
|
2106
2751
|
|
|
2107
2752
|
const parsePerseusAnswerArea = pipeParsers(defaulted(object({}), () => ({}))).then(convert(toAnswerArea)).parser;
|
|
2753
|
+
|
|
2754
|
+
// Some answerAreas have extra, bogus fields, like:
|
|
2755
|
+
//
|
|
2756
|
+
// "answerArea": {
|
|
2757
|
+
// "type": "multiple",
|
|
2758
|
+
// "options": {},
|
|
2759
|
+
// "version": null,
|
|
2760
|
+
// "static": false,
|
|
2761
|
+
// "graded": false,
|
|
2762
|
+
// "alignment": "",
|
|
2763
|
+
// }
|
|
2764
|
+
//
|
|
2765
|
+
// This function filters the fields of an answerArea object, keeping only the
|
|
2766
|
+
// known ones, and converts `undefined` and `null` values to `false`.
|
|
2108
2767
|
function toAnswerArea(raw) {
|
|
2109
2768
|
return {
|
|
2110
2769
|
zTable: !!raw.zTable,
|
|
@@ -2127,15 +2786,37 @@ const parsePerseusItem$1 = object({
|
|
|
2127
2786
|
major: number,
|
|
2128
2787
|
minor: number
|
|
2129
2788
|
}))),
|
|
2789
|
+
// Deprecated field
|
|
2130
2790
|
answer: any
|
|
2131
2791
|
});
|
|
2132
2792
|
|
|
2793
|
+
/**
|
|
2794
|
+
* Helper to parse PerseusItem JSON
|
|
2795
|
+
* Why not just use JSON.parse? We want:
|
|
2796
|
+
* - To make sure types are correct
|
|
2797
|
+
* - To give us a central place to validate/transform output if needed
|
|
2798
|
+
* @deprecated - use parseAndMigratePerseusItem instead
|
|
2799
|
+
* @param {string} json - the stringified PerseusItem JSON
|
|
2800
|
+
* @returns {PerseusItem} the parsed PerseusItem object
|
|
2801
|
+
*/
|
|
2133
2802
|
function parsePerseusItem(json) {
|
|
2803
|
+
// Try to block a cheating vector which relies on monkey-patching
|
|
2804
|
+
// JSON.parse
|
|
2134
2805
|
if (isRealJSONParse(JSON.parse)) {
|
|
2135
2806
|
return JSON.parse(json);
|
|
2136
2807
|
}
|
|
2137
2808
|
throw new Error("Something went wrong.");
|
|
2138
2809
|
}
|
|
2810
|
+
/**
|
|
2811
|
+
* Parses a PerseusItem from a JSON string, migrates old formats to the latest
|
|
2812
|
+
* schema, and runtime-typechecks the result. Use this to parse assessmentItem
|
|
2813
|
+
* data.
|
|
2814
|
+
*
|
|
2815
|
+
* @returns a {@link Result} of the parsed PerseusItem. If the result is a
|
|
2816
|
+
* failure, it will contain an error message describing where in the tree
|
|
2817
|
+
* parsing failed.
|
|
2818
|
+
* @throws SyntaxError if the argument is not well-formed JSON.
|
|
2819
|
+
*/
|
|
2139
2820
|
function parseAndMigratePerseusItem(json) {
|
|
2140
2821
|
throwErrorIfCheatingDetected();
|
|
2141
2822
|
const object = JSON.parse(json);
|
|
@@ -2148,6 +2829,16 @@ function parseAndMigratePerseusItem(json) {
|
|
|
2148
2829
|
}
|
|
2149
2830
|
return result;
|
|
2150
2831
|
}
|
|
2832
|
+
|
|
2833
|
+
/**
|
|
2834
|
+
* Parses a PerseusArticle from a JSON string, migrates old formats to the
|
|
2835
|
+
* latest schema, and runtime-typechecks the result.
|
|
2836
|
+
*
|
|
2837
|
+
* @returns a {@link Result} of the parsed PerseusArticle. If the result is a
|
|
2838
|
+
* failure, it will contain an error message describing where in the tree
|
|
2839
|
+
* parsing failed.
|
|
2840
|
+
* @throws SyntaxError if the argument is not well-formed JSON.
|
|
2841
|
+
*/
|
|
2151
2842
|
function parseAndMigratePerseusArticle(json) {
|
|
2152
2843
|
throwErrorIfCheatingDetected();
|
|
2153
2844
|
const object = JSON.parse(json);
|
|
@@ -2160,48 +2851,131 @@ function parseAndMigratePerseusArticle(json) {
|
|
|
2160
2851
|
}
|
|
2161
2852
|
return result;
|
|
2162
2853
|
}
|
|
2854
|
+
|
|
2855
|
+
/**
|
|
2856
|
+
* Tries to block a cheating vector that relies on monkey-patching JSON.parse.
|
|
2857
|
+
*/
|
|
2858
|
+
// TODO(LEMS-2331): delete this function once server-side scoring is done.
|
|
2163
2859
|
function throwErrorIfCheatingDetected() {
|
|
2164
2860
|
if (!isRealJSONParse(JSON.parse)) {
|
|
2165
2861
|
throw new Error("Something went wrong.");
|
|
2166
2862
|
}
|
|
2167
2863
|
}
|
|
2168
2864
|
|
|
2865
|
+
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
2866
|
+
// version number during the release build.
|
|
2867
|
+
// In dev, you'll never see the version number.
|
|
2868
|
+
|
|
2169
2869
|
const libName = "@khanacademy/perseus-core";
|
|
2170
|
-
const libVersion = "
|
|
2171
|
-
addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
2870
|
+
const libVersion = "7.0.0";
|
|
2871
|
+
perseusUtils.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
2172
2872
|
|
|
2873
|
+
/**
|
|
2874
|
+
* @typedef {Object} Errors utility for referencing the Perseus error taxonomy.
|
|
2875
|
+
*/
|
|
2173
2876
|
const Errors = Object.freeze({
|
|
2877
|
+
/**
|
|
2878
|
+
* @property {ErrorKind} Unknown The kind of error is not known.
|
|
2879
|
+
*/
|
|
2174
2880
|
Unknown: "Unknown",
|
|
2881
|
+
/**
|
|
2882
|
+
* @property {ErrorKind} Internal The error is internal to the executing code.
|
|
2883
|
+
*/
|
|
2175
2884
|
Internal: "Internal",
|
|
2885
|
+
/**
|
|
2886
|
+
* @property {ErrorKind} InvalidInput There was a problem with the provided
|
|
2887
|
+
* input, such as the wrong format or a null value.
|
|
2888
|
+
*/
|
|
2176
2889
|
InvalidInput: "InvalidInput",
|
|
2890
|
+
/**
|
|
2891
|
+
* @property {ErrorKind} NotAllowed There was a problem due to the state of
|
|
2892
|
+
* the system not matching the requested operation or input. For example,
|
|
2893
|
+
* trying to create a username that is valid, but is already taken by
|
|
2894
|
+
* another user. Use {@link InvalidInput} instead when the input isn't
|
|
2895
|
+
* valid regardless of the state of the system. Use {@link NotFound} when
|
|
2896
|
+
* the failure is due to not being able to find a resource.
|
|
2897
|
+
*/
|
|
2177
2898
|
NotAllowed: "NotAllowed",
|
|
2899
|
+
/**
|
|
2900
|
+
* @property {ErrorKind} TransientService There was a problem when making a
|
|
2901
|
+
* request to a service.
|
|
2902
|
+
*/
|
|
2178
2903
|
TransientService: "TransientService",
|
|
2904
|
+
/**
|
|
2905
|
+
* @property {ErrorKind} Service There was a non-transient problem when
|
|
2906
|
+
* making a request to service.
|
|
2907
|
+
*/
|
|
2179
2908
|
Service: "Service"
|
|
2180
2909
|
});
|
|
2181
2910
|
|
|
2911
|
+
/**
|
|
2912
|
+
* @type {ErrorKind} The kind of error being reported
|
|
2913
|
+
*/
|
|
2914
|
+
|
|
2182
2915
|
class PerseusError extends Error {
|
|
2916
|
+
kind;
|
|
2917
|
+
metadata;
|
|
2183
2918
|
constructor(message, kind, options) {
|
|
2184
2919
|
super(message);
|
|
2185
|
-
this.kind = void 0;
|
|
2186
|
-
this.metadata = void 0;
|
|
2187
2920
|
this.kind = kind;
|
|
2188
|
-
this.metadata = options
|
|
2921
|
+
this.metadata = options?.metadata;
|
|
2189
2922
|
}
|
|
2190
2923
|
}
|
|
2191
2924
|
|
|
2192
|
-
|
|
2193
|
-
|
|
2925
|
+
/**
|
|
2926
|
+
* _ utilities for objects
|
|
2927
|
+
*/
|
|
2928
|
+
|
|
2929
|
+
|
|
2930
|
+
/**
|
|
2931
|
+
* Does a pluck on keys inside objects in an object
|
|
2932
|
+
*
|
|
2933
|
+
* Ex:
|
|
2934
|
+
* tools = {
|
|
2935
|
+
* translation: {
|
|
2936
|
+
* enabled: true
|
|
2937
|
+
* },
|
|
2938
|
+
* rotation: {
|
|
2939
|
+
* enabled: false
|
|
2940
|
+
* }
|
|
2941
|
+
* };
|
|
2942
|
+
* pluckObject(tools, "enabled") returns {
|
|
2943
|
+
* translation: true
|
|
2944
|
+
* rotation: false
|
|
2945
|
+
* }
|
|
2946
|
+
*/
|
|
2947
|
+
const pluck = function (table, subKey) {
|
|
2948
|
+
return ___default.default.object(___default.default.map(table, function (value, key) {
|
|
2194
2949
|
return [key, value[subKey]];
|
|
2195
2950
|
}));
|
|
2196
2951
|
};
|
|
2197
|
-
|
|
2952
|
+
|
|
2953
|
+
/**
|
|
2954
|
+
* Maps an object to an object
|
|
2955
|
+
*
|
|
2956
|
+
* > mapObject({a: '1', b: '2'}, (value, key) => {
|
|
2957
|
+
* return value + 1;
|
|
2958
|
+
* });
|
|
2959
|
+
* {a: 2, b: 3}
|
|
2960
|
+
*/
|
|
2961
|
+
const mapObject = function (obj, lambda) {
|
|
2198
2962
|
const result = {};
|
|
2199
2963
|
Object.keys(obj).forEach(key => {
|
|
2964
|
+
// @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
|
|
2200
2965
|
result[key] = lambda(obj[key], key);
|
|
2201
2966
|
});
|
|
2202
2967
|
return result;
|
|
2203
2968
|
};
|
|
2204
2969
|
|
|
2970
|
+
/**
|
|
2971
|
+
* For details on the individual options, see the
|
|
2972
|
+
* PerseusCategorizerWidgetOptions type
|
|
2973
|
+
*/
|
|
2974
|
+
|
|
2975
|
+
/**
|
|
2976
|
+
* Given a PerseusCategorizerWidgetOptions object, return a new object with only
|
|
2977
|
+
* the public options that should be exposed to the client.
|
|
2978
|
+
*/
|
|
2205
2979
|
function getCategorizerPublicWidgetOptions(options) {
|
|
2206
2980
|
return {
|
|
2207
2981
|
items: options.items,
|
|
@@ -2256,6 +3030,15 @@ const definitionWidgetLogic = {
|
|
|
2256
3030
|
defaultAlignment: "inline"
|
|
2257
3031
|
};
|
|
2258
3032
|
|
|
3033
|
+
/**
|
|
3034
|
+
* For details on the individual options, see the
|
|
3035
|
+
* PerseusDropdownWidgetOptions type
|
|
3036
|
+
*/
|
|
3037
|
+
|
|
3038
|
+
/**
|
|
3039
|
+
* Given a PerseusDropdownWidgetOptions object, return a new object with only
|
|
3040
|
+
* the public options that should be exposed to the client.
|
|
3041
|
+
*/
|
|
2259
3042
|
function getDropdownPublicWidgetOptions(options) {
|
|
2260
3043
|
return {
|
|
2261
3044
|
choices: options.choices.map(choice => ({
|
|
@@ -2336,6 +3119,15 @@ const defaultWidgetOptions$p = {
|
|
|
2336
3119
|
functions: ["f", "g", "h"]
|
|
2337
3120
|
};
|
|
2338
3121
|
|
|
3122
|
+
/**
|
|
3123
|
+
* For details on the individual options, see the
|
|
3124
|
+
* PerseusExpressionWidgetOptions type
|
|
3125
|
+
*/
|
|
3126
|
+
|
|
3127
|
+
/**
|
|
3128
|
+
* Given a PerseusExpressionWidgetOptions object, return a new object with only
|
|
3129
|
+
* the public options that should be exposed to the client.
|
|
3130
|
+
*/
|
|
2339
3131
|
function getExpressionPublicWidgetOptions(options) {
|
|
2340
3132
|
return {
|
|
2341
3133
|
buttonSets: options.buttonSets,
|
|
@@ -2377,9 +3169,11 @@ const gradedGroupSetWidgetLogic = {
|
|
|
2377
3169
|
defaultWidgetOptions: defaultWidgetOptions$n
|
|
2378
3170
|
};
|
|
2379
3171
|
|
|
2380
|
-
const _excluded$8 = ["correct"];
|
|
2381
3172
|
function getGrapherPublicWidgetOptions(options) {
|
|
2382
|
-
const
|
|
3173
|
+
const {
|
|
3174
|
+
correct: _,
|
|
3175
|
+
...publicOptions
|
|
3176
|
+
} = options;
|
|
2383
3177
|
return publicOptions;
|
|
2384
3178
|
}
|
|
2385
3179
|
|
|
@@ -2491,9 +3285,11 @@ const interactionWidgetLogic = {
|
|
|
2491
3285
|
defaultWidgetOptions: defaultWidgetOptions$h
|
|
2492
3286
|
};
|
|
2493
3287
|
|
|
2494
|
-
const _excluded$7 = ["correct"];
|
|
2495
3288
|
function getInteractiveGraphPublicWidgetOptions(options) {
|
|
2496
|
-
const
|
|
3289
|
+
const {
|
|
3290
|
+
correct: _,
|
|
3291
|
+
...publicOptions
|
|
3292
|
+
} = options;
|
|
2497
3293
|
return publicOptions;
|
|
2498
3294
|
}
|
|
2499
3295
|
|
|
@@ -2521,14 +3317,22 @@ const interactiveGraphWidgetLogic = {
|
|
|
2521
3317
|
getPublicWidgetOptions: getInteractiveGraphPublicWidgetOptions
|
|
2522
3318
|
};
|
|
2523
3319
|
|
|
2524
|
-
|
|
3320
|
+
/**
|
|
3321
|
+
* For details on the individual options, see the
|
|
3322
|
+
* PerseusLabelImageWidgetOptions type
|
|
3323
|
+
*/
|
|
3324
|
+
|
|
2525
3325
|
function getLabelImagePublicWidgetOptions(options) {
|
|
2526
|
-
return
|
|
3326
|
+
return {
|
|
3327
|
+
...options,
|
|
2527
3328
|
markers: options.markers.map(getLabelImageMarkerPublicData)
|
|
2528
|
-
}
|
|
3329
|
+
};
|
|
2529
3330
|
}
|
|
2530
3331
|
function getLabelImageMarkerPublicData(marker) {
|
|
2531
|
-
const
|
|
3332
|
+
const {
|
|
3333
|
+
answers: _,
|
|
3334
|
+
...publicData
|
|
3335
|
+
} = marker;
|
|
2532
3336
|
return publicData;
|
|
2533
3337
|
}
|
|
2534
3338
|
|
|
@@ -2548,9 +3352,14 @@ const labelImageWidgetLogic = {
|
|
|
2548
3352
|
getPublicWidgetOptions: getLabelImagePublicWidgetOptions
|
|
2549
3353
|
};
|
|
2550
3354
|
|
|
2551
|
-
|
|
3355
|
+
/* Note(tamara): Brought over from the perseus package packages/perseus/src/util.ts file.
|
|
3356
|
+
May be useful to bring other perseus package utilities here. Contains utility functions
|
|
3357
|
+
and types used across multiple widgets for randomization and shuffling. */
|
|
3358
|
+
|
|
3359
|
+
const seededRNG = function (seed) {
|
|
2552
3360
|
let randomSeed = seed;
|
|
2553
3361
|
return function () {
|
|
3362
|
+
// Robert Jenkins' 32 bit integer hash function.
|
|
2554
3363
|
let seed = randomSeed;
|
|
2555
3364
|
seed = seed + 0x7ed55d16 + (seed << 12) & 0xffffffff;
|
|
2556
3365
|
seed = (seed ^ 0xc761c23c ^ seed >>> 19) & 0xffffffff;
|
|
@@ -2561,10 +3370,20 @@ const seededRNG = function seededRNG(seed) {
|
|
|
2561
3370
|
return (randomSeed = seed & 0xfffffff) / 0x10000000;
|
|
2562
3371
|
};
|
|
2563
3372
|
};
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
3373
|
+
|
|
3374
|
+
// Shuffle an array using a given random seed or function.
|
|
3375
|
+
// If `ensurePermuted` is true, the input and output are guaranteed to be
|
|
3376
|
+
// distinct permutations.
|
|
3377
|
+
function shuffle(array, randomSeed) {
|
|
3378
|
+
let ensurePermuted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
|
3379
|
+
// Always return a copy of the input array
|
|
3380
|
+
const shuffled = ___default.default.clone(array);
|
|
3381
|
+
|
|
3382
|
+
// Handle edge cases (input array is empty or uniform)
|
|
3383
|
+
if (
|
|
3384
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
3385
|
+
!shuffled.length || ___default.default.all(shuffled, function (value) {
|
|
3386
|
+
return ___default.default.isEqual(value, shuffled[0]);
|
|
2568
3387
|
})) {
|
|
2569
3388
|
return shuffled;
|
|
2570
3389
|
}
|
|
@@ -2575,53 +3394,77 @@ function shuffle(array, randomSeed, ensurePermuted = false) {
|
|
|
2575
3394
|
random = seededRNG(randomSeed);
|
|
2576
3395
|
}
|
|
2577
3396
|
do {
|
|
3397
|
+
// Fischer-Yates shuffle
|
|
2578
3398
|
for (let top = shuffled.length; top > 0; top--) {
|
|
2579
3399
|
const newEnd = Math.floor(random() * top);
|
|
2580
3400
|
const temp = shuffled[newEnd];
|
|
3401
|
+
|
|
3402
|
+
// @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
|
|
2581
3403
|
shuffled[newEnd] = shuffled[top - 1];
|
|
3404
|
+
// @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
|
|
2582
3405
|
shuffled[top - 1] = temp;
|
|
2583
3406
|
}
|
|
2584
|
-
} while (ensurePermuted &&
|
|
3407
|
+
} while (ensurePermuted && ___default.default.isEqual(array, shuffled));
|
|
2585
3408
|
return shuffled;
|
|
2586
3409
|
}
|
|
2587
3410
|
const random = seededRNG(new Date().getTime() & 0xffffffff);
|
|
2588
3411
|
|
|
3412
|
+
// TODO(LEMS-2841): Should be able to remove once getPublicWidgetOptions is hooked up
|
|
3413
|
+
|
|
3414
|
+
// TODO(LEMS-2841): Should be able to remove once getPublicWidgetOptions is hooked up
|
|
2589
3415
|
const shuffleMatcher = props => {
|
|
3416
|
+
// Use the same random() function to shuffle both columns sequentially
|
|
2590
3417
|
const rng = seededRNG(props.problemNum);
|
|
2591
3418
|
let left;
|
|
2592
3419
|
if (!props.orderMatters) {
|
|
3420
|
+
// If the order doesn't matter, don't shuffle the left column
|
|
2593
3421
|
left = props.left;
|
|
2594
3422
|
} else {
|
|
2595
|
-
left = shuffle(props.left, rng, true);
|
|
3423
|
+
left = shuffle(props.left, rng, /* ensurePermuted */true);
|
|
2596
3424
|
}
|
|
2597
|
-
const right = shuffle(props.right, rng, true);
|
|
3425
|
+
const right = shuffle(props.right, rng, /* ensurePermuted */true);
|
|
2598
3426
|
return {
|
|
2599
3427
|
left,
|
|
2600
3428
|
right
|
|
2601
3429
|
};
|
|
2602
3430
|
};
|
|
3431
|
+
|
|
3432
|
+
// TODO(LEMS-2841): Can shorten to shuffleMatcher after above function removed
|
|
2603
3433
|
function shuffleMatcherWithRandom(data) {
|
|
3434
|
+
// Use the same random() function to shuffle both columns sequentially
|
|
2604
3435
|
let left;
|
|
2605
3436
|
if (!data.orderMatters) {
|
|
3437
|
+
// If the order doesn't matter, don't shuffle the left column
|
|
2606
3438
|
left = data.left;
|
|
2607
3439
|
} else {
|
|
2608
|
-
left = shuffle(data.left, Math.random, true);
|
|
3440
|
+
left = shuffle(data.left, Math.random, /* ensurePermuted */true);
|
|
2609
3441
|
}
|
|
2610
|
-
const right = shuffle(data.right, Math.random, true);
|
|
3442
|
+
const right = shuffle(data.right, Math.random, /* ensurePermuted */true);
|
|
2611
3443
|
return {
|
|
2612
3444
|
left,
|
|
2613
3445
|
right
|
|
2614
3446
|
};
|
|
2615
3447
|
}
|
|
3448
|
+
|
|
3449
|
+
/**
|
|
3450
|
+
* For details on the individual options, see the
|
|
3451
|
+
* PerseusMatcherWidgetOptions type
|
|
3452
|
+
*/
|
|
3453
|
+
|
|
3454
|
+
/**
|
|
3455
|
+
* Given a PerseusMatcherWidgetOptions object, return a new object with only
|
|
3456
|
+
* the public options that should be exposed to the client.
|
|
3457
|
+
*/
|
|
2616
3458
|
function getMatcherPublicWidgetOptions(options) {
|
|
2617
3459
|
const {
|
|
2618
3460
|
left,
|
|
2619
3461
|
right
|
|
2620
3462
|
} = shuffleMatcherWithRandom(options);
|
|
2621
|
-
return
|
|
3463
|
+
return {
|
|
3464
|
+
...options,
|
|
2622
3465
|
left: left,
|
|
2623
3466
|
right: right
|
|
2624
|
-
}
|
|
3467
|
+
};
|
|
2625
3468
|
}
|
|
2626
3469
|
|
|
2627
3470
|
const defaultWidgetOptions$e = {
|
|
@@ -2637,9 +3480,11 @@ const matcherWidgetLogic = {
|
|
|
2637
3480
|
getPublicWidgetOptions: getMatcherPublicWidgetOptions
|
|
2638
3481
|
};
|
|
2639
3482
|
|
|
2640
|
-
const _excluded$5 = ["answers"];
|
|
2641
3483
|
function getMatrixPublicWidgetOptions(options) {
|
|
2642
|
-
const
|
|
3484
|
+
const {
|
|
3485
|
+
answers: _,
|
|
3486
|
+
...publicOptions
|
|
3487
|
+
} = options;
|
|
2643
3488
|
return publicOptions;
|
|
2644
3489
|
}
|
|
2645
3490
|
|
|
@@ -2656,7 +3501,6 @@ const matrixWidgetLogic = {
|
|
|
2656
3501
|
getPublicWidgetOptions: getMatrixPublicWidgetOptions
|
|
2657
3502
|
};
|
|
2658
3503
|
|
|
2659
|
-
const _excluded$4 = ["imageUrl", "imageTop", "imageLeft"];
|
|
2660
3504
|
const currentVersion$1 = {
|
|
2661
3505
|
major: 1,
|
|
2662
3506
|
minor: 0
|
|
@@ -2664,18 +3508,19 @@ const currentVersion$1 = {
|
|
|
2664
3508
|
const widgetOptionsUpgrades = {
|
|
2665
3509
|
"1": v0options => {
|
|
2666
3510
|
const {
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
return
|
|
3511
|
+
imageUrl,
|
|
3512
|
+
imageTop,
|
|
3513
|
+
imageLeft,
|
|
3514
|
+
...rest
|
|
3515
|
+
} = v0options;
|
|
3516
|
+
return {
|
|
3517
|
+
...rest,
|
|
2673
3518
|
image: {
|
|
2674
3519
|
url: imageUrl,
|
|
2675
3520
|
top: imageTop,
|
|
2676
3521
|
left: imageLeft
|
|
2677
3522
|
}
|
|
2678
|
-
}
|
|
3523
|
+
};
|
|
2679
3524
|
}
|
|
2680
3525
|
};
|
|
2681
3526
|
const defaultWidgetOptions$c = {
|
|
@@ -2696,9 +3541,12 @@ const measurerWidgetLogic = {
|
|
|
2696
3541
|
defaultWidgetOptions: defaultWidgetOptions$c
|
|
2697
3542
|
};
|
|
2698
3543
|
|
|
2699
|
-
const _excluded$3 = ["correctX", "correctRel"];
|
|
2700
3544
|
function getNumberLinePublicWidgetOptions(options) {
|
|
2701
|
-
const
|
|
3545
|
+
const {
|
|
3546
|
+
correctX: _,
|
|
3547
|
+
correctRel: __,
|
|
3548
|
+
...publicOptions
|
|
3549
|
+
} = options;
|
|
2702
3550
|
return publicOptions;
|
|
2703
3551
|
}
|
|
2704
3552
|
|
|
@@ -2722,7 +3570,15 @@ const numberLineWidgetLogic = {
|
|
|
2722
3570
|
getPublicWidgetOptions: getNumberLinePublicWidgetOptions
|
|
2723
3571
|
};
|
|
2724
3572
|
|
|
2725
|
-
|
|
3573
|
+
/**
|
|
3574
|
+
* For details on the individual options, see the
|
|
3575
|
+
* PerseusNumericInputWidgetOptions type
|
|
3576
|
+
*/
|
|
3577
|
+
|
|
3578
|
+
/**
|
|
3579
|
+
* This data from `answers` is used pre-scoring to give hints
|
|
3580
|
+
* to the learner regarding the format of accepted answers
|
|
3581
|
+
*/
|
|
2726
3582
|
function getNumericInputAnswerPublicData(answer) {
|
|
2727
3583
|
const {
|
|
2728
3584
|
answerForms,
|
|
@@ -2735,14 +3591,20 @@ function getNumericInputAnswerPublicData(answer) {
|
|
|
2735
3591
|
status
|
|
2736
3592
|
};
|
|
2737
3593
|
}
|
|
3594
|
+
|
|
3595
|
+
/**
|
|
3596
|
+
* Given a PerseusNumericInputWidgetOptions object, return a new object with only
|
|
3597
|
+
* the public options that should be exposed to the client.
|
|
3598
|
+
*/
|
|
2738
3599
|
function getNumericInputPublicWidgetOptions(options) {
|
|
2739
3600
|
const {
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
return
|
|
3601
|
+
answers,
|
|
3602
|
+
...publicWidgetOptions
|
|
3603
|
+
} = options;
|
|
3604
|
+
return {
|
|
3605
|
+
...publicWidgetOptions,
|
|
2744
3606
|
answers: answers.map(getNumericInputAnswerPublicData)
|
|
2745
|
-
}
|
|
3607
|
+
};
|
|
2746
3608
|
}
|
|
2747
3609
|
|
|
2748
3610
|
const defaultWidgetOptions$a = {
|
|
@@ -2767,6 +3629,15 @@ const numericInputWidgetLogic = {
|
|
|
2767
3629
|
getPublicWidgetOptions: getNumericInputPublicWidgetOptions
|
|
2768
3630
|
};
|
|
2769
3631
|
|
|
3632
|
+
/**
|
|
3633
|
+
* For details on the individual options, see the
|
|
3634
|
+
* PerseusOrdererWidgetOptions type
|
|
3635
|
+
*/
|
|
3636
|
+
|
|
3637
|
+
/**
|
|
3638
|
+
* Given a PerseusOrdererWidgetOptions object, return a new object with only
|
|
3639
|
+
* the public options that should be exposed to the client.
|
|
3640
|
+
*/
|
|
2770
3641
|
function getOrdererPublicWidgetOptions(options) {
|
|
2771
3642
|
return {
|
|
2772
3643
|
options: options.options,
|
|
@@ -2837,9 +3708,20 @@ const phetSimulationWidgetLogic = {
|
|
|
2837
3708
|
defaultWidgetOptions: defaultWidgetOptions$5
|
|
2838
3709
|
};
|
|
2839
3710
|
|
|
2840
|
-
|
|
3711
|
+
/**
|
|
3712
|
+
* For details on the individual options, see the
|
|
3713
|
+
* PerseusPlotterWidgetOptions type
|
|
3714
|
+
*/
|
|
3715
|
+
|
|
3716
|
+
/**
|
|
3717
|
+
* Given a PerseusPlotterWidgetOptions object, return a new object with only
|
|
3718
|
+
* the public options that should be exposed to the client.
|
|
3719
|
+
*/
|
|
2841
3720
|
function getPlotterPublicWidgetOptions(options) {
|
|
2842
|
-
const
|
|
3721
|
+
const {
|
|
3722
|
+
correct: _,
|
|
3723
|
+
...publicOptions
|
|
3724
|
+
} = options;
|
|
2843
3725
|
return publicOptions;
|
|
2844
3726
|
}
|
|
2845
3727
|
|
|
@@ -2873,6 +3755,19 @@ const pythonProgramWidgetLogic = {
|
|
|
2873
3755
|
defaultWidgetOptions: defaultWidgetOptions$3
|
|
2874
3756
|
};
|
|
2875
3757
|
|
|
3758
|
+
/**
|
|
3759
|
+
* For details on the individual options, see the
|
|
3760
|
+
* PerseusRadioWidgetOptions type
|
|
3761
|
+
*/
|
|
3762
|
+
|
|
3763
|
+
/**
|
|
3764
|
+
* Only the options from each Radio choice that should be exposed to the client.
|
|
3765
|
+
*/
|
|
3766
|
+
|
|
3767
|
+
/**
|
|
3768
|
+
* Given a PerseusRadioChoice object, return a new object with only the public
|
|
3769
|
+
* data that should be included in the Radio public widget options.
|
|
3770
|
+
*/
|
|
2876
3771
|
function getRadioChoicePublicData(choice) {
|
|
2877
3772
|
const {
|
|
2878
3773
|
content,
|
|
@@ -2885,9 +3780,23 @@ function getRadioChoicePublicData(choice) {
|
|
|
2885
3780
|
widgets
|
|
2886
3781
|
};
|
|
2887
3782
|
}
|
|
3783
|
+
|
|
3784
|
+
/**
|
|
3785
|
+
* Shared functionality to determine if numCorrect is used, because:
|
|
3786
|
+
*
|
|
3787
|
+
* 1. numCorrect is conditionally used for rendering pre-scoring
|
|
3788
|
+
* 2. numCorrect also exposes information about answers
|
|
3789
|
+
*
|
|
3790
|
+
* So only include/use numCorrect when we know it's useful.
|
|
3791
|
+
*/
|
|
2888
3792
|
function usesNumCorrect(multipleSelect, countChoices, numCorrect) {
|
|
2889
3793
|
return multipleSelect && countChoices && numCorrect;
|
|
2890
3794
|
}
|
|
3795
|
+
|
|
3796
|
+
/**
|
|
3797
|
+
* Given a PerseusRadioWidgetOptions object, return a new object with only
|
|
3798
|
+
* the public options that should be exposed to the client.
|
|
3799
|
+
*/
|
|
2891
3800
|
function getRadioPublicWidgetOptions(options) {
|
|
2892
3801
|
const {
|
|
2893
3802
|
numCorrect,
|
|
@@ -2895,10 +3804,12 @@ function getRadioPublicWidgetOptions(options) {
|
|
|
2895
3804
|
multipleSelect,
|
|
2896
3805
|
countChoices
|
|
2897
3806
|
} = options;
|
|
2898
|
-
return
|
|
3807
|
+
return {
|
|
3808
|
+
...options,
|
|
3809
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
2899
3810
|
numCorrect: usesNumCorrect(multipleSelect, countChoices, numCorrect) ? numCorrect : undefined,
|
|
2900
3811
|
choices: choices.map(getRadioChoicePublicData)
|
|
2901
|
-
}
|
|
3812
|
+
};
|
|
2902
3813
|
}
|
|
2903
3814
|
|
|
2904
3815
|
const radioWidgetLogic = {
|
|
@@ -2909,12 +3820,28 @@ const radioWidgetLogic = {
|
|
|
2909
3820
|
getPublicWidgetOptions: getRadioPublicWidgetOptions
|
|
2910
3821
|
};
|
|
2911
3822
|
|
|
3823
|
+
/**
|
|
3824
|
+
* For details on the individual options, see the
|
|
3825
|
+
* PerseusSorterWidgetOptions type
|
|
3826
|
+
*/
|
|
3827
|
+
|
|
3828
|
+
/**
|
|
3829
|
+
* Given a PerseusSorterWidgetOptions object, return a new object with only
|
|
3830
|
+
* the public options that should be exposed to the client.
|
|
3831
|
+
*/
|
|
2912
3832
|
function getSorterPublicWidgetOptions(options) {
|
|
2913
|
-
const shuffledCorrect = shuffle(options.correct, Math.random, true);
|
|
2914
|
-
return
|
|
3833
|
+
const shuffledCorrect = shuffle(options.correct, Math.random, /* ensurePermuted */true);
|
|
3834
|
+
return {
|
|
3835
|
+
...options,
|
|
3836
|
+
// Note(Tamara): This does not provide correct answer information any longer.
|
|
3837
|
+
// To maintain compatibility with the original widget options, we are
|
|
3838
|
+
// keeping the key the same. Represents initial state of the cards here.
|
|
2915
3839
|
correct: shuffledCorrect,
|
|
3840
|
+
// Note(Tamara): This new key is only added here with "true". There isn't
|
|
3841
|
+
// a place where it is set to false. It indicates that the correct field
|
|
3842
|
+
// has been shuffled and no longer contains correct answer info.
|
|
2916
3843
|
isCorrectShuffled: true
|
|
2917
|
-
}
|
|
3844
|
+
};
|
|
2918
3845
|
}
|
|
2919
3846
|
|
|
2920
3847
|
const defaultWidgetOptions$2 = {
|
|
@@ -2928,14 +3855,19 @@ const sorterWidgetLogic = {
|
|
|
2928
3855
|
getPublicWidgetOptions: getSorterPublicWidgetOptions
|
|
2929
3856
|
};
|
|
2930
3857
|
|
|
2931
|
-
const _excluded = ["answers"];
|
|
2932
3858
|
function getTablePublicWidgetOptions(options) {
|
|
2933
|
-
const
|
|
3859
|
+
const {
|
|
3860
|
+
answers: _,
|
|
3861
|
+
...publicOptions
|
|
3862
|
+
} = options;
|
|
2934
3863
|
return publicOptions;
|
|
2935
3864
|
}
|
|
2936
3865
|
|
|
2937
3866
|
const defaultRows = 4;
|
|
2938
3867
|
const defaultColumns = 1;
|
|
3868
|
+
|
|
3869
|
+
// initialize a 2D array
|
|
3870
|
+
// (defaultRows x defaultColumns) of empty strings
|
|
2939
3871
|
const answers = new Array(defaultRows).fill(0).map(() => new Array(defaultColumns).fill(""));
|
|
2940
3872
|
const defaultWidgetOptions$1 = {
|
|
2941
3873
|
headers: [""],
|
|
@@ -2969,34 +3901,60 @@ function isWidgetRegistered(type) {
|
|
|
2969
3901
|
}
|
|
2970
3902
|
function getCurrentVersion(type) {
|
|
2971
3903
|
const widgetLogic = widgets[type];
|
|
2972
|
-
return
|
|
3904
|
+
return widgetLogic?.version || {
|
|
2973
3905
|
major: 0,
|
|
2974
3906
|
minor: 0
|
|
2975
3907
|
};
|
|
2976
3908
|
}
|
|
3909
|
+
|
|
3910
|
+
// TODO(LEMS-2870): getPublicWidgetOptionsFunction/PublicWidgetOptionsFunction
|
|
3911
|
+
// need better types
|
|
2977
3912
|
const getPublicWidgetOptionsFunction = name => {
|
|
2978
|
-
|
|
2979
|
-
return (_widgets$name$getPubl = (_widgets$name = widgets[name]) == null ? void 0 : _widgets$name.getPublicWidgetOptions) != null ? _widgets$name$getPubl : i => i;
|
|
3913
|
+
return widgets[name]?.getPublicWidgetOptions ?? (i => i);
|
|
2980
3914
|
};
|
|
2981
3915
|
function getWidgetOptionsUpgrades(type) {
|
|
2982
3916
|
const widgetLogic = widgets[type];
|
|
2983
|
-
return
|
|
3917
|
+
return widgetLogic?.widgetOptionsUpgrades || {};
|
|
2984
3918
|
}
|
|
2985
3919
|
function getDefaultWidgetOptions(type) {
|
|
2986
3920
|
const widgetLogic = widgets[type];
|
|
2987
|
-
return
|
|
3921
|
+
return widgetLogic?.defaultWidgetOptions || {};
|
|
2988
3922
|
}
|
|
3923
|
+
|
|
3924
|
+
/**
|
|
3925
|
+
* Handling for the optional alignments for widgets
|
|
3926
|
+
* See widget-container.jsx for details on how alignments are implemented.
|
|
3927
|
+
*/
|
|
3928
|
+
|
|
3929
|
+
/**
|
|
3930
|
+
* Returns the list of supported alignments for the given (string) widget
|
|
3931
|
+
* type. This is used primarily at editing time to display the choices
|
|
3932
|
+
* for the user.
|
|
3933
|
+
*
|
|
3934
|
+
* Supported alignments are given as an array of strings in the exports of
|
|
3935
|
+
* a widget's module.
|
|
3936
|
+
*/
|
|
2989
3937
|
const getSupportedAlignments = type => {
|
|
2990
|
-
var _widgetLogic$supporte;
|
|
2991
3938
|
const widgetLogic = widgets[type];
|
|
2992
|
-
if (!
|
|
3939
|
+
if (!widgetLogic?.supportedAlignments?.[0]) {
|
|
3940
|
+
// default alignments
|
|
2993
3941
|
return ["default"];
|
|
2994
3942
|
}
|
|
2995
|
-
return widgetLogic
|
|
3943
|
+
return widgetLogic?.supportedAlignments;
|
|
2996
3944
|
};
|
|
3945
|
+
|
|
3946
|
+
/**
|
|
3947
|
+
* For the given (string) widget type, determine the default alignment for
|
|
3948
|
+
* the widget. This is used at rendering time to go from "default" alignment
|
|
3949
|
+
* to the actual alignment displayed on the screen.
|
|
3950
|
+
*
|
|
3951
|
+
* The default alignment is given either as a string (called
|
|
3952
|
+
* `defaultAlignment`) or a function (called `getDefaultAlignment`) on
|
|
3953
|
+
* the exports of a widget's module.
|
|
3954
|
+
*/
|
|
2997
3955
|
const getDefaultAlignment = type => {
|
|
2998
3956
|
const widgetLogic = widgets[type];
|
|
2999
|
-
if (!
|
|
3957
|
+
if (!widgetLogic?.defaultAlignment) {
|
|
3000
3958
|
return "block";
|
|
3001
3959
|
}
|
|
3002
3960
|
return widgetLogic.defaultAlignment;
|
|
@@ -3048,23 +4006,52 @@ var coreWidgetRegistry = /*#__PURE__*/Object.freeze({
|
|
|
3048
4006
|
const DEFAULT_STATIC = false;
|
|
3049
4007
|
const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
|
|
3050
4008
|
const type = oldWidgetInfo.type;
|
|
3051
|
-
|
|
4009
|
+
// NOTE(jeremy): This looks like it could be replaced by fixing types so
|
|
4010
|
+
// that `type` is non-optional. But we're seeing this in Sentry today so I
|
|
4011
|
+
// suspect we have legacy data (potentially unpublished) and we should
|
|
4012
|
+
// figure that out before depending solely on types.
|
|
4013
|
+
if (!___default.default.isString(type)) {
|
|
3052
4014
|
throw new PerseusError("widget type must be a string, but was: " + type, Errors.Internal);
|
|
3053
4015
|
}
|
|
3054
4016
|
if (!isWidgetRegistered(type)) {
|
|
4017
|
+
// If we have a widget that isn't registered, we can't upgrade it
|
|
4018
|
+
// TODO(aria): Figure out what the best thing to do here would be
|
|
3055
4019
|
return oldWidgetInfo;
|
|
3056
4020
|
}
|
|
4021
|
+
|
|
4022
|
+
// Unversioned widgets (pre-July 2014) are all implicitly 0.0
|
|
3057
4023
|
const initialVersion = oldWidgetInfo.version || {
|
|
3058
4024
|
major: 0,
|
|
3059
4025
|
minor: 0
|
|
3060
4026
|
};
|
|
3061
4027
|
const latestVersion = getCurrentVersion(type);
|
|
4028
|
+
|
|
4029
|
+
// If the widget version is later than what we understand (major
|
|
4030
|
+
// version is higher than latest, or major versions are equal and minor
|
|
4031
|
+
// version is higher than latest), don't perform any upgrades.
|
|
3062
4032
|
if (initialVersion.major > latestVersion.major || initialVersion.major === latestVersion.major && initialVersion.minor > latestVersion.minor) {
|
|
3063
4033
|
return oldWidgetInfo;
|
|
3064
4034
|
}
|
|
3065
|
-
|
|
4035
|
+
|
|
4036
|
+
// We do a clone here so that it's safe to mutate the input parameter
|
|
4037
|
+
// in propUpgrades functions (which I will probably accidentally do at
|
|
4038
|
+
// some point, and we would like to not break when that happens).
|
|
4039
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
4040
|
+
let newEditorOptions = ___default.default.clone(oldWidgetInfo.options) || {};
|
|
3066
4041
|
const upgradePropsMap = getWidgetOptionsUpgrades(type);
|
|
3067
|
-
|
|
4042
|
+
|
|
4043
|
+
// Empty props usually mean a newly created widget by the editor,
|
|
4044
|
+
// and are always considerered up-to-date.
|
|
4045
|
+
// Mostly, we'd rather not run upgrade functions on props that are
|
|
4046
|
+
// not complete.
|
|
4047
|
+
if (___default.default.keys(newEditorOptions).length !== 0) {
|
|
4048
|
+
// We loop through all the versions after the current version of
|
|
4049
|
+
// the loaded widget, up to and including the latest version of the
|
|
4050
|
+
// loaded widget, and run the upgrade function to bring our loaded
|
|
4051
|
+
// widget's props up to that version.
|
|
4052
|
+
// There is a little subtlety here in that we call
|
|
4053
|
+
// upgradePropsMap[1] to upgrade *to* version 1,
|
|
4054
|
+
// (not from version 1).
|
|
3068
4055
|
for (let nextVersion = initialVersion.major + 1; nextVersion <= latestVersion.major; nextVersion++) {
|
|
3069
4056
|
if (upgradePropsMap[String(nextVersion)]) {
|
|
3070
4057
|
newEditorOptions = upgradePropsMap[String(nextVersion)](newEditorOptions);
|
|
@@ -3080,12 +4067,23 @@ const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
|
|
|
3080
4067
|
}
|
|
3081
4068
|
}
|
|
3082
4069
|
}
|
|
4070
|
+
|
|
4071
|
+
// Minor version upgrades (eg. new optional props) don't have
|
|
4072
|
+
// transform functions. Instead, we fill in the new props with their
|
|
4073
|
+
// defaults.
|
|
3083
4074
|
const defaultOptions = getDefaultWidgetOptions(type);
|
|
3084
|
-
newEditorOptions =
|
|
4075
|
+
newEditorOptions = {
|
|
4076
|
+
...defaultOptions,
|
|
4077
|
+
...newEditorOptions
|
|
4078
|
+
};
|
|
3085
4079
|
let alignment = oldWidgetInfo.alignment;
|
|
4080
|
+
|
|
4081
|
+
// Widgets that support multiple alignments will "lock in" the
|
|
4082
|
+
// alignment to the alignment that would be listed first in the
|
|
4083
|
+
// select box. If the widget only supports one alignment, the
|
|
4084
|
+
// alignment value will likely just end up as "default".
|
|
3086
4085
|
if (alignment == null || alignment === "default") {
|
|
3087
|
-
|
|
3088
|
-
alignment = (_getSupportedAlignmen = getSupportedAlignments(type)) == null ? void 0 : _getSupportedAlignmen[0];
|
|
4086
|
+
alignment = getSupportedAlignments(type)?.[0];
|
|
3089
4087
|
if (!alignment) {
|
|
3090
4088
|
throw new PerseusError("No default alignment found when upgrading widget", Errors.Internal, {
|
|
3091
4089
|
metadata: {
|
|
@@ -3098,46 +4096,146 @@ const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
|
|
|
3098
4096
|
if (widgetStatic == null) {
|
|
3099
4097
|
widgetStatic = DEFAULT_STATIC;
|
|
3100
4098
|
}
|
|
3101
|
-
return
|
|
4099
|
+
return {
|
|
4100
|
+
...oldWidgetInfo,
|
|
4101
|
+
// maintain other info, like type
|
|
4102
|
+
// After upgrading we guarantee that the version is up-to-date
|
|
3102
4103
|
version: latestVersion,
|
|
4104
|
+
// Default graded to true (so null/undefined becomes true):
|
|
3103
4105
|
graded: oldWidgetInfo.graded != null ? oldWidgetInfo.graded : true,
|
|
3104
4106
|
alignment: alignment,
|
|
3105
4107
|
static: widgetStatic,
|
|
3106
4108
|
options: newEditorOptions
|
|
3107
|
-
}
|
|
4109
|
+
};
|
|
3108
4110
|
};
|
|
3109
4111
|
function getUpgradedWidgetOptions(oldWidgetOptions) {
|
|
3110
4112
|
return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
|
|
3111
4113
|
if (!widgetInfo.type || !widgetInfo.alignment) {
|
|
3112
4114
|
const newValues = {};
|
|
3113
4115
|
if (!widgetInfo.type) {
|
|
4116
|
+
// TODO: why does widget have no type?
|
|
4117
|
+
// We don't want to derive type from widget ID
|
|
4118
|
+
// see: LEMS-1845
|
|
3114
4119
|
newValues.type = widgetId.split(" ")[0];
|
|
3115
4120
|
}
|
|
3116
4121
|
if (!widgetInfo.alignment) {
|
|
3117
4122
|
newValues.alignment = "default";
|
|
3118
4123
|
}
|
|
3119
|
-
widgetInfo =
|
|
4124
|
+
widgetInfo = {
|
|
4125
|
+
...widgetInfo,
|
|
4126
|
+
...newValues
|
|
4127
|
+
};
|
|
3120
4128
|
}
|
|
3121
4129
|
return upgradeWidgetInfoToLatestVersion(widgetInfo);
|
|
3122
4130
|
});
|
|
3123
4131
|
}
|
|
3124
4132
|
|
|
4133
|
+
/**
|
|
4134
|
+
* Return a copy of a Perseus item with rubric data removed (ie answers)
|
|
4135
|
+
*
|
|
4136
|
+
* @param originalItem - the original, full Perseus item (which includes the rubric - aka answer data)
|
|
4137
|
+
*/
|
|
3125
4138
|
function splitPerseusItem(originalItem) {
|
|
3126
|
-
|
|
3127
|
-
const
|
|
3128
|
-
const originalWidgets = (_item$widgets = item.widgets) != null ? _item$widgets : {};
|
|
4139
|
+
const item = ___default.default.clone(originalItem);
|
|
4140
|
+
const originalWidgets = item.widgets ?? {};
|
|
3129
4141
|
const upgradedWidgets = getUpgradedWidgetOptions(originalWidgets);
|
|
3130
4142
|
const splitWidgets = {};
|
|
3131
4143
|
for (const [id, widget] of Object.entries(upgradedWidgets)) {
|
|
3132
4144
|
const publicWidgetOptionsFun = getPublicWidgetOptionsFunction(widget.type);
|
|
3133
|
-
splitWidgets[id] =
|
|
4145
|
+
splitWidgets[id] = {
|
|
4146
|
+
...widget,
|
|
3134
4147
|
options: publicWidgetOptionsFun(widget.options)
|
|
3135
|
-
}
|
|
4148
|
+
};
|
|
3136
4149
|
}
|
|
3137
|
-
return
|
|
4150
|
+
return {
|
|
4151
|
+
...item,
|
|
3138
4152
|
widgets: splitWidgets
|
|
3139
|
-
}
|
|
4153
|
+
};
|
|
3140
4154
|
}
|
|
3141
4155
|
|
|
3142
|
-
|
|
4156
|
+
exports.CoreWidgetRegistry = coreWidgetRegistry;
|
|
4157
|
+
exports.Errors = Errors;
|
|
4158
|
+
exports.GrapherUtil = grapherUtil;
|
|
4159
|
+
exports.ItemExtras = ItemExtras;
|
|
4160
|
+
exports.PerseusError = PerseusError;
|
|
4161
|
+
exports.PerseusExpressionAnswerFormConsidered = PerseusExpressionAnswerFormConsidered;
|
|
4162
|
+
exports.addWidget = addWidget;
|
|
4163
|
+
exports.approximateDeepEqual = approximateDeepEqual;
|
|
4164
|
+
exports.approximateEqual = approximateEqual;
|
|
4165
|
+
exports.categorizerLogic = categorizerWidgetLogic;
|
|
4166
|
+
exports.csProgramLogic = csProgramWidgetLogic;
|
|
4167
|
+
exports.deepClone = deepClone;
|
|
4168
|
+
exports.definitionLogic = definitionWidgetLogic;
|
|
4169
|
+
exports.deriveExtraKeys = deriveExtraKeys;
|
|
4170
|
+
exports.deriveNumCorrect = deriveNumCorrect;
|
|
4171
|
+
exports.dropdownLogic = dropdownWidgetLogic;
|
|
4172
|
+
exports.explanationLogic = explanationWidgetLogic;
|
|
4173
|
+
exports.expressionLogic = expressionWidgetLogic;
|
|
4174
|
+
exports.getCSProgramPublicWidgetOptions = getCSProgramPublicWidgetOptions;
|
|
4175
|
+
exports.getCategorizerPublicWidgetOptions = getCategorizerPublicWidgetOptions;
|
|
4176
|
+
exports.getDecimalSeparator = getDecimalSeparator;
|
|
4177
|
+
exports.getDropdownPublicWidgetOptions = getDropdownPublicWidgetOptions;
|
|
4178
|
+
exports.getExpressionPublicWidgetOptions = getExpressionPublicWidgetOptions;
|
|
4179
|
+
exports.getGrapherPublicWidgetOptions = getGrapherPublicWidgetOptions;
|
|
4180
|
+
exports.getIFramePublicWidgetOptions = getIFramePublicWidgetOptions;
|
|
4181
|
+
exports.getInteractiveGraphPublicWidgetOptions = getInteractiveGraphPublicWidgetOptions;
|
|
4182
|
+
exports.getLabelImagePublicWidgetOptions = getLabelImagePublicWidgetOptions;
|
|
4183
|
+
exports.getMatcherPublicWidgetOptions = getMatcherPublicWidgetOptions;
|
|
4184
|
+
exports.getMatrixPublicWidgetOptions = getMatrixPublicWidgetOptions;
|
|
4185
|
+
exports.getMatrixSize = getMatrixSize;
|
|
4186
|
+
exports.getNumberLinePublicWidgetOptions = getNumberLinePublicWidgetOptions;
|
|
4187
|
+
exports.getNumericInputPublicWidgetOptions = getNumericInputPublicWidgetOptions;
|
|
4188
|
+
exports.getOrdererPublicWidgetOptions = getOrdererPublicWidgetOptions;
|
|
4189
|
+
exports.getPlotterPublicWidgetOptions = getPlotterPublicWidgetOptions;
|
|
4190
|
+
exports.getRadioPublicWidgetOptions = getRadioPublicWidgetOptions;
|
|
4191
|
+
exports.getSorterPublicWidgetOptions = getSorterPublicWidgetOptions;
|
|
4192
|
+
exports.getTablePublicWidgetOptions = getTablePublicWidgetOptions;
|
|
4193
|
+
exports.getUpgradedWidgetOptions = getUpgradedWidgetOptions;
|
|
4194
|
+
exports.getWidgetIdsFromContent = getWidgetIdsFromContent;
|
|
4195
|
+
exports.getWidgetIdsFromContentByType = getWidgetIdsFromContentByType;
|
|
4196
|
+
exports.gradedGroupLogic = gradedGroupWidgetLogic;
|
|
4197
|
+
exports.gradedGroupSetLogic = gradedGroupSetWidgetLogic;
|
|
4198
|
+
exports.grapherLogic = grapherWidgetLogic;
|
|
4199
|
+
exports.groupLogic = groupWidgetLogic;
|
|
4200
|
+
exports.iframeLogic = iframeWidgetLogic;
|
|
4201
|
+
exports.imageLogic = imageWidgetLogic;
|
|
4202
|
+
exports.inputNumberLogic = inputNumberWidgetLogic;
|
|
4203
|
+
exports.interactionLogic = interactionWidgetLogic;
|
|
4204
|
+
exports.interactiveGraphLogic = interactiveGraphWidgetLogic;
|
|
4205
|
+
exports.isFailure = isFailure;
|
|
4206
|
+
exports.isSuccess = isSuccess;
|
|
4207
|
+
exports.labelImageLogic = labelImageWidgetLogic;
|
|
4208
|
+
exports.libVersion = libVersion;
|
|
4209
|
+
exports.lockedFigureColorNames = lockedFigureColorNames;
|
|
4210
|
+
exports.lockedFigureColors = lockedFigureColors;
|
|
4211
|
+
exports.lockedFigureFillStyles = lockedFigureFillStyles;
|
|
4212
|
+
exports.mapObject = mapObject;
|
|
4213
|
+
exports.matcherLogic = matcherWidgetLogic;
|
|
4214
|
+
exports.matrixLogic = matrixWidgetLogic;
|
|
4215
|
+
exports.measurerLogic = measurerWidgetLogic;
|
|
4216
|
+
exports.numberLineLogic = numberLineWidgetLogic;
|
|
4217
|
+
exports.numericInputLogic = numericInputWidgetLogic;
|
|
4218
|
+
exports.ordererLogic = ordererWidgetLogic;
|
|
4219
|
+
exports.parseAndMigratePerseusArticle = parseAndMigratePerseusArticle;
|
|
4220
|
+
exports.parseAndMigratePerseusItem = parseAndMigratePerseusItem;
|
|
4221
|
+
exports.parsePerseusItem = parsePerseusItem;
|
|
4222
|
+
exports.passageLogic = passageWidgetLogic;
|
|
4223
|
+
exports.passageRefLogic = passageRefWidgetLogic;
|
|
4224
|
+
exports.passageRefTargetLogic = passageRefTargetWidgetLogic;
|
|
4225
|
+
exports.phetSimulationLogic = phetSimulationWidgetLogic;
|
|
4226
|
+
exports.plotterLogic = plotterWidgetLogic;
|
|
4227
|
+
exports.plotterPlotTypes = plotterPlotTypes;
|
|
4228
|
+
exports.pluck = pluck;
|
|
4229
|
+
exports.pythonProgramLogic = pythonProgramWidgetLogic;
|
|
4230
|
+
exports.radioLogic = radioWidgetLogic;
|
|
4231
|
+
exports.random = random;
|
|
4232
|
+
exports.seededRNG = seededRNG;
|
|
4233
|
+
exports.shuffle = shuffle;
|
|
4234
|
+
exports.shuffleMatcher = shuffleMatcher;
|
|
4235
|
+
exports.sorterLogic = sorterWidgetLogic;
|
|
4236
|
+
exports.splitPerseusItem = splitPerseusItem;
|
|
4237
|
+
exports.tableLogic = tableWidgetLogic;
|
|
4238
|
+
exports.upgradeWidgetInfoToLatestVersion = upgradeWidgetInfoToLatestVersion;
|
|
4239
|
+
exports.usesNumCorrect = usesNumCorrect;
|
|
4240
|
+
exports.videoLogic = videoWidgetLogic;
|
|
3143
4241
|
//# sourceMappingURL=index.js.map
|