@khanacademy/perseus-core 7.0.0 → 7.0.2
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 +241 -4027
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +182 -4008
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -29,580 +29,17 @@ function _interopNamespaceCompat(e) {
|
|
|
29
29
|
var ___default = /*#__PURE__*/_interopDefaultCompat(_);
|
|
30
30
|
var KAS__namespace = /*#__PURE__*/_interopNamespaceCompat(KAS);
|
|
31
31
|
|
|
32
|
-
function getMatrixSize(matrix) {
|
|
33
|
-
const matrixSize = [1, 1];
|
|
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) => {
|
|
38
|
-
let rowWidth = 0;
|
|
39
|
-
___default.default(matrixRow).each((matrixCol, col) => {
|
|
40
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
41
|
-
if (matrixCol != null && matrixCol.toString().length) {
|
|
42
|
-
rowWidth = col + 1;
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Matrix width:
|
|
47
|
-
matrixSize[1] = Math.max(matrixSize[1], rowWidth);
|
|
48
|
-
|
|
49
|
-
// Matrix height:
|
|
50
|
-
if (rowWidth > 0) {
|
|
51
|
-
matrixSize[0] = Math.max(matrixSize[0], row + 1);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
return matrixSize;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Get the character used for separating decimals.
|
|
59
|
-
*/
|
|
60
|
-
const getDecimalSeparator = locale => {
|
|
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
|
|
68
|
-
case "ka":
|
|
69
|
-
return ",";
|
|
70
|
-
default:
|
|
71
|
-
const numberWithDecimalSeparator = 1.1;
|
|
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] ?? ".";
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* APPROXIMATE equality on numbers and primitives.
|
|
86
|
-
*/
|
|
87
|
-
function approximateEqual(x, y) {
|
|
88
|
-
if (typeof x === "number" && typeof y === "number") {
|
|
89
|
-
return Math.abs(x - y) < 1e-9;
|
|
90
|
-
}
|
|
91
|
-
return x === y;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Deep APPROXIMATE equality on primitives, numbers, arrays, and objects.
|
|
96
|
-
* Recursive.
|
|
97
|
-
*/
|
|
98
|
-
function approximateDeepEqual(x, y) {
|
|
99
|
-
if (Array.isArray(x) && Array.isArray(y)) {
|
|
100
|
-
if (x.length !== y.length) {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
for (let i = 0; i < x.length; i++) {
|
|
104
|
-
if (!approximateDeepEqual(x[i], y[i])) {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
if (Array.isArray(x) || Array.isArray(y)) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
if (typeof x === "function" && typeof y === "function") {
|
|
114
|
-
return approximateEqual(x, y);
|
|
115
|
-
}
|
|
116
|
-
if (typeof x === "function" || typeof y === "function") {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
if (typeof x === "object" && typeof y === "object" && !!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'.
|
|
122
|
-
return approximateDeepEqual(y[k], v);
|
|
123
|
-
}) && ___default.default.all(y, function (v, k) {
|
|
124
|
-
// @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
|
|
125
|
-
return approximateDeepEqual(x[k], v);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
return approximateEqual(x, y);
|
|
132
|
-
}
|
|
32
|
+
function getMatrixSize(matrix){const matrixSize=[1,1];___default.default(matrix).each((matrixRow,row)=>{let rowWidth=0;___default.default(matrixRow).each((matrixCol,col)=>{if(matrixCol!=null&&matrixCol.toString().length){rowWidth=col+1;}});matrixSize[1]=Math.max(matrixSize[1],rowWidth);if(rowWidth>0){matrixSize[0]=Math.max(matrixSize[0],row+1);}});return matrixSize}
|
|
133
33
|
|
|
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
|
-
*/
|
|
141
|
-
function addWidget(id) {
|
|
142
|
-
return `[[☃ ${id}]]`;
|
|
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
|
-
*/
|
|
152
|
-
function getWidgetRegex() {
|
|
153
|
-
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
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
|
-
*/
|
|
166
|
-
function getWidgetIdsFromContent(content) {
|
|
167
|
-
const widgets = [];
|
|
168
|
-
const localWidgetRegex = getWidgetRegex();
|
|
169
|
-
let match = localWidgetRegex.exec(content);
|
|
170
|
-
while (match !== null) {
|
|
171
|
-
widgets.push(match[1]);
|
|
172
|
-
match = localWidgetRegex.exec(content);
|
|
173
|
-
}
|
|
174
|
-
return widgets;
|
|
175
|
-
}
|
|
34
|
+
const getDecimalSeparator=locale=>{switch(locale){case "ka":return ",";default:const numberWithDecimalSeparator=1.1;const match=new Intl.NumberFormat(locale).format(numberWithDecimalSeparator).match(/[^\d\u0661\u06F1\u0967\u09e7]/);return match?.[0]??"."}};
|
|
176
35
|
|
|
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
|
-
*/
|
|
186
|
-
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
187
|
-
const rv = [];
|
|
188
|
-
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
189
|
-
widgetIdsInContent.forEach(widgetId => {
|
|
190
|
-
const widget = widgetMap[widgetId];
|
|
191
|
-
if (widget?.type === type) {
|
|
192
|
-
rv.push(widgetId);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
return rv;
|
|
196
|
-
}
|
|
36
|
+
function approximateEqual(x,y){if(typeof x==="number"&&typeof y==="number"){return Math.abs(x-y)<1e-9}return x===y}function approximateDeepEqual(x,y){if(Array.isArray(x)&&Array.isArray(y)){if(x.length!==y.length){return false}for(let i=0;i<x.length;i++){if(!approximateDeepEqual(x[i],y[i])){return false}}return true}if(Array.isArray(x)||Array.isArray(y)){return false}if(typeof x==="function"&&typeof y==="function"){return approximateEqual(x,y)}if(typeof x==="function"||typeof y==="function"){return false}if(typeof x==="object"&&typeof y==="object"&&!!x&&!!y){return x===y||___default.default.all(x,function(v,k){return approximateDeepEqual(y[k],v)})&&___default.default.all(y,function(v,k){return approximateDeepEqual(x[k],v)})}if(typeof x==="object"&&!!x||typeof y==="object"&&!!y){return false}return approximateEqual(x,y)}
|
|
197
37
|
|
|
198
|
-
|
|
199
|
-
// Record<string, Cloneable> as well. Currently, it only does arrays.
|
|
38
|
+
function addWidget(id){return `[[☃ ${id}]]`}function getWidgetRegex(){return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g}function getWidgetIdsFromContent(content){const widgets=[];const localWidgetRegex=getWidgetRegex();let match=localWidgetRegex.exec(content);while(match!==null){widgets.push(match[1]);match=localWidgetRegex.exec(content);}return widgets}function getWidgetIdsFromContentByType(type,content,widgetMap){const rv=[];const widgetIdsInContent=getWidgetIdsFromContent(content);widgetIdsInContent.forEach(widgetId=>{const widget=widgetMap[widgetId];if(widget?.type===type){rv.push(widgetId);}});return rv}
|
|
200
39
|
|
|
201
|
-
function deepClone(obj)
|
|
202
|
-
if (Array.isArray(obj)) {
|
|
203
|
-
return obj.map(deepClone);
|
|
204
|
-
}
|
|
205
|
-
return obj;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const MOVABLES = {
|
|
209
|
-
PLOT: "PLOT",
|
|
210
|
-
PARABOLA: "PARABOLA",
|
|
211
|
-
SINUSOID: "SINUSOID"
|
|
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.
|
|
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.
|
|
220
|
-
let amplitude = coeffs[0];
|
|
221
|
-
let angularFrequency = coeffs[1];
|
|
222
|
-
let phase = coeffs[2];
|
|
223
|
-
const verticalOffset = coeffs[3];
|
|
224
|
-
|
|
225
|
-
// Guarantee a > 0
|
|
226
|
-
if (amplitude < 0) {
|
|
227
|
-
amplitude *= -1;
|
|
228
|
-
angularFrequency *= -1;
|
|
229
|
-
phase *= -1;
|
|
230
|
-
}
|
|
231
|
-
const period = 2 * Math.PI;
|
|
232
|
-
// Guarantee b > 0
|
|
233
|
-
if (angularFrequency < 0) {
|
|
234
|
-
angularFrequency *= -1;
|
|
235
|
-
phase *= -1;
|
|
236
|
-
phase += period / 2;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Guarantee c is smallest possible positive value
|
|
240
|
-
while (phase > 0) {
|
|
241
|
-
phase -= period;
|
|
242
|
-
}
|
|
243
|
-
while (phase < 0) {
|
|
244
|
-
phase += period;
|
|
245
|
-
}
|
|
246
|
-
return [amplitude, angularFrequency, phase, verticalOffset];
|
|
247
|
-
}
|
|
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.
|
|
252
|
-
let amplitude = coeffs[0];
|
|
253
|
-
let angularFrequency = coeffs[1];
|
|
254
|
-
let phase = coeffs[2];
|
|
255
|
-
const verticalOffset = coeffs[3];
|
|
256
|
-
|
|
257
|
-
// Guarantee a > 0
|
|
258
|
-
if (amplitude < 0) {
|
|
259
|
-
amplitude *= -1;
|
|
260
|
-
angularFrequency *= -1;
|
|
261
|
-
phase *= -1;
|
|
262
|
-
}
|
|
263
|
-
const period = Math.PI;
|
|
264
|
-
// Guarantee b > 0
|
|
265
|
-
if (angularFrequency < 0) {
|
|
266
|
-
angularFrequency *= -1;
|
|
267
|
-
phase *= -1;
|
|
268
|
-
phase += period / 2;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Guarantee c is smallest possible positive value
|
|
272
|
-
while (phase > 0) {
|
|
273
|
-
phase -= period;
|
|
274
|
-
}
|
|
275
|
-
while (phase < 0) {
|
|
276
|
-
phase += period;
|
|
277
|
-
}
|
|
278
|
-
return [amplitude, angularFrequency, phase, verticalOffset];
|
|
279
|
-
}
|
|
280
|
-
const PlotDefaults = {
|
|
281
|
-
areEqual: function (coeffs1, coeffs2) {
|
|
282
|
-
return approximateDeepEqual(coeffs1, coeffs2);
|
|
283
|
-
},
|
|
284
|
-
movable: MOVABLES.PLOT,
|
|
285
|
-
getPropsForCoeffs: function (coeffs) {
|
|
286
|
-
return {
|
|
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)
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
const Linear = ___default.default.extend({}, PlotDefaults, {
|
|
293
|
-
url: "https://ka-perseus-graphie.s3.amazonaws.com/67aaf581e6d9ef9038c10558a1f70ac21c11c9f8.png",
|
|
294
|
-
defaultCoords: [[0.25, 0.75], [0.75, 0.75]],
|
|
295
|
-
getCoefficients: function (coords) {
|
|
296
|
-
const p1 = coords[0];
|
|
297
|
-
const p2 = coords[1];
|
|
298
|
-
const denom = p2[0] - p1[0];
|
|
299
|
-
const num = p2[1] - p1[1];
|
|
300
|
-
if (denom === 0) {
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
const m = num / denom;
|
|
304
|
-
const b = p2[1] - m * p2[0];
|
|
305
|
-
return [m, b];
|
|
306
|
-
},
|
|
307
|
-
getFunctionForCoeffs: function (coeffs, x) {
|
|
308
|
-
const m = coeffs[0];
|
|
309
|
-
const b = coeffs[1];
|
|
310
|
-
return m * x + b;
|
|
311
|
-
},
|
|
312
|
-
getEquationString: function (coords) {
|
|
313
|
-
const coeffs = this.getCoefficients(coords);
|
|
314
|
-
const m = coeffs[0];
|
|
315
|
-
const b = coeffs[1];
|
|
316
|
-
return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
const Quadratic = ___default.default.extend({}, PlotDefaults, {
|
|
320
|
-
url: "https://ka-perseus-graphie.s3.amazonaws.com/e23d36e6fc29ee37174e92c9daba2a66677128ab.png",
|
|
321
|
-
defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
|
|
322
|
-
movable: MOVABLES.PARABOLA,
|
|
323
|
-
getCoefficients: function (coords) {
|
|
324
|
-
const p1 = coords[0];
|
|
325
|
-
const p2 = coords[1];
|
|
326
|
-
|
|
327
|
-
// Parabola with vertex (h, k) has form: y = a * (h - k)^2 + k
|
|
328
|
-
const h = p1[0];
|
|
329
|
-
const k = p1[1];
|
|
330
|
-
|
|
331
|
-
// Use these to calculate familiar a, b, c
|
|
332
|
-
const a = (p2[1] - k) / ((p2[0] - h) * (p2[0] - h));
|
|
333
|
-
const b = -2 * h * a;
|
|
334
|
-
const c = a * h * h + k;
|
|
335
|
-
return [a, b, c];
|
|
336
|
-
},
|
|
337
|
-
getFunctionForCoeffs: function (coeffs, x) {
|
|
338
|
-
const a = coeffs[0];
|
|
339
|
-
const b = coeffs[1];
|
|
340
|
-
const c = coeffs[2];
|
|
341
|
-
return (a * x + b) * x + c;
|
|
342
|
-
},
|
|
343
|
-
getPropsForCoeffs: function (coeffs) {
|
|
344
|
-
return {
|
|
345
|
-
a: coeffs[0],
|
|
346
|
-
b: coeffs[1],
|
|
347
|
-
c: coeffs[2]
|
|
348
|
-
};
|
|
349
|
-
},
|
|
350
|
-
getEquationString: function (coords) {
|
|
351
|
-
const coeffs = this.getCoefficients(coords);
|
|
352
|
-
const a = coeffs[0];
|
|
353
|
-
const b = coeffs[1];
|
|
354
|
-
const c = coeffs[2];
|
|
355
|
-
return "y = " + a.toFixed(3) + "x^2 + " + b.toFixed(3) + "x + " + c.toFixed(3);
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
const Sinusoid = ___default.default.extend({}, PlotDefaults, {
|
|
359
|
-
url: "https://ka-perseus-graphie.s3.amazonaws.com/3d68e7718498475f53b206c2ab285626baf8857e.png",
|
|
360
|
-
defaultCoords: [[0.5, 0.5], [0.6, 0.6]],
|
|
361
|
-
movable: MOVABLES.SINUSOID,
|
|
362
|
-
getCoefficients: function (coords) {
|
|
363
|
-
const p1 = coords[0];
|
|
364
|
-
const p2 = coords[1];
|
|
365
|
-
const a = p2[1] - p1[1];
|
|
366
|
-
const b = Math.PI / (2 * (p2[0] - p1[0]));
|
|
367
|
-
const c = p1[0] * b;
|
|
368
|
-
const d = p1[1];
|
|
369
|
-
return [a, b, c, d];
|
|
370
|
-
},
|
|
371
|
-
getFunctionForCoeffs: function (coeffs, x) {
|
|
372
|
-
const a = coeffs[0];
|
|
373
|
-
const b = coeffs[1];
|
|
374
|
-
const c = coeffs[2];
|
|
375
|
-
const d = coeffs[3];
|
|
376
|
-
return a * Math.sin(b * x - c) + d;
|
|
377
|
-
},
|
|
378
|
-
getPropsForCoeffs: function (coeffs) {
|
|
379
|
-
return {
|
|
380
|
-
a: coeffs[0],
|
|
381
|
-
b: coeffs[1],
|
|
382
|
-
c: coeffs[2],
|
|
383
|
-
d: coeffs[3]
|
|
384
|
-
};
|
|
385
|
-
},
|
|
386
|
-
getEquationString: function (coords) {
|
|
387
|
-
const coeffs = this.getCoefficients(coords);
|
|
388
|
-
const a = coeffs[0];
|
|
389
|
-
const b = coeffs[1];
|
|
390
|
-
const c = coeffs[2];
|
|
391
|
-
const d = coeffs[3];
|
|
392
|
-
return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
|
|
393
|
-
},
|
|
394
|
-
areEqual: function (coeffs1, coeffs2) {
|
|
395
|
-
return approximateDeepEqual(canonicalSineCoefficients(coeffs1), canonicalSineCoefficients(coeffs2));
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
const Tangent = ___default.default.extend({}, PlotDefaults, {
|
|
399
|
-
url: "https://ka-perseus-graphie.s3.amazonaws.com/7db80d23c35214f98659fe1cf0765811c1bbfbba.png",
|
|
400
|
-
defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
|
|
401
|
-
getCoefficients: function (coords) {
|
|
402
|
-
const p1 = coords[0];
|
|
403
|
-
const p2 = coords[1];
|
|
404
|
-
const a = p2[1] - p1[1];
|
|
405
|
-
const b = Math.PI / (4 * (p2[0] - p1[0]));
|
|
406
|
-
const c = p1[0] * b;
|
|
407
|
-
const d = p1[1];
|
|
408
|
-
return [a, b, c, d];
|
|
409
|
-
},
|
|
410
|
-
getFunctionForCoeffs: function (coeffs, x) {
|
|
411
|
-
const a = coeffs[0];
|
|
412
|
-
const b = coeffs[1];
|
|
413
|
-
const c = coeffs[2];
|
|
414
|
-
const d = coeffs[3];
|
|
415
|
-
return a * Math.tan(b * x - c) + d;
|
|
416
|
-
},
|
|
417
|
-
getEquationString: function (coords) {
|
|
418
|
-
const coeffs = this.getCoefficients(coords);
|
|
419
|
-
const a = coeffs[0];
|
|
420
|
-
const b = coeffs[1];
|
|
421
|
-
const c = coeffs[2];
|
|
422
|
-
const d = coeffs[3];
|
|
423
|
-
return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
|
|
424
|
-
},
|
|
425
|
-
areEqual: function (coeffs1, coeffs2) {
|
|
426
|
-
return approximateDeepEqual(canonicalTangentCoefficients(coeffs1), canonicalTangentCoefficients(coeffs2));
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
const Exponential = ___default.default.extend({}, PlotDefaults, {
|
|
430
|
-
url: "https://ka-perseus-graphie.s3.amazonaws.com/9cbfad55525e3ce755a31a631b074670a5dad611.png",
|
|
431
|
-
defaultCoords: [[0.5, 0.55], [0.75, 0.75]],
|
|
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
|
-
*/
|
|
450
|
-
extraCoordConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
|
|
451
|
-
const y = asymptote[0][1];
|
|
452
|
-
return ___default.default.all(coords, coord => coord[1] !== y);
|
|
453
|
-
},
|
|
454
|
-
extraAsymptoteConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
|
|
455
|
-
const y = newCoord[1];
|
|
456
|
-
const isValid = ___default.default.all(coords, coord => coord[1] > y) || ___default.default.all(coords, coord => coord[1] < y);
|
|
457
|
-
if (isValid) {
|
|
458
|
-
return [oldCoord[0], y];
|
|
459
|
-
}
|
|
460
|
-
// Snap the asymptote as close as possible, i.e., if the user moves
|
|
461
|
-
// the mouse really quickly into an invalid region
|
|
462
|
-
const oldY = oldCoord[1];
|
|
463
|
-
const wasBelow = ___default.default.all(coords, coord => coord[1] > oldY);
|
|
464
|
-
if (wasBelow) {
|
|
465
|
-
const bottomMost = ___default.default.min(___default.default.map(coords, coord => coord[1]));
|
|
466
|
-
return [oldCoord[0], bottomMost - graph.snapStep[1]];
|
|
467
|
-
}
|
|
468
|
-
const topMost = ___default.default.max(___default.default.map(coords, coord => coord[1]));
|
|
469
|
-
return [oldCoord[0], topMost + graph.snapStep[1]];
|
|
470
|
-
},
|
|
471
|
-
allowReflectOverAsymptote: true,
|
|
472
|
-
getCoefficients: function (coords, asymptote) {
|
|
473
|
-
const p1 = coords[0];
|
|
474
|
-
const p2 = coords[1];
|
|
475
|
-
const c = asymptote[0][1];
|
|
476
|
-
const b = Math.log((p1[1] - c) / (p2[1] - c)) / (p1[0] - p2[0]);
|
|
477
|
-
const a = (p1[1] - c) / Math.exp(b * p1[0]);
|
|
478
|
-
return [a, b, c];
|
|
479
|
-
},
|
|
480
|
-
getFunctionForCoeffs: function (coeffs, x) {
|
|
481
|
-
const a = coeffs[0];
|
|
482
|
-
const b = coeffs[1];
|
|
483
|
-
const c = coeffs[2];
|
|
484
|
-
return a * Math.exp(b * x) + c;
|
|
485
|
-
},
|
|
486
|
-
getEquationString: function (coords, asymptote) {
|
|
487
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
488
|
-
if (!asymptote) {
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
491
|
-
const coeffs = this.getCoefficients(coords, asymptote);
|
|
492
|
-
const a = coeffs[0];
|
|
493
|
-
const b = coeffs[1];
|
|
494
|
-
const c = coeffs[2];
|
|
495
|
-
return "y = " + a.toFixed(3) + "e^(" + b.toFixed(3) + "x) + " + c.toFixed(3);
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
const Logarithm = ___default.default.extend({}, PlotDefaults, {
|
|
499
|
-
url: "https://ka-perseus-graphie.s3.amazonaws.com/f6491e99d34af34d924bfe0231728ad912068dc3.png",
|
|
500
|
-
defaultCoords: [[0.55, 0.5], [0.75, 0.75]],
|
|
501
|
-
defaultAsymptote: [[0.5, 0], [0.5, 1.0]],
|
|
502
|
-
extraCoordConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
|
|
503
|
-
const x = asymptote[0][0];
|
|
504
|
-
return ___default.default.all(coords, coord => coord[0] !== x) && coords[0][1] !== coords[1][1];
|
|
505
|
-
},
|
|
506
|
-
extraAsymptoteConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
|
|
507
|
-
const x = newCoord[0];
|
|
508
|
-
const isValid = ___default.default.all(coords, coord => coord[0] > x) || ___default.default.all(coords, coord => coord[0] < x);
|
|
509
|
-
if (isValid) {
|
|
510
|
-
return [x, oldCoord[1]];
|
|
511
|
-
}
|
|
512
|
-
// Snap the asymptote as close as possible, i.e., if the user moves
|
|
513
|
-
// the mouse really quickly into an invalid region
|
|
514
|
-
const oldX = oldCoord[0];
|
|
515
|
-
const wasLeft = ___default.default.all(coords, coord => coord[0] > oldX);
|
|
516
|
-
if (wasLeft) {
|
|
517
|
-
const leftMost = ___default.default.min(___default.default.map(coords, coord => coord[0]));
|
|
518
|
-
return [leftMost - graph.snapStep[0], oldCoord[1]];
|
|
519
|
-
}
|
|
520
|
-
const rightMost = ___default.default.max(___default.default.map(coords, coord => coord[0]));
|
|
521
|
-
return [rightMost + graph.snapStep[0], oldCoord[1]];
|
|
522
|
-
},
|
|
523
|
-
allowReflectOverAsymptote: true,
|
|
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'.
|
|
529
|
-
const flip = coord => [coord[1], coord[0]];
|
|
530
|
-
const inverseCoeffs = Exponential.getCoefficients(___default.default.map(coords, flip), ___default.default.map(asymptote, flip));
|
|
531
|
-
if (inverseCoeffs) {
|
|
532
|
-
const c = -inverseCoeffs[2] / inverseCoeffs[0];
|
|
533
|
-
const b = 1 / inverseCoeffs[0];
|
|
534
|
-
const a = 1 / inverseCoeffs[1];
|
|
535
|
-
return [a, b, c];
|
|
536
|
-
}
|
|
537
|
-
},
|
|
538
|
-
getFunctionForCoeffs: function (coeffs, x, asymptote) {
|
|
539
|
-
const a = coeffs[0];
|
|
540
|
-
const b = coeffs[1];
|
|
541
|
-
const c = coeffs[2];
|
|
542
|
-
return a * Math.log(b * x + c);
|
|
543
|
-
},
|
|
544
|
-
getEquationString: function (coords, asymptote) {
|
|
545
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
546
|
-
if (!asymptote) {
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
const coeffs = this.getCoefficients(coords, asymptote);
|
|
550
|
-
const a = coeffs[0];
|
|
551
|
-
const b = coeffs[1];
|
|
552
|
-
const c = coeffs[2];
|
|
553
|
-
return "y = ln(" + a.toFixed(3) + "x + " + b.toFixed(3) + ") + " + c.toFixed(3);
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
const AbsoluteValue = ___default.default.extend({}, PlotDefaults, {
|
|
557
|
-
url: "https://ka-perseus-graphie.s3.amazonaws.com/8256a630175a0cb1d11de223d6de0266daf98721.png",
|
|
558
|
-
defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
|
|
559
|
-
getCoefficients: function (coords) {
|
|
560
|
-
const p1 = coords[0];
|
|
561
|
-
const p2 = coords[1];
|
|
562
|
-
const denom = p2[0] - p1[0];
|
|
563
|
-
const num = p2[1] - p1[1];
|
|
564
|
-
if (denom === 0) {
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
let m = Math.abs(num / denom);
|
|
568
|
-
if (p2[1] < p1[1]) {
|
|
569
|
-
m *= -1;
|
|
570
|
-
}
|
|
571
|
-
const horizontalOffset = p1[0];
|
|
572
|
-
const verticalOffset = p1[1];
|
|
573
|
-
return [m, horizontalOffset, verticalOffset];
|
|
574
|
-
},
|
|
575
|
-
getFunctionForCoeffs: function (coeffs, x) {
|
|
576
|
-
const m = coeffs[0];
|
|
577
|
-
const horizontalOffset = coeffs[1];
|
|
578
|
-
const verticalOffset = coeffs[2];
|
|
579
|
-
return m * Math.abs(x - horizontalOffset) + verticalOffset;
|
|
580
|
-
},
|
|
581
|
-
getEquationString: function (coords) {
|
|
582
|
-
const coeffs = this.getCoefficients(coords);
|
|
583
|
-
const m = coeffs[0];
|
|
584
|
-
const horizontalOffset = coeffs[1];
|
|
585
|
-
const verticalOffset = coeffs[2];
|
|
586
|
-
return "y = " + m.toFixed(3) + "| x - " + horizontalOffset.toFixed(3) + "| + " + verticalOffset.toFixed(3);
|
|
587
|
-
}
|
|
588
|
-
});
|
|
40
|
+
function deepClone(obj){if(Array.isArray(obj)){return obj.map(deepClone)}return obj}
|
|
589
41
|
|
|
590
|
-
|
|
591
|
-
const functionTypeMapping = {
|
|
592
|
-
linear: Linear,
|
|
593
|
-
quadratic: Quadratic,
|
|
594
|
-
sinusoid: Sinusoid,
|
|
595
|
-
tangent: Tangent,
|
|
596
|
-
exponential: Exponential,
|
|
597
|
-
logarithm: Logarithm,
|
|
598
|
-
absolute_value: AbsoluteValue
|
|
599
|
-
};
|
|
600
|
-
const allTypes = ___default.default.keys(functionTypeMapping);
|
|
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.
|
|
604
|
-
return functionTypeMapping[type];
|
|
605
|
-
}
|
|
42
|
+
const MOVABLES={PLOT:"PLOT",PARABOLA:"PARABOLA",SINUSOID:"SINUSOID"};function canonicalSineCoefficients(coeffs){let amplitude=coeffs[0];let angularFrequency=coeffs[1];let phase=coeffs[2];const verticalOffset=coeffs[3];if(amplitude<0){amplitude*=-1;angularFrequency*=-1;phase*=-1;}const period=2*Math.PI;if(angularFrequency<0){angularFrequency*=-1;phase*=-1;phase+=period/2;}while(phase>0){phase-=period;}while(phase<0){phase+=period;}return [amplitude,angularFrequency,phase,verticalOffset]}function canonicalTangentCoefficients(coeffs){let amplitude=coeffs[0];let angularFrequency=coeffs[1];let phase=coeffs[2];const verticalOffset=coeffs[3];if(amplitude<0){amplitude*=-1;angularFrequency*=-1;phase*=-1;}const period=Math.PI;if(angularFrequency<0){angularFrequency*=-1;phase*=-1;phase+=period/2;}while(phase>0){phase-=period;}while(phase<0){phase+=period;}return [amplitude,angularFrequency,phase,verticalOffset]}const PlotDefaults={areEqual:function(coeffs1,coeffs2){return approximateDeepEqual(coeffs1,coeffs2)},movable:MOVABLES.PLOT,getPropsForCoeffs:function(coeffs){return {fn:___default.default.partial(this.getFunctionForCoeffs,coeffs)}}};const Linear=___default.default.extend({},PlotDefaults,{url:"https://ka-perseus-graphie.s3.amazonaws.com/67aaf581e6d9ef9038c10558a1f70ac21c11c9f8.png",defaultCoords:[[.25,.75],[.75,.75]],getCoefficients:function(coords){const p1=coords[0];const p2=coords[1];const denom=p2[0]-p1[0];const num=p2[1]-p1[1];if(denom===0){return}const m=num/denom;const b=p2[1]-m*p2[0];return [m,b]},getFunctionForCoeffs:function(coeffs,x){const m=coeffs[0];const b=coeffs[1];return m*x+b},getEquationString:function(coords){const coeffs=this.getCoefficients(coords);const m=coeffs[0];const b=coeffs[1];return "y = "+m.toFixed(3)+"x + "+b.toFixed(3)}});const Quadratic=___default.default.extend({},PlotDefaults,{url:"https://ka-perseus-graphie.s3.amazonaws.com/e23d36e6fc29ee37174e92c9daba2a66677128ab.png",defaultCoords:[[.5,.5],[.75,.75]],movable:MOVABLES.PARABOLA,getCoefficients:function(coords){const p1=coords[0];const p2=coords[1];const h=p1[0];const k=p1[1];const a=(p2[1]-k)/((p2[0]-h)*(p2[0]-h));const b=-2*h*a;const c=a*h*h+k;return [a,b,c]},getFunctionForCoeffs:function(coeffs,x){const a=coeffs[0];const b=coeffs[1];const c=coeffs[2];return (a*x+b)*x+c},getPropsForCoeffs:function(coeffs){return {a:coeffs[0],b:coeffs[1],c:coeffs[2]}},getEquationString:function(coords){const coeffs=this.getCoefficients(coords);const a=coeffs[0];const b=coeffs[1];const c=coeffs[2];return "y = "+a.toFixed(3)+"x^2 + "+b.toFixed(3)+"x + "+c.toFixed(3)}});const Sinusoid=___default.default.extend({},PlotDefaults,{url:"https://ka-perseus-graphie.s3.amazonaws.com/3d68e7718498475f53b206c2ab285626baf8857e.png",defaultCoords:[[.5,.5],[.6,.6]],movable:MOVABLES.SINUSOID,getCoefficients:function(coords){const p1=coords[0];const p2=coords[1];const a=p2[1]-p1[1];const b=Math.PI/(2*(p2[0]-p1[0]));const c=p1[0]*b;const d=p1[1];return [a,b,c,d]},getFunctionForCoeffs:function(coeffs,x){const a=coeffs[0];const b=coeffs[1];const c=coeffs[2];const d=coeffs[3];return a*Math.sin(b*x-c)+d},getPropsForCoeffs:function(coeffs){return {a:coeffs[0],b:coeffs[1],c:coeffs[2],d:coeffs[3]}},getEquationString:function(coords){const coeffs=this.getCoefficients(coords);const a=coeffs[0];const b=coeffs[1];const c=coeffs[2];const d=coeffs[3];return "y = "+a.toFixed(3)+" sin("+b.toFixed(3)+"x - "+c.toFixed(3)+") + "+d.toFixed(3)},areEqual:function(coeffs1,coeffs2){return approximateDeepEqual(canonicalSineCoefficients(coeffs1),canonicalSineCoefficients(coeffs2))}});const Tangent=___default.default.extend({},PlotDefaults,{url:"https://ka-perseus-graphie.s3.amazonaws.com/7db80d23c35214f98659fe1cf0765811c1bbfbba.png",defaultCoords:[[.5,.5],[.75,.75]],getCoefficients:function(coords){const p1=coords[0];const p2=coords[1];const a=p2[1]-p1[1];const b=Math.PI/(4*(p2[0]-p1[0]));const c=p1[0]*b;const d=p1[1];return [a,b,c,d]},getFunctionForCoeffs:function(coeffs,x){const a=coeffs[0];const b=coeffs[1];const c=coeffs[2];const d=coeffs[3];return a*Math.tan(b*x-c)+d},getEquationString:function(coords){const coeffs=this.getCoefficients(coords);const a=coeffs[0];const b=coeffs[1];const c=coeffs[2];const d=coeffs[3];return "y = "+a.toFixed(3)+" sin("+b.toFixed(3)+"x - "+c.toFixed(3)+") + "+d.toFixed(3)},areEqual:function(coeffs1,coeffs2){return approximateDeepEqual(canonicalTangentCoefficients(coeffs1),canonicalTangentCoefficients(coeffs2))}});const Exponential=___default.default.extend({},PlotDefaults,{url:"https://ka-perseus-graphie.s3.amazonaws.com/9cbfad55525e3ce755a31a631b074670a5dad611.png",defaultCoords:[[.5,.55],[.75,.75]],defaultAsymptote:[[0,.5],[1,.5]],extraCoordConstraint:function(newCoord,oldCoord,coords,asymptote,graph){const y=asymptote[0][1];return ___default.default.all(coords,coord=>coord[1]!==y)},extraAsymptoteConstraint:function(newCoord,oldCoord,coords,asymptote,graph){const y=newCoord[1];const isValid=___default.default.all(coords,coord=>coord[1]>y)||___default.default.all(coords,coord=>coord[1]<y);if(isValid){return [oldCoord[0],y]}const oldY=oldCoord[1];const wasBelow=___default.default.all(coords,coord=>coord[1]>oldY);if(wasBelow){const bottomMost=___default.default.min(___default.default.map(coords,coord=>coord[1]));return [oldCoord[0],bottomMost-graph.snapStep[1]]}const topMost=___default.default.max(___default.default.map(coords,coord=>coord[1]));return [oldCoord[0],topMost+graph.snapStep[1]]},allowReflectOverAsymptote:true,getCoefficients:function(coords,asymptote){const p1=coords[0];const p2=coords[1];const c=asymptote[0][1];const b=Math.log((p1[1]-c)/(p2[1]-c))/(p1[0]-p2[0]);const a=(p1[1]-c)/Math.exp(b*p1[0]);return [a,b,c]},getFunctionForCoeffs:function(coeffs,x){const a=coeffs[0];const b=coeffs[1];const c=coeffs[2];return a*Math.exp(b*x)+c},getEquationString:function(coords,asymptote){if(!asymptote){return null}const coeffs=this.getCoefficients(coords,asymptote);const a=coeffs[0];const b=coeffs[1];const c=coeffs[2];return "y = "+a.toFixed(3)+"e^("+b.toFixed(3)+"x) + "+c.toFixed(3)}});const Logarithm=___default.default.extend({},PlotDefaults,{url:"https://ka-perseus-graphie.s3.amazonaws.com/f6491e99d34af34d924bfe0231728ad912068dc3.png",defaultCoords:[[.55,.5],[.75,.75]],defaultAsymptote:[[.5,0],[.5,1]],extraCoordConstraint:function(newCoord,oldCoord,coords,asymptote,graph){const x=asymptote[0][0];return ___default.default.all(coords,coord=>coord[0]!==x)&&coords[0][1]!==coords[1][1]},extraAsymptoteConstraint:function(newCoord,oldCoord,coords,asymptote,graph){const x=newCoord[0];const isValid=___default.default.all(coords,coord=>coord[0]>x)||___default.default.all(coords,coord=>coord[0]<x);if(isValid){return [x,oldCoord[1]]}const oldX=oldCoord[0];const wasLeft=___default.default.all(coords,coord=>coord[0]>oldX);if(wasLeft){const leftMost=___default.default.min(___default.default.map(coords,coord=>coord[0]));return [leftMost-graph.snapStep[0],oldCoord[1]]}const rightMost=___default.default.max(___default.default.map(coords,coord=>coord[0]));return [rightMost+graph.snapStep[0],oldCoord[1]]},allowReflectOverAsymptote:true,getCoefficients:function(coords,asymptote){const flip=coord=>[coord[1],coord[0]];const inverseCoeffs=Exponential.getCoefficients(___default.default.map(coords,flip),___default.default.map(asymptote,flip));if(inverseCoeffs){const c=-inverseCoeffs[2]/inverseCoeffs[0];const b=1/inverseCoeffs[0];const a=1/inverseCoeffs[1];return [a,b,c]}},getFunctionForCoeffs:function(coeffs,x,asymptote){const a=coeffs[0];const b=coeffs[1];const c=coeffs[2];return a*Math.log(b*x+c)},getEquationString:function(coords,asymptote){if(!asymptote){return null}const coeffs=this.getCoefficients(coords,asymptote);const a=coeffs[0];const b=coeffs[1];const c=coeffs[2];return "y = ln("+a.toFixed(3)+"x + "+b.toFixed(3)+") + "+c.toFixed(3)}});const AbsoluteValue=___default.default.extend({},PlotDefaults,{url:"https://ka-perseus-graphie.s3.amazonaws.com/8256a630175a0cb1d11de223d6de0266daf98721.png",defaultCoords:[[.5,.5],[.75,.75]],getCoefficients:function(coords){const p1=coords[0];const p2=coords[1];const denom=p2[0]-p1[0];const num=p2[1]-p1[1];if(denom===0){return}let m=Math.abs(num/denom);if(p2[1]<p1[1]){m*=-1;}const horizontalOffset=p1[0];const verticalOffset=p1[1];return [m,horizontalOffset,verticalOffset]},getFunctionForCoeffs:function(coeffs,x){const m=coeffs[0];const horizontalOffset=coeffs[1];const verticalOffset=coeffs[2];return m*Math.abs(x-horizontalOffset)+verticalOffset},getEquationString:function(coords){const coeffs=this.getCoefficients(coords);const m=coeffs[0];const horizontalOffset=coeffs[1];const verticalOffset=coeffs[2];return "y = "+m.toFixed(3)+"| x - "+horizontalOffset.toFixed(3)+"| + "+verticalOffset.toFixed(3)}});const functionTypeMapping={linear:Linear,quadratic:Quadratic,sinusoid:Sinusoid,tangent:Tangent,exponential:Exponential,logarithm:Logarithm,absolute_value:AbsoluteValue};const allTypes=___default.default.keys(functionTypeMapping);function functionForType(type){return functionTypeMapping[type]}
|
|
606
43
|
|
|
607
44
|
var grapherUtil = /*#__PURE__*/Object.freeze({
|
|
608
45
|
__proto__: null,
|
|
@@ -611,3386 +48,269 @@ var grapherUtil = /*#__PURE__*/Object.freeze({
|
|
|
611
48
|
functionForType: functionForType
|
|
612
49
|
});
|
|
613
50
|
|
|
614
|
-
function isRealJSONParse(jsonParse) {
|
|
615
|
-
const randomPhrase = buildRandomPhrase();
|
|
616
|
-
const randomHintPhrase = buildRandomPhrase();
|
|
617
|
-
const randomString = buildRandomString();
|
|
618
|
-
const testingObject = JSON.stringify({
|
|
619
|
-
answerArea: {
|
|
620
|
-
calculator: false,
|
|
621
|
-
chi2Table: false,
|
|
622
|
-
financialCalculatorMonthlyPayment: false,
|
|
623
|
-
financialCalculatorTimeToPayOff: false,
|
|
624
|
-
financialCalculatorTotalAmount: false,
|
|
625
|
-
periodicTable: false,
|
|
626
|
-
periodicTableWithKey: false,
|
|
627
|
-
tTable: false,
|
|
628
|
-
zTable: false
|
|
629
|
-
},
|
|
630
|
-
hints: [randomHintPhrase, `=${Math.floor(Math.random() * 50) + 1}`],
|
|
631
|
-
itemDataVersion: {
|
|
632
|
-
major: 0,
|
|
633
|
-
minor: 1
|
|
634
|
-
},
|
|
635
|
-
question: {
|
|
636
|
-
content: `${randomPhrase}`,
|
|
637
|
-
images: {},
|
|
638
|
-
widgets: {
|
|
639
|
-
expression1: {
|
|
640
|
-
alignment: "default",
|
|
641
|
-
graded: false,
|
|
642
|
-
options: {
|
|
643
|
-
answerForms: [{
|
|
644
|
-
considered: "wrong",
|
|
645
|
-
form: false,
|
|
646
|
-
key: 0,
|
|
647
|
-
simplify: false,
|
|
648
|
-
value: `${randomString}`
|
|
649
|
-
}],
|
|
650
|
-
ariaLabel: "Answer",
|
|
651
|
-
buttonSets: ["basic"],
|
|
652
|
-
functions: ["f", "g", "h"],
|
|
653
|
-
static: true,
|
|
654
|
-
times: false,
|
|
655
|
-
visibleLabel: "Answer"
|
|
656
|
-
},
|
|
657
|
-
static: true,
|
|
658
|
-
type: "expression",
|
|
659
|
-
version: {
|
|
660
|
-
major: 1,
|
|
661
|
-
minor: 0
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
});
|
|
667
|
-
const testJSON = buildTestData(testingObject.replace(/"/g, '\\"'));
|
|
668
|
-
const parsedTestJSON = jsonParse(testJSON);
|
|
669
|
-
const parsedTestItemData = parsedTestJSON.data.assessmentItem.item.itemData;
|
|
670
|
-
return approximateDeepEqual(parsedTestItemData, testingObject);
|
|
671
|
-
}
|
|
672
|
-
function buildRandomString() {
|
|
673
|
-
let capitalize = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
|
674
|
-
let randomString = "";
|
|
675
|
-
const randomLength = Math.floor(Math.random() * 8) + 3;
|
|
676
|
-
for (let i = 0; i < randomLength; i++) {
|
|
677
|
-
const randomLetter = String.fromCharCode(97 + Math.floor(Math.random() * 26));
|
|
678
|
-
randomString += capitalize && i === 0 ? randomLetter.toUpperCase() : randomLetter;
|
|
679
|
-
}
|
|
680
|
-
return randomString;
|
|
681
|
-
}
|
|
682
|
-
function buildRandomPhrase() {
|
|
683
|
-
const phrases = [];
|
|
684
|
-
const randomLength = Math.floor(Math.random() * 10) + 5;
|
|
685
|
-
for (let i = 0; i < randomLength; i++) {
|
|
686
|
-
phrases.push(buildRandomString(i === 0));
|
|
687
|
-
}
|
|
688
|
-
const modifierStart = ["**", "$"];
|
|
689
|
-
const modifierEnd = ["**", "$"];
|
|
690
|
-
const modifierIndex = Math.floor(Math.random() * modifierStart.length);
|
|
691
|
-
return `${modifierStart[modifierIndex]}${phrases.join(" ")}${modifierEnd[modifierIndex]}`;
|
|
692
|
-
}
|
|
693
|
-
function buildTestData(testObject) {
|
|
694
|
-
return `{"data":{"assessmentItem":{"__typename":"AssessmentItemOrError","error":null,"item":{"__typename":"AssessmentItem","id":"x890b3c70f3e8f4a6","itemData":"${testObject}","problemType":"Type 1","sha":"c7284a3ad65214b4e62bccce236d92f7f5d35941"}}}}`;
|
|
695
|
-
}
|
|
51
|
+
function isRealJSONParse(jsonParse){const randomPhrase=buildRandomPhrase();const randomHintPhrase=buildRandomPhrase();const randomString=buildRandomString();const testingObject=JSON.stringify({answerArea:{calculator:false,chi2Table:false,financialCalculatorMonthlyPayment:false,financialCalculatorTimeToPayOff:false,financialCalculatorTotalAmount:false,periodicTable:false,periodicTableWithKey:false,tTable:false,zTable:false},hints:[randomHintPhrase,`=${Math.floor(Math.random()*50)+1}`],itemDataVersion:{major:0,minor:1},question:{content:`${randomPhrase}`,images:{},widgets:{expression1:{alignment:"default",graded:false,options:{answerForms:[{considered:"wrong",form:false,key:0,simplify:false,value:`${randomString}`}],ariaLabel:"Answer",buttonSets:["basic"],functions:["f","g","h"],static:true,times:false,visibleLabel:"Answer"},static:true,type:"expression",version:{major:1,minor:0}}}}});const testJSON=buildTestData(testingObject.replace(/"/g,'\\"'));const parsedTestJSON=jsonParse(testJSON);const parsedTestItemData=parsedTestJSON.data.assessmentItem.item.itemData;return approximateDeepEqual(parsedTestItemData,testingObject)}function buildRandomString(capitalize=false){let randomString="";const randomLength=Math.floor(Math.random()*8)+3;for(let i=0;i<randomLength;i++){const randomLetter=String.fromCharCode(97+Math.floor(Math.random()*26));randomString+=capitalize&&i===0?randomLetter.toUpperCase():randomLetter;}return randomString}function buildRandomPhrase(){const phrases=[];const randomLength=Math.floor(Math.random()*10)+5;for(let i=0;i<randomLength;i++){phrases.push(buildRandomString(i===0));}const modifierStart=["**","$"];const modifierEnd=["**","$"];const modifierIndex=Math.floor(Math.random()*modifierStart.length);return `${modifierStart[modifierIndex]}${phrases.join(" ")}${modifierEnd[modifierIndex]}`}function buildTestData(testObject){return `{"data":{"assessmentItem":{"__typename":"AssessmentItemOrError","error":null,"item":{"__typename":"AssessmentItem","id":"x890b3c70f3e8f4a6","itemData":"${testObject}","problemType":"Type 1","sha":"c7284a3ad65214b4e62bccce236d92f7f5d35941"}}}}`}
|
|
696
52
|
|
|
697
|
-
function success(value) {
|
|
698
|
-
return {
|
|
699
|
-
type: "success",
|
|
700
|
-
value
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
function failure(detail) {
|
|
704
|
-
return {
|
|
705
|
-
type: "failure",
|
|
706
|
-
detail
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
function isFailure(result) {
|
|
710
|
-
return result.type === "failure";
|
|
711
|
-
}
|
|
712
|
-
function isSuccess(result) {
|
|
713
|
-
return result.type === "success";
|
|
714
|
-
}
|
|
53
|
+
function success(value){return {type:"success",value}}function failure(detail){return {type:"failure",detail}}function isFailure(result){return result.type==="failure"}function isSuccess(result){return result.type==="success"}function all(results,combineFailureDetails=a=>a){const values=[];const failureDetails=[];for(const result of results){if(result.type==="success"){values.push(result.value);}else {failureDetails.push(result.detail);}}if(failureDetails.length>0){return failure(failureDetails.reduce(combineFailureDetails))}return success(values)}
|
|
715
54
|
|
|
716
|
-
|
|
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;
|
|
720
|
-
const values = [];
|
|
721
|
-
const failureDetails = [];
|
|
722
|
-
for (const result of results) {
|
|
723
|
-
if (result.type === "success") {
|
|
724
|
-
values.push(result.value);
|
|
725
|
-
} else {
|
|
726
|
-
failureDetails.push(result.detail);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
if (failureDetails.length > 0) {
|
|
730
|
-
return failure(failureDetails.reduce(combineFailureDetails));
|
|
731
|
-
}
|
|
732
|
-
return success(values);
|
|
733
|
-
}
|
|
55
|
+
class ErrorTrackingParseContext{failure(expected,badValue){return failure([{expected:wrapInArray(expected),badValue,path:this.path}])}forSubtree(key){return new ErrorTrackingParseContext([...this.path,key])}success(value){return success(value)}constructor(path){this.path=path;}}function wrapInArray(a){return Array.isArray(a)?a:[a]}
|
|
734
56
|
|
|
735
|
-
|
|
736
|
-
constructor(path) {
|
|
737
|
-
this.path = path;
|
|
738
|
-
}
|
|
739
|
-
failure(expected, badValue) {
|
|
740
|
-
return failure([{
|
|
741
|
-
expected: wrapInArray(expected),
|
|
742
|
-
badValue,
|
|
743
|
-
path: this.path
|
|
744
|
-
}]);
|
|
745
|
-
}
|
|
746
|
-
forSubtree(key) {
|
|
747
|
-
return new ErrorTrackingParseContext([...this.path, key]);
|
|
748
|
-
}
|
|
749
|
-
success(value) {
|
|
750
|
-
return success(value);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
function wrapInArray(a) {
|
|
754
|
-
return Array.isArray(a) ? a : [a];
|
|
755
|
-
}
|
|
57
|
+
function formatPath(path){return "(root)"+path.map(formatPathSegment).join("")}function formatPathSegment(segment){if(typeof segment==="string"){return validIdentifier.test(segment)?"."+segment:`[${JSON.stringify(segment)}]`}return `[${segment.toString()}]`}const validIdentifier=/^[A-Za-z$_][A-Za-z$_0-9]*$/;
|
|
756
58
|
|
|
757
|
-
function formatPath(path) {
|
|
758
|
-
return "(root)" + path.map(formatPathSegment).join("");
|
|
759
|
-
}
|
|
760
|
-
function formatPathSegment(segment) {
|
|
761
|
-
if (typeof segment === "string") {
|
|
762
|
-
return validIdentifier.test(segment) ? "." + segment : `[${JSON.stringify(segment)}]`;
|
|
763
|
-
}
|
|
764
|
-
return `[${segment.toString()}]`;
|
|
765
|
-
}
|
|
766
|
-
const validIdentifier = /^[A-Za-z$_][A-Za-z$_0-9]*$/;
|
|
59
|
+
function message(failure){const expected=conjoin(failure.expected);const path=formatPath(failure.path);const badValue=JSON.stringify(failure.badValue);return `At ${path} -- expected ${expected}, but got ${badValue}`}function conjoin(items){switch(items.length){case 1:return items[0];case 2:return items.join(" or ");default:{const allButLast=items.slice(0,items.length-1);const last=items[items.length-1];return allButLast.join(", ")+", or "+last}}}
|
|
767
60
|
|
|
768
|
-
function
|
|
769
|
-
const expected = conjoin(failure.expected);
|
|
770
|
-
const path = formatPath(failure.path);
|
|
771
|
-
const badValue = JSON.stringify(failure.badValue);
|
|
772
|
-
return `At ${path} -- expected ${expected}, but got ${badValue}`;
|
|
773
|
-
}
|
|
774
|
-
function conjoin(items) {
|
|
775
|
-
switch (items.length) {
|
|
776
|
-
// TODO(benchristel): handle 0 if this is reused elsewhere.
|
|
777
|
-
case 1:
|
|
778
|
-
return items[0];
|
|
779
|
-
case 2:
|
|
780
|
-
return items.join(" or ");
|
|
781
|
-
default:
|
|
782
|
-
{
|
|
783
|
-
const allButLast = items.slice(0, items.length - 1);
|
|
784
|
-
const last = items[items.length - 1];
|
|
785
|
-
return allButLast.join(", ") + ", or " + last;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
61
|
+
function parse(value,parser){const result=parser(value,new ErrorTrackingParseContext([]));if(isFailure(result)){return failure(result.detail.map(message).join("; "))}return result}
|
|
789
62
|
|
|
790
|
-
|
|
791
|
-
const result = parser(value, new ErrorTrackingParseContext([]));
|
|
792
|
-
if (isFailure(result)) {
|
|
793
|
-
return failure(result.detail.map(message).join("; "));
|
|
794
|
-
}
|
|
795
|
-
return result;
|
|
796
|
-
}
|
|
63
|
+
const any=(rawValue,ctx)=>ctx.success(rawValue);
|
|
797
64
|
|
|
798
|
-
|
|
65
|
+
function array(elementParser){return (rawValue,ctx)=>{if(!Array.isArray(rawValue)){return ctx.failure("array",rawValue)}const elementResults=rawValue.map((elem,i)=>elementParser(elem,ctx.forSubtree(i)));return all(elementResults,concat)}}function concat(a,b){return [...a,...b]}
|
|
799
66
|
|
|
800
|
-
function
|
|
801
|
-
return (rawValue, ctx) => {
|
|
802
|
-
if (!Array.isArray(rawValue)) {
|
|
803
|
-
return ctx.failure("array", rawValue);
|
|
804
|
-
}
|
|
805
|
-
const elementResults = rawValue.map((elem, i) => elementParser(elem, ctx.forSubtree(i)));
|
|
806
|
-
return all(elementResults, concat);
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
function concat(a, b) {
|
|
810
|
-
return [...a, ...b];
|
|
811
|
-
}
|
|
67
|
+
function boolean(rawValue,ctx){if(typeof rawValue==="boolean"){return ctx.success(rawValue)}return ctx.failure("boolean",rawValue)}
|
|
812
68
|
|
|
813
|
-
function
|
|
814
|
-
if (typeof rawValue === "boolean") {
|
|
815
|
-
return ctx.success(rawValue);
|
|
816
|
-
}
|
|
817
|
-
return ctx.failure("boolean", rawValue);
|
|
818
|
-
}
|
|
69
|
+
function constant(acceptedValue){return (rawValue,ctx)=>{if(rawValue!==acceptedValue){return ctx.failure(String(JSON.stringify(acceptedValue)),rawValue)}return ctx.success(acceptedValue)}}
|
|
819
70
|
|
|
820
|
-
function
|
|
821
|
-
return (rawValue, ctx) => {
|
|
822
|
-
if (rawValue !== acceptedValue) {
|
|
823
|
-
return ctx.failure(String(JSON.stringify(acceptedValue)), rawValue);
|
|
824
|
-
}
|
|
825
|
-
return ctx.success(acceptedValue);
|
|
826
|
-
};
|
|
827
|
-
}
|
|
71
|
+
function enumeration(...acceptedValues){return (rawValue,ctx)=>{if(typeof rawValue==="string"){const index=acceptedValues.indexOf(rawValue);if(index>-1){return ctx.success(acceptedValues[index])}}const expected=acceptedValues.map(v=>JSON.stringify(v));return ctx.failure(expected,rawValue)}}
|
|
828
72
|
|
|
829
|
-
function
|
|
830
|
-
for (var _len = arguments.length, acceptedValues = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
831
|
-
acceptedValues[_key] = arguments[_key];
|
|
832
|
-
}
|
|
833
|
-
return (rawValue, ctx) => {
|
|
834
|
-
if (typeof rawValue === "string") {
|
|
835
|
-
const index = acceptedValues.indexOf(rawValue);
|
|
836
|
-
if (index > -1) {
|
|
837
|
-
return ctx.success(acceptedValues[index]);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
const expected = acceptedValues.map(v => JSON.stringify(v));
|
|
841
|
-
return ctx.failure(expected, rawValue);
|
|
842
|
-
};
|
|
843
|
-
}
|
|
73
|
+
function isObject(x){return x!=null&&Object.getPrototypeOf(x)===Object.prototype}
|
|
844
74
|
|
|
845
|
-
function
|
|
846
|
-
return x != null && Object.getPrototypeOf(x) === Object.prototype;
|
|
847
|
-
}
|
|
75
|
+
function nullable(parseValue){return (rawValue,ctx)=>{if(rawValue===null){return ctx.success(rawValue)}return parseValue(rawValue,ctx)}}
|
|
848
76
|
|
|
849
|
-
|
|
850
|
-
return (rawValue, ctx) => {
|
|
851
|
-
if (rawValue === null) {
|
|
852
|
-
return ctx.success(rawValue);
|
|
853
|
-
}
|
|
854
|
-
return parseValue(rawValue, ctx);
|
|
855
|
-
};
|
|
856
|
-
}
|
|
77
|
+
const number=(rawValue,ctx)=>{if(typeof rawValue==="number"){return ctx.success(rawValue)}return ctx.failure("number",rawValue)};
|
|
857
78
|
|
|
858
|
-
const
|
|
859
|
-
if (typeof rawValue === "number") {
|
|
860
|
-
return ctx.success(rawValue);
|
|
861
|
-
}
|
|
862
|
-
return ctx.failure("number", rawValue);
|
|
863
|
-
};
|
|
864
|
-
|
|
865
|
-
function object(schema) {
|
|
866
|
-
return (rawValue, ctx) => {
|
|
867
|
-
if (!isObject(rawValue)) {
|
|
868
|
-
return ctx.failure("object", rawValue);
|
|
869
|
-
}
|
|
870
|
-
const ret = {
|
|
871
|
-
...rawValue
|
|
872
|
-
};
|
|
873
|
-
const mismatches = [];
|
|
874
|
-
for (const [prop, propParser] of Object.entries(schema)) {
|
|
875
|
-
const result = propParser(rawValue[prop], ctx.forSubtree(prop));
|
|
876
|
-
if (isSuccess(result)) {
|
|
877
|
-
if (result.value !== undefined || prop in rawValue) {
|
|
878
|
-
ret[prop] = result.value;
|
|
879
|
-
}
|
|
880
|
-
} else {
|
|
881
|
-
mismatches.push(...result.detail);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
if (mismatches.length > 0) {
|
|
885
|
-
return failure(mismatches);
|
|
886
|
-
}
|
|
887
|
-
return ctx.success(ret);
|
|
888
|
-
};
|
|
889
|
-
}
|
|
79
|
+
function object(schema){return (rawValue,ctx)=>{if(!isObject(rawValue)){return ctx.failure("object",rawValue)}const ret={...rawValue};const mismatches=[];for(const[prop,propParser]of Object.entries(schema)){const result=propParser(rawValue[prop],ctx.forSubtree(prop));if(isSuccess(result)){if(result.value!==undefined||prop in rawValue){ret[prop]=result.value;}}else {mismatches.push(...result.detail);}}if(mismatches.length>0){return failure(mismatches)}return ctx.success(ret)}}
|
|
890
80
|
|
|
891
|
-
function optional(parseValue) {
|
|
892
|
-
return (rawValue, ctx) => {
|
|
893
|
-
if (rawValue === undefined) {
|
|
894
|
-
return ctx.success(rawValue);
|
|
895
|
-
}
|
|
896
|
-
return parseValue(rawValue, ctx);
|
|
897
|
-
};
|
|
898
|
-
}
|
|
81
|
+
function optional(parseValue){return (rawValue,ctx)=>{if(rawValue===undefined){return ctx.success(rawValue)}return parseValue(rawValue,ctx)}}
|
|
899
82
|
|
|
900
|
-
function pair(parseA, parseB)
|
|
901
|
-
return (rawValue, ctx) => {
|
|
902
|
-
if (!Array.isArray(rawValue)) {
|
|
903
|
-
return ctx.failure("array", rawValue);
|
|
904
|
-
}
|
|
905
|
-
if (rawValue.length !== 2) {
|
|
906
|
-
return ctx.failure("array of length 2", rawValue);
|
|
907
|
-
}
|
|
908
|
-
const [rawA, rawB] = rawValue;
|
|
909
|
-
const resultA = parseA(rawA, ctx.forSubtree(0));
|
|
910
|
-
if (isFailure(resultA)) {
|
|
911
|
-
return resultA;
|
|
912
|
-
}
|
|
913
|
-
const resultB = parseB(rawB, ctx.forSubtree(1));
|
|
914
|
-
if (isFailure(resultB)) {
|
|
915
|
-
return resultB;
|
|
916
|
-
}
|
|
917
|
-
return ctx.success([resultA.value, resultB.value]);
|
|
918
|
-
};
|
|
919
|
-
}
|
|
83
|
+
function pair(parseA,parseB){return (rawValue,ctx)=>{if(!Array.isArray(rawValue)){return ctx.failure("array",rawValue)}if(rawValue.length!==2){return ctx.failure("array of length 2",rawValue)}const[rawA,rawB]=rawValue;const resultA=parseA(rawA,ctx.forSubtree(0));if(isFailure(resultA)){return resultA}const resultB=parseB(rawB,ctx.forSubtree(1));if(isFailure(resultB)){return resultB}return ctx.success([resultA.value,resultB.value])}}
|
|
920
84
|
|
|
921
|
-
function pipeParsers(p) {
|
|
922
|
-
return new ParserPipeline(p);
|
|
923
|
-
}
|
|
924
|
-
class ParserPipeline {
|
|
925
|
-
constructor(parser) {
|
|
926
|
-
this.parser = parser;
|
|
927
|
-
}
|
|
928
|
-
then(nextParser) {
|
|
929
|
-
return new ParserPipeline(composeParsers(this.parser, nextParser));
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
function composeParsers(parserA, parserB) {
|
|
933
|
-
return (rawValue, ctx) => {
|
|
934
|
-
const partialResult = parserA(rawValue, ctx);
|
|
935
|
-
if (isFailure(partialResult)) {
|
|
936
|
-
return partialResult;
|
|
937
|
-
}
|
|
938
|
-
return parserB(partialResult.value, ctx);
|
|
939
|
-
};
|
|
940
|
-
}
|
|
85
|
+
function pipeParsers(p){return new ParserPipeline(p)}class ParserPipeline{then(nextParser){return new ParserPipeline(composeParsers(this.parser,nextParser))}constructor(parser){this.parser=parser;}}function composeParsers(parserA,parserB){return (rawValue,ctx)=>{const partialResult=parserA(rawValue,ctx);if(isFailure(partialResult)){return partialResult}return parserB(partialResult.value,ctx)}}
|
|
941
86
|
|
|
942
|
-
function record(parseKey, parseValue)
|
|
943
|
-
return (rawValue, ctx) => {
|
|
944
|
-
if (!isObject(rawValue)) {
|
|
945
|
-
return ctx.failure("object", rawValue);
|
|
946
|
-
}
|
|
947
|
-
const result = {};
|
|
948
|
-
const mismatches = [];
|
|
949
|
-
for (const [key, value] of Object.entries(rawValue)) {
|
|
950
|
-
const entryCtx = ctx.forSubtree(key);
|
|
951
|
-
const keyResult = parseKey(key, entryCtx);
|
|
952
|
-
if (isFailure(keyResult)) {
|
|
953
|
-
mismatches.push(...keyResult.detail);
|
|
954
|
-
}
|
|
955
|
-
const valueResult = parseValue(value, entryCtx);
|
|
956
|
-
if (isFailure(valueResult)) {
|
|
957
|
-
mismatches.push(...valueResult.detail);
|
|
958
|
-
}
|
|
959
|
-
if (isSuccess(keyResult) && isSuccess(valueResult)) {
|
|
960
|
-
result[keyResult.value] = valueResult.value;
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
if (mismatches.length > 0) {
|
|
964
|
-
return failure(mismatches);
|
|
965
|
-
}
|
|
966
|
-
return ctx.success(result);
|
|
967
|
-
};
|
|
968
|
-
}
|
|
87
|
+
function record(parseKey,parseValue){return (rawValue,ctx)=>{if(!isObject(rawValue)){return ctx.failure("object",rawValue)}const result={};const mismatches=[];for(const[key,value]of Object.entries(rawValue)){const entryCtx=ctx.forSubtree(key);const keyResult=parseKey(key,entryCtx);if(isFailure(keyResult)){mismatches.push(...keyResult.detail);}const valueResult=parseValue(value,entryCtx);if(isFailure(valueResult)){mismatches.push(...valueResult.detail);}if(isSuccess(keyResult)&&isSuccess(valueResult)){result[keyResult.value]=valueResult.value;}}if(mismatches.length>0){return failure(mismatches)}return ctx.success(result)}}
|
|
969
88
|
|
|
970
|
-
const string
|
|
971
|
-
if (typeof rawValue === "string") {
|
|
972
|
-
return ctx.success(rawValue);
|
|
973
|
-
}
|
|
974
|
-
return ctx.failure("string", rawValue);
|
|
975
|
-
};
|
|
976
|
-
|
|
977
|
-
function trio(parseA, parseB, parseC) {
|
|
978
|
-
return (rawValue, ctx) => {
|
|
979
|
-
if (!Array.isArray(rawValue)) {
|
|
980
|
-
return ctx.failure("array", rawValue);
|
|
981
|
-
}
|
|
982
|
-
if (rawValue.length !== 3) {
|
|
983
|
-
return ctx.failure("array of length 3", rawValue);
|
|
984
|
-
}
|
|
985
|
-
const resultA = parseA(rawValue[0], ctx.forSubtree(0));
|
|
986
|
-
if (isFailure(resultA)) {
|
|
987
|
-
return resultA;
|
|
988
|
-
}
|
|
989
|
-
const resultB = parseB(rawValue[1], ctx.forSubtree(1));
|
|
990
|
-
if (isFailure(resultB)) {
|
|
991
|
-
return resultB;
|
|
992
|
-
}
|
|
993
|
-
const resultC = parseC(rawValue[2], ctx.forSubtree(2));
|
|
994
|
-
if (isFailure(resultC)) {
|
|
995
|
-
return resultC;
|
|
996
|
-
}
|
|
997
|
-
return ctx.success([resultA.value, resultB.value, resultC.value]);
|
|
998
|
-
};
|
|
999
|
-
}
|
|
89
|
+
const string=(rawValue,ctx)=>{if(typeof rawValue==="string"){return ctx.success(rawValue)}return ctx.failure("string",rawValue)};
|
|
1000
90
|
|
|
1001
|
-
function
|
|
1002
|
-
return new UnionBuilder(parseBranch);
|
|
1003
|
-
}
|
|
1004
|
-
class UnionBuilder {
|
|
1005
|
-
constructor(parser) {
|
|
1006
|
-
this.parser = parser;
|
|
1007
|
-
}
|
|
1008
|
-
or(newBranch) {
|
|
1009
|
-
return new UnionBuilder(either(this.parser, newBranch));
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
function either(parseA, parseB) {
|
|
1013
|
-
return (rawValue, ctx) => {
|
|
1014
|
-
const resultA = parseA(rawValue, ctx);
|
|
1015
|
-
if (isSuccess(resultA)) {
|
|
1016
|
-
return resultA;
|
|
1017
|
-
}
|
|
1018
|
-
return parseB(rawValue, ctx);
|
|
1019
|
-
};
|
|
1020
|
-
}
|
|
91
|
+
function trio(parseA,parseB,parseC){return (rawValue,ctx)=>{if(!Array.isArray(rawValue)){return ctx.failure("array",rawValue)}if(rawValue.length!==3){return ctx.failure("array of length 3",rawValue)}const resultA=parseA(rawValue[0],ctx.forSubtree(0));if(isFailure(resultA)){return resultA}const resultB=parseB(rawValue[1],ctx.forSubtree(1));if(isFailure(resultB)){return resultB}const resultC=parseC(rawValue[2],ctx.forSubtree(2));if(isFailure(resultC)){return resultC}return ctx.success([resultA.value,resultB.value,resultC.value])}}
|
|
1021
92
|
|
|
1022
|
-
function
|
|
1023
|
-
return (rawValue, ctx) => {
|
|
1024
|
-
if (rawValue == null) {
|
|
1025
|
-
return success(fallback(rawValue));
|
|
1026
|
-
}
|
|
1027
|
-
return parser(rawValue, ctx);
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
93
|
+
function union(parseBranch){return new UnionBuilder(parseBranch)}class UnionBuilder{or(newBranch){return new UnionBuilder(either(this.parser,newBranch))}constructor(parser){this.parser=parser;}}function either(parseA,parseB){return (rawValue,ctx)=>{const resultA=parseA(rawValue,ctx);if(isSuccess(resultA)){return resultA}return parseB(rawValue,ctx)}}
|
|
1030
94
|
|
|
1031
|
-
|
|
1032
|
-
width: number,
|
|
1033
|
-
height: number
|
|
1034
|
-
})), () => ({}));
|
|
1035
|
-
|
|
1036
|
-
function parseWidget(parseType, parseOptions) {
|
|
1037
|
-
return object({
|
|
1038
|
-
type: parseType,
|
|
1039
|
-
static: optional(boolean),
|
|
1040
|
-
graded: optional(boolean),
|
|
1041
|
-
alignment: optional(string),
|
|
1042
|
-
options: parseOptions,
|
|
1043
|
-
key: optional(nullable(number)),
|
|
1044
|
-
version: optional(object({
|
|
1045
|
-
major: number,
|
|
1046
|
-
minor: number
|
|
1047
|
-
}))
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
function parseWidgetWithVersion(parseVersion, parseType, parseOptions) {
|
|
1051
|
-
return object({
|
|
1052
|
-
type: parseType,
|
|
1053
|
-
static: optional(boolean),
|
|
1054
|
-
graded: optional(boolean),
|
|
1055
|
-
alignment: optional(string),
|
|
1056
|
-
options: parseOptions,
|
|
1057
|
-
key: optional(nullable(number)),
|
|
1058
|
-
version: parseVersion
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
95
|
+
function defaulted(parser,fallback){return (rawValue,ctx)=>{if(rawValue==null){return success(fallback(rawValue))}return parser(rawValue,ctx)}}
|
|
1061
96
|
|
|
1062
|
-
const
|
|
1063
|
-
items: array(string),
|
|
1064
|
-
categories: array(string),
|
|
1065
|
-
randomizeItems: defaulted(boolean, () => false),
|
|
1066
|
-
static: defaulted(boolean, () => false),
|
|
1067
|
-
values: array(defaulted(number, () => 0)),
|
|
1068
|
-
highlightLint: optional(boolean),
|
|
1069
|
-
linterContext: optional(object({
|
|
1070
|
-
contentType: string,
|
|
1071
|
-
paths: array(string),
|
|
1072
|
-
stack: array(string)
|
|
1073
|
-
}))
|
|
1074
|
-
}));
|
|
1075
|
-
|
|
1076
|
-
const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
|
|
1077
|
-
programID: string,
|
|
1078
|
-
programType: any,
|
|
1079
|
-
settings: array(object({
|
|
1080
|
-
name: string,
|
|
1081
|
-
value: string
|
|
1082
|
-
})),
|
|
1083
|
-
showEditor: boolean,
|
|
1084
|
-
showButtons: boolean,
|
|
1085
|
-
height: number,
|
|
1086
|
-
static: defaulted(boolean, () => false)
|
|
1087
|
-
}));
|
|
1088
|
-
|
|
1089
|
-
const parseDefinitionWidget = parseWidget(constant("definition"), object({
|
|
1090
|
-
togglePrompt: string,
|
|
1091
|
-
definition: string,
|
|
1092
|
-
static: defaulted(boolean, () => false)
|
|
1093
|
-
}));
|
|
1094
|
-
|
|
1095
|
-
const parseDropdownWidget = parseWidget(constant("dropdown"), object({
|
|
1096
|
-
placeholder: defaulted(string, () => ""),
|
|
1097
|
-
ariaLabel: optional(string),
|
|
1098
|
-
visibleLabel: optional(string),
|
|
1099
|
-
static: defaulted(boolean, () => false),
|
|
1100
|
-
choices: array(object({
|
|
1101
|
-
content: string,
|
|
1102
|
-
correct: boolean
|
|
1103
|
-
}))
|
|
1104
|
-
}));
|
|
1105
|
-
|
|
1106
|
-
const parseExplanationWidget = parseWidget(constant("explanation"), object({
|
|
1107
|
-
showPrompt: string,
|
|
1108
|
-
hidePrompt: string,
|
|
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.
|
|
1113
|
-
widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
|
|
1114
|
-
static: defaulted(boolean, () => false)
|
|
1115
|
-
}));
|
|
1116
|
-
|
|
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
|
-
*/
|
|
1135
|
-
function deriveExtraKeys(widgetOptions) {
|
|
1136
|
-
if (widgetOptions.extraKeys) {
|
|
1137
|
-
return widgetOptions.extraKeys;
|
|
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.
|
|
1142
|
-
const defaultKeys = ["PI"];
|
|
1143
|
-
if (widgetOptions.answerForms == null) {
|
|
1144
|
-
return defaultKeys;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
// Extract any and all variables and constants from the answer forms.
|
|
1148
|
-
const uniqueExtraVariables = {};
|
|
1149
|
-
const uniqueExtraConstants = {};
|
|
1150
|
-
for (const answerForm of widgetOptions.answerForms) {
|
|
1151
|
-
const maybeExpr = KAS__namespace.parse(answerForm.value, widgetOptions);
|
|
1152
|
-
if (maybeExpr.parsed) {
|
|
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.
|
|
1158
|
-
const isGreek = symbol => symbol === "pi" || symbol === "theta";
|
|
1159
|
-
const toKey = symbol => isGreek(symbol) ? symbol.toUpperCase() : symbol;
|
|
1160
|
-
const isKey = key => KeypadKeys.includes(key);
|
|
1161
|
-
for (const variable of expr.getVars()) {
|
|
1162
|
-
const maybeKey = toKey(variable);
|
|
1163
|
-
if (isKey(maybeKey)) {
|
|
1164
|
-
uniqueExtraVariables[maybeKey] = true;
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
for (const constant of expr.getConsts()) {
|
|
1168
|
-
const maybeKey = toKey(constant);
|
|
1169
|
-
if (isKey(maybeKey)) {
|
|
1170
|
-
uniqueExtraConstants[maybeKey] = true;
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// TODO(charlie): Alert the keypad as to which of these symbols should be
|
|
1177
|
-
// treated as functions.
|
|
1178
|
-
const extraVariables = Object.keys(uniqueExtraVariables).sort();
|
|
1179
|
-
const extraConstants = Object.keys(uniqueExtraConstants).sort();
|
|
1180
|
-
const extraKeys = [...extraVariables, ...extraConstants];
|
|
1181
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
1182
|
-
if (!extraKeys.length) {
|
|
1183
|
-
return defaultKeys;
|
|
1184
|
-
}
|
|
1185
|
-
return extraKeys;
|
|
1186
|
-
}
|
|
97
|
+
const parseImages=defaulted(record(string,object({width:number,height:number})),()=>({}));
|
|
1187
98
|
|
|
1188
|
-
|
|
1189
|
-
// using that function. The returned parser never fails.
|
|
1190
|
-
function convert(f) {
|
|
1191
|
-
return (rawValue, ctx) => ctx.success(f(rawValue));
|
|
1192
|
-
}
|
|
99
|
+
function parseWidget(parseType,parseOptions){return object({type:parseType,static:optional(boolean),graded:optional(boolean),alignment:optional(string),options:parseOptions,key:optional(nullable(number)),version:optional(object({major:number,minor:number}))})}function parseWidgetWithVersion(parseVersion,parseType,parseOptions){return object({type:parseType,static:optional(boolean),graded:optional(boolean),alignment:optional(string),options:parseOptions,key:optional(nullable(number)),version:parseVersion})}
|
|
1193
100
|
|
|
1194
|
-
const
|
|
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
|
-
*/
|
|
1220
|
-
function versionedWidgetOptions(latestMajorVersion, parseLatest) {
|
|
1221
|
-
return new VersionedWidgetOptionsParserBuilder(latestMajorVersion, parseLatest, latest => latest, (raw, ctx) => ctx.failure("widget options with a known version number", raw));
|
|
1222
|
-
}
|
|
1223
|
-
class VersionedWidgetOptionsParserBuilder {
|
|
1224
|
-
parser;
|
|
1225
|
-
constructor(majorVersion, parseThisVersion, migrateToLatest, parseOtherVersions) {
|
|
1226
|
-
this.migrateToLatest = migrateToLatest;
|
|
1227
|
-
this.parseOtherVersions = parseOtherVersions;
|
|
1228
|
-
const parseThisVersionAndMigrateToLatest = pipeParsers(parseThisVersion).then(convert(this.migrateToLatest)).parser;
|
|
1229
|
-
this.parser = (raw, ctx) => {
|
|
1230
|
-
if (!isObject(raw)) {
|
|
1231
|
-
return ctx.failure("object", raw);
|
|
1232
|
-
}
|
|
1233
|
-
const versionParseResult = parseVersionedObject(raw, ctx);
|
|
1234
|
-
if (isFailure(versionParseResult)) {
|
|
1235
|
-
return versionParseResult;
|
|
1236
|
-
}
|
|
1237
|
-
if (versionParseResult.value.version.major !== majorVersion) {
|
|
1238
|
-
return this.parseOtherVersions(raw, ctx);
|
|
1239
|
-
}
|
|
1240
|
-
return parseThisVersionAndMigrateToLatest(raw, ctx);
|
|
1241
|
-
};
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
/**
|
|
1245
|
-
* Add a migration from an old version of the widget options.
|
|
1246
|
-
*/
|
|
1247
|
-
withMigrationFrom(majorVersion, parseOldVersion, migrateToNextVersion) {
|
|
1248
|
-
const parseOtherVersions = this.parser;
|
|
1249
|
-
const migrateToLatest = old => this.migrateToLatest(migrateToNextVersion(old));
|
|
1250
|
-
return new VersionedWidgetOptionsParserBuilder(majorVersion, parseOldVersion, migrateToLatest, parseOtherVersions);
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
const parseVersionedObject = object({
|
|
1254
|
-
version: defaulted(object({
|
|
1255
|
-
major: number,
|
|
1256
|
-
minor: number
|
|
1257
|
-
}), () => ({
|
|
1258
|
-
major: 0,
|
|
1259
|
-
minor: 0
|
|
1260
|
-
}))
|
|
1261
|
-
});
|
|
101
|
+
const parseCategorizerWidget=parseWidget(constant("categorizer"),object({items:array(string),categories:array(string),randomizeItems:defaulted(boolean,()=>false),static:defaulted(boolean,()=>false),values:array(defaulted(number,()=>0)),highlightLint:optional(boolean),linterContext:optional(object({contentType:string,paths:array(string),stack:array(string)}))}));
|
|
1262
102
|
|
|
1263
|
-
const
|
|
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.
|
|
1268
|
-
value: optional(string),
|
|
1269
|
-
form: defaulted(boolean, () => false),
|
|
1270
|
-
simplify: defaulted(boolean, () => false),
|
|
1271
|
-
considered: enumeration("correct", "wrong", "ungraded"),
|
|
1272
|
-
key: pipeParsers(stringOrNumberOrNullOrUndefined).then(convert(String)).parser
|
|
1273
|
-
});
|
|
1274
|
-
function removeInvalidAnswerForms(possiblyInvalid) {
|
|
1275
|
-
const valid = [];
|
|
1276
|
-
for (const answerForm of possiblyInvalid) {
|
|
1277
|
-
const {
|
|
1278
|
-
value
|
|
1279
|
-
} = answerForm;
|
|
1280
|
-
if (value != null) {
|
|
1281
|
-
// Copying the object seems to be needed to make TypeScript happy
|
|
1282
|
-
valid.push({
|
|
1283
|
-
...answerForm,
|
|
1284
|
-
value
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
return valid;
|
|
1289
|
-
}
|
|
1290
|
-
const version2$1 = object({
|
|
1291
|
-
major: constant(2),
|
|
1292
|
-
minor: number
|
|
1293
|
-
});
|
|
1294
|
-
const parseExpressionWidgetV2 = parseWidgetWithVersion(version2$1, constant("expression"), object({
|
|
1295
|
-
answerForms: pipeParsers(array(parsePossiblyInvalidAnswerForm)).then(convert(removeInvalidAnswerForms)).parser,
|
|
1296
|
-
functions: array(string),
|
|
1297
|
-
times: boolean,
|
|
1298
|
-
visibleLabel: optional(string),
|
|
1299
|
-
ariaLabel: optional(string),
|
|
1300
|
-
buttonSets: parseLegacyButtonSets,
|
|
1301
|
-
buttonsVisible: optional(enumeration("always", "never", "focused")),
|
|
1302
|
-
extraKeys: array(enumeration(...KeypadKeys))
|
|
1303
|
-
}));
|
|
1304
|
-
const version1$1 = object({
|
|
1305
|
-
major: constant(1),
|
|
1306
|
-
minor: number
|
|
1307
|
-
});
|
|
1308
|
-
const parseExpressionWidgetV1 = parseWidgetWithVersion(version1$1, constant("expression"), object({
|
|
1309
|
-
answerForms: pipeParsers(array(parsePossiblyInvalidAnswerForm)).then(convert(removeInvalidAnswerForms)).parser,
|
|
1310
|
-
functions: array(string),
|
|
1311
|
-
times: boolean,
|
|
1312
|
-
visibleLabel: optional(string),
|
|
1313
|
-
ariaLabel: optional(string),
|
|
1314
|
-
buttonSets: parseLegacyButtonSets,
|
|
1315
|
-
buttonsVisible: optional(enumeration("always", "never", "focused"))
|
|
1316
|
-
}));
|
|
1317
|
-
function migrateV1ToV2$1(widget) {
|
|
1318
|
-
const {
|
|
1319
|
-
options
|
|
1320
|
-
} = widget;
|
|
1321
|
-
return {
|
|
1322
|
-
...widget,
|
|
1323
|
-
version: {
|
|
1324
|
-
major: 2,
|
|
1325
|
-
minor: 0
|
|
1326
|
-
},
|
|
1327
|
-
options: {
|
|
1328
|
-
times: options.times,
|
|
1329
|
-
buttonSets: options.buttonSets,
|
|
1330
|
-
functions: options.functions,
|
|
1331
|
-
buttonsVisible: options.buttonsVisible,
|
|
1332
|
-
visibleLabel: options.visibleLabel,
|
|
1333
|
-
ariaLabel: options.ariaLabel,
|
|
1334
|
-
answerForms: options.answerForms,
|
|
1335
|
-
extraKeys: deriveExtraKeys(options)
|
|
1336
|
-
}
|
|
1337
|
-
};
|
|
1338
|
-
}
|
|
1339
|
-
const version0$1 = optional(object({
|
|
1340
|
-
major: constant(0),
|
|
1341
|
-
minor: number
|
|
1342
|
-
}));
|
|
1343
|
-
const parseExpressionWidgetV0 = parseWidgetWithVersion(version0$1, constant("expression"), object({
|
|
1344
|
-
functions: array(string),
|
|
1345
|
-
times: boolean,
|
|
1346
|
-
visibleLabel: optional(string),
|
|
1347
|
-
ariaLabel: optional(string),
|
|
1348
|
-
form: boolean,
|
|
1349
|
-
simplify: boolean,
|
|
1350
|
-
value: string,
|
|
1351
|
-
buttonSets: parseLegacyButtonSets,
|
|
1352
|
-
buttonsVisible: optional(enumeration("always", "never", "focused"))
|
|
1353
|
-
}));
|
|
1354
|
-
function migrateV0ToV1$1(widget) {
|
|
1355
|
-
const {
|
|
1356
|
-
options
|
|
1357
|
-
} = widget;
|
|
1358
|
-
return {
|
|
1359
|
-
...widget,
|
|
1360
|
-
version: {
|
|
1361
|
-
major: 1,
|
|
1362
|
-
minor: 0
|
|
1363
|
-
},
|
|
1364
|
-
options: {
|
|
1365
|
-
times: options.times,
|
|
1366
|
-
buttonSets: options.buttonSets,
|
|
1367
|
-
functions: options.functions,
|
|
1368
|
-
buttonsVisible: options.buttonsVisible,
|
|
1369
|
-
visibleLabel: options.visibleLabel,
|
|
1370
|
-
ariaLabel: options.ariaLabel,
|
|
1371
|
-
answerForms: [{
|
|
1372
|
-
considered: "correct",
|
|
1373
|
-
form: options.form,
|
|
1374
|
-
simplify: options.simplify,
|
|
1375
|
-
value: options.value
|
|
1376
|
-
}]
|
|
1377
|
-
}
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
const parseExpressionWidget = versionedWidgetOptions(2, parseExpressionWidgetV2).withMigrationFrom(1, parseExpressionWidgetV1, migrateV1ToV2$1).withMigrationFrom(0, parseExpressionWidgetV0, migrateV0ToV1$1).parser;
|
|
1381
|
-
|
|
1382
|
-
const falseToNull = pipeParsers(constant(false)).then(convert(() => null)).parser;
|
|
1383
|
-
const parseGradedGroupWidgetOptions = object({
|
|
1384
|
-
title: defaulted(string, () => ""),
|
|
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.
|
|
1389
|
-
hint: union(falseToNull).or(constant(null)).or(constant(undefined)).or((rawVal, ctx) => parsePerseusRenderer(rawVal, ctx)).parser,
|
|
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.
|
|
1394
|
-
widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
|
|
1395
|
-
widgetEnabled: optional(nullable(boolean)),
|
|
1396
|
-
immutableWidgets: optional(nullable(boolean)),
|
|
1397
|
-
images: record(string, object({
|
|
1398
|
-
width: number,
|
|
1399
|
-
height: number
|
|
1400
|
-
}))
|
|
1401
|
-
});
|
|
1402
|
-
const parseGradedGroupWidget = parseWidget(constant("graded-group"), parseGradedGroupWidgetOptions);
|
|
1403
|
-
|
|
1404
|
-
const parseGradedGroupSetWidget = parseWidget(constant("graded-group-set"), object({
|
|
1405
|
-
gradedGroups: array(parseGradedGroupWidgetOptions)
|
|
1406
|
-
}));
|
|
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
|
-
*/
|
|
1415
|
-
function discriminatedUnionOn(discriminantKey) {
|
|
1416
|
-
const noMoreBranches = (raw, ctx) => {
|
|
1417
|
-
if (!isObject(raw)) {
|
|
1418
|
-
return ctx.failure("object", raw);
|
|
1419
|
-
}
|
|
1420
|
-
return ctx.forSubtree(discriminantKey).failure("a valid value", raw[discriminantKey]);
|
|
1421
|
-
};
|
|
1422
|
-
return new DiscriminatedUnionBuilder(discriminantKey, noMoreBranches);
|
|
1423
|
-
}
|
|
1424
|
-
class DiscriminatedUnionBuilder {
|
|
1425
|
-
constructor(discriminantKey, parser) {
|
|
1426
|
-
this.discriminantKey = discriminantKey;
|
|
1427
|
-
this.parser = parser;
|
|
1428
|
-
}
|
|
1429
|
-
withBranch(discriminantValue, parseNewVariant) {
|
|
1430
|
-
const parseNewBranch = discriminatedUnionBranch(this.discriminantKey, discriminantValue, parseNewVariant, this.parser);
|
|
1431
|
-
return new DiscriminatedUnionBuilder(this.discriminantKey, parseNewBranch);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
function discriminatedUnionBranch(discriminantKey, discriminantValue, parseVariant, parseOtherBranches) {
|
|
1435
|
-
return (raw, ctx) => {
|
|
1436
|
-
if (!isObject(raw)) {
|
|
1437
|
-
return ctx.failure("object", raw);
|
|
1438
|
-
}
|
|
1439
|
-
if (raw[discriminantKey] === discriminantValue) {
|
|
1440
|
-
return parseVariant(raw, ctx);
|
|
1441
|
-
}
|
|
1442
|
-
return parseOtherBranches(raw, ctx);
|
|
1443
|
-
};
|
|
1444
|
-
}
|
|
103
|
+
const parseCSProgramWidget=parseWidget(constant("cs-program"),object({programID:string,programType:any,settings:array(object({name:string,value:string})),showEditor:boolean,showButtons:boolean,height:number,static:defaulted(boolean,()=>false)}));
|
|
1445
104
|
|
|
1446
|
-
const
|
|
1447
|
-
const pairOfPoints = pair(pairOfNumbers$3, pairOfNumbers$3);
|
|
1448
|
-
const parseGrapherWidget = parseWidget(constant("grapher"), object({
|
|
1449
|
-
availableTypes: array(enumeration("absolute_value", "exponential", "linear", "logarithm", "quadratic", "sinusoid", "tangent")),
|
|
1450
|
-
correct: discriminatedUnionOn("type").withBranch("absolute_value", object({
|
|
1451
|
-
type: constant("absolute_value"),
|
|
1452
|
-
coords: nullable(pairOfPoints)
|
|
1453
|
-
})).withBranch("exponential", object({
|
|
1454
|
-
type: constant("exponential"),
|
|
1455
|
-
asymptote: pairOfPoints,
|
|
1456
|
-
coords: nullable(pairOfPoints)
|
|
1457
|
-
})).withBranch("linear", object({
|
|
1458
|
-
type: constant("linear"),
|
|
1459
|
-
coords: nullable(pairOfPoints)
|
|
1460
|
-
})).withBranch("logarithm", object({
|
|
1461
|
-
type: constant("logarithm"),
|
|
1462
|
-
asymptote: pairOfPoints,
|
|
1463
|
-
coords: nullable(pairOfPoints)
|
|
1464
|
-
})).withBranch("quadratic", object({
|
|
1465
|
-
type: constant("quadratic"),
|
|
1466
|
-
coords: nullable(pairOfPoints)
|
|
1467
|
-
})).withBranch("sinusoid", object({
|
|
1468
|
-
type: constant("sinusoid"),
|
|
1469
|
-
coords: nullable(pairOfPoints)
|
|
1470
|
-
})).withBranch("tangent", object({
|
|
1471
|
-
type: constant("tangent"),
|
|
1472
|
-
coords: nullable(pairOfPoints)
|
|
1473
|
-
})).parser,
|
|
1474
|
-
graph: object({
|
|
1475
|
-
backgroundImage: object({
|
|
1476
|
-
bottom: optional(number),
|
|
1477
|
-
height: optional(number),
|
|
1478
|
-
left: optional(number),
|
|
1479
|
-
scale: optional(number),
|
|
1480
|
-
url: optional(nullable(string)),
|
|
1481
|
-
width: optional(number)
|
|
1482
|
-
}),
|
|
1483
|
-
box: optional(pairOfNumbers$3),
|
|
1484
|
-
editableSettings: optional(array(enumeration("graph", "snap", "image", "measure"))),
|
|
1485
|
-
gridStep: optional(pairOfNumbers$3),
|
|
1486
|
-
labels: pair(string, string),
|
|
1487
|
-
markings: enumeration("graph", "none", "grid"),
|
|
1488
|
-
range: pair(pairOfNumbers$3, pairOfNumbers$3),
|
|
1489
|
-
rulerLabel: constant(""),
|
|
1490
|
-
rulerTicks: number,
|
|
1491
|
-
showProtractor: optional(boolean),
|
|
1492
|
-
showRuler: optional(boolean),
|
|
1493
|
-
showTooltips: optional(boolean),
|
|
1494
|
-
snapStep: optional(pairOfNumbers$3),
|
|
1495
|
-
step: pairOfNumbers$3,
|
|
1496
|
-
valid: optional(union(boolean).or(string).parser)
|
|
1497
|
-
})
|
|
1498
|
-
}));
|
|
1499
|
-
|
|
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));
|
|
1505
|
-
|
|
1506
|
-
const parseIframeWidget = parseWidget(constant("iframe"), object({
|
|
1507
|
-
url: string,
|
|
1508
|
-
settings: optional(array(object({
|
|
1509
|
-
name: string,
|
|
1510
|
-
value: string
|
|
1511
|
-
}))),
|
|
1512
|
-
width: union(number).or(string).parser,
|
|
1513
|
-
height: union(number).or(string).parser,
|
|
1514
|
-
allowFullScreen: defaulted(boolean, () => false),
|
|
1515
|
-
allowTopNavigation: optional(boolean),
|
|
1516
|
-
static: defaulted(boolean, () => false)
|
|
1517
|
-
}));
|
|
1518
|
-
|
|
1519
|
-
const stringToNumber = (rawValue, ctx) => {
|
|
1520
|
-
if (typeof rawValue === "number") {
|
|
1521
|
-
return ctx.success(rawValue);
|
|
1522
|
-
}
|
|
1523
|
-
const parsedNumber = +rawValue;
|
|
1524
|
-
if (rawValue === "" || isNaN(parsedNumber)) {
|
|
1525
|
-
return ctx.failure("a number or numeric string", rawValue);
|
|
1526
|
-
}
|
|
1527
|
-
return ctx.success(parsedNumber);
|
|
1528
|
-
};
|
|
1529
|
-
|
|
1530
|
-
function emptyToZero(x) {
|
|
1531
|
-
return x === "" ? 0 : x;
|
|
1532
|
-
}
|
|
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;
|
|
1538
|
-
const dimensionOrUndefined = defaulted(imageDimensionToNumber, () => undefined);
|
|
1539
|
-
const parsePerseusImageBackground = object({
|
|
1540
|
-
url: optional(nullable(string)),
|
|
1541
|
-
width: dimensionOrUndefined,
|
|
1542
|
-
height: dimensionOrUndefined,
|
|
1543
|
-
top: dimensionOrUndefined,
|
|
1544
|
-
left: dimensionOrUndefined,
|
|
1545
|
-
bottom: dimensionOrUndefined,
|
|
1546
|
-
scale: dimensionOrUndefined
|
|
1547
|
-
});
|
|
105
|
+
const parseDefinitionWidget=parseWidget(constant("definition"),object({togglePrompt:string,definition:string,static:defaulted(boolean,()=>false)}));
|
|
1548
106
|
|
|
1549
|
-
const
|
|
1550
|
-
const parseImageWidget = parseWidget(constant("image"), object({
|
|
1551
|
-
title: optional(string),
|
|
1552
|
-
caption: optional(string),
|
|
1553
|
-
alt: optional(string),
|
|
1554
|
-
backgroundImage: parsePerseusImageBackground,
|
|
1555
|
-
static: optional(boolean),
|
|
1556
|
-
labels: optional(array(object({
|
|
1557
|
-
content: string,
|
|
1558
|
-
alignment: string,
|
|
1559
|
-
coordinates: array(number)
|
|
1560
|
-
}))),
|
|
1561
|
-
range: optional(pair(pairOfNumbers$2, pairOfNumbers$2)),
|
|
1562
|
-
box: optional(pairOfNumbers$2)
|
|
1563
|
-
}));
|
|
1564
|
-
|
|
1565
|
-
const booleanToString = (rawValue, ctx) => {
|
|
1566
|
-
if (typeof rawValue === "boolean") {
|
|
1567
|
-
return ctx.success(String(rawValue));
|
|
1568
|
-
}
|
|
1569
|
-
return ctx.failure("boolean", rawValue);
|
|
1570
|
-
};
|
|
1571
|
-
const parseInputNumberWidget = parseWidget(constant("input-number"), object({
|
|
1572
|
-
answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
|
|
1573
|
-
inexact: optional(boolean),
|
|
1574
|
-
maxError: optional(union(number).or(string).parser),
|
|
1575
|
-
rightAlign: optional(boolean),
|
|
1576
|
-
simplify: enumeration("required", "optional", "enforced"),
|
|
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.
|
|
1582
|
-
value: union(number).or(string).or(booleanToString).parser,
|
|
1583
|
-
customKeypad: optional(boolean)
|
|
1584
|
-
}));
|
|
1585
|
-
|
|
1586
|
-
const pairOfNumbers$1 = pair(number, number);
|
|
1587
|
-
const stringOrEmpty = defaulted(string, () => "");
|
|
1588
|
-
const parseKey = pipeParsers(optional(string)).then(convert(String)).parser;
|
|
1589
|
-
const parseFunctionElement = object({
|
|
1590
|
-
type: constant("function"),
|
|
1591
|
-
key: parseKey,
|
|
1592
|
-
options: object({
|
|
1593
|
-
value: string,
|
|
1594
|
-
funcName: string,
|
|
1595
|
-
rangeMin: string,
|
|
1596
|
-
rangeMax: string,
|
|
1597
|
-
color: string,
|
|
1598
|
-
strokeDasharray: string,
|
|
1599
|
-
strokeWidth: number
|
|
1600
|
-
})
|
|
1601
|
-
});
|
|
1602
|
-
const parseLabelElement = object({
|
|
1603
|
-
type: constant("label"),
|
|
1604
|
-
key: parseKey,
|
|
1605
|
-
options: object({
|
|
1606
|
-
label: string,
|
|
1607
|
-
color: string,
|
|
1608
|
-
coordX: string,
|
|
1609
|
-
coordY: string
|
|
1610
|
-
})
|
|
1611
|
-
});
|
|
1612
|
-
const parseLineElement = object({
|
|
1613
|
-
type: constant("line"),
|
|
1614
|
-
key: parseKey,
|
|
1615
|
-
options: object({
|
|
1616
|
-
color: string,
|
|
1617
|
-
startX: string,
|
|
1618
|
-
startY: string,
|
|
1619
|
-
endX: string,
|
|
1620
|
-
endY: string,
|
|
1621
|
-
strokeDasharray: string,
|
|
1622
|
-
strokeWidth: number,
|
|
1623
|
-
arrows: string
|
|
1624
|
-
})
|
|
1625
|
-
});
|
|
1626
|
-
const parseMovableLineElement = object({
|
|
1627
|
-
type: constant("movable-line"),
|
|
1628
|
-
key: parseKey,
|
|
1629
|
-
options: object({
|
|
1630
|
-
startX: string,
|
|
1631
|
-
startY: string,
|
|
1632
|
-
startSubscript: number,
|
|
1633
|
-
endX: string,
|
|
1634
|
-
endY: string,
|
|
1635
|
-
endSubscript: number,
|
|
1636
|
-
constraint: string,
|
|
1637
|
-
snap: number,
|
|
1638
|
-
constraintFn: string,
|
|
1639
|
-
constraintXMin: string,
|
|
1640
|
-
constraintXMax: string,
|
|
1641
|
-
constraintYMin: string,
|
|
1642
|
-
constraintYMax: string
|
|
1643
|
-
})
|
|
1644
|
-
});
|
|
1645
|
-
const parseMovablePointElement = object({
|
|
1646
|
-
type: constant("movable-point"),
|
|
1647
|
-
key: parseKey,
|
|
1648
|
-
options: object({
|
|
1649
|
-
startX: string,
|
|
1650
|
-
startY: string,
|
|
1651
|
-
varSubscript: number,
|
|
1652
|
-
constraint: string,
|
|
1653
|
-
snap: number,
|
|
1654
|
-
constraintFn: string,
|
|
1655
|
-
constraintXMin: stringOrEmpty,
|
|
1656
|
-
constraintXMax: stringOrEmpty,
|
|
1657
|
-
constraintYMin: stringOrEmpty,
|
|
1658
|
-
constraintYMax: stringOrEmpty
|
|
1659
|
-
})
|
|
1660
|
-
});
|
|
1661
|
-
const parseParametricElement = object({
|
|
1662
|
-
type: constant("parametric"),
|
|
1663
|
-
key: parseKey,
|
|
1664
|
-
options: object({
|
|
1665
|
-
x: string,
|
|
1666
|
-
y: string,
|
|
1667
|
-
rangeMin: string,
|
|
1668
|
-
rangeMax: string,
|
|
1669
|
-
color: string,
|
|
1670
|
-
strokeDasharray: string,
|
|
1671
|
-
strokeWidth: number
|
|
1672
|
-
})
|
|
1673
|
-
});
|
|
1674
|
-
const parsePointElement = object({
|
|
1675
|
-
type: constant("point"),
|
|
1676
|
-
key: parseKey,
|
|
1677
|
-
options: object({
|
|
1678
|
-
color: string,
|
|
1679
|
-
coordX: string,
|
|
1680
|
-
coordY: string
|
|
1681
|
-
})
|
|
1682
|
-
});
|
|
1683
|
-
const parseRectangleElement = object({
|
|
1684
|
-
type: constant("rectangle"),
|
|
1685
|
-
key: parseKey,
|
|
1686
|
-
options: object({
|
|
1687
|
-
color: string,
|
|
1688
|
-
coordX: string,
|
|
1689
|
-
coordY: string,
|
|
1690
|
-
width: string,
|
|
1691
|
-
height: string
|
|
1692
|
-
})
|
|
1693
|
-
});
|
|
1694
|
-
const parseInteractionWidget = parseWidget(constant("interaction"), object({
|
|
1695
|
-
static: defaulted(boolean, () => false),
|
|
1696
|
-
graph: object({
|
|
1697
|
-
editableSettings: optional(array(enumeration("canvas", "graph"))),
|
|
1698
|
-
box: pairOfNumbers$1,
|
|
1699
|
-
labels: array(string),
|
|
1700
|
-
range: pair(pairOfNumbers$1, pairOfNumbers$1),
|
|
1701
|
-
gridStep: pairOfNumbers$1,
|
|
1702
|
-
markings: enumeration("graph", "grid", "none"),
|
|
1703
|
-
snapStep: optional(pairOfNumbers$1),
|
|
1704
|
-
valid: optional(union(boolean).or(string).parser),
|
|
1705
|
-
backgroundImage: optional(parsePerseusImageBackground),
|
|
1706
|
-
showProtractor: optional(boolean),
|
|
1707
|
-
showRuler: optional(boolean),
|
|
1708
|
-
rulerLabel: optional(string),
|
|
1709
|
-
rulerTicks: optional(number),
|
|
1710
|
-
tickStep: pairOfNumbers$1
|
|
1711
|
-
}),
|
|
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)
|
|
1713
|
-
}));
|
|
1714
|
-
|
|
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
|
|
107
|
+
const parseDropdownWidget=parseWidget(constant("dropdown"),object({placeholder:defaulted(string,()=>""),ariaLabel:optional(string),visibleLabel:optional(string),static:defaulted(boolean,()=>false),choices:array(object({content:string,correct:boolean}))}));
|
|
1922
108
|
|
|
1923
|
-
|
|
109
|
+
const parseExplanationWidget=parseWidget(constant("explanation"),object({showPrompt:string,hidePrompt:string,explanation:string,widgets:defaulted((rawVal,ctx)=>parseWidgetsMap(rawVal,ctx),()=>({})),static:defaulted(boolean,()=>false)}));
|
|
1924
110
|
|
|
1925
|
-
|
|
111
|
+
const KeypadKeys=["PLUS","MINUS","NEGATIVE","TIMES","DIVIDE","DECIMAL","PERIOD","PERCENT","CDOT","EQUAL","NEQ","GT","LT","GEQ","LEQ","FRAC_INCLUSIVE","FRAC_EXCLUSIVE","FRAC","EXP","EXP_2","EXP_3","SQRT","CUBE_ROOT","RADICAL","LEFT_PAREN","RIGHT_PAREN","LN","LOG","LOG_N","SIN","COS","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","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"];
|
|
1926
112
|
|
|
1927
|
-
|
|
113
|
+
function deriveExtraKeys(widgetOptions){if(widgetOptions.extraKeys){return widgetOptions.extraKeys}const defaultKeys=["PI"];if(widgetOptions.answerForms==null){return defaultKeys}const uniqueExtraVariables={};const uniqueExtraConstants={};for(const answerForm of widgetOptions.answerForms){const maybeExpr=KAS__namespace.parse(answerForm.value,widgetOptions);if(maybeExpr.parsed){const expr=maybeExpr.expr;const isGreek=symbol=>symbol==="pi"||symbol==="theta";const toKey=symbol=>isGreek(symbol)?symbol.toUpperCase():symbol;const isKey=key=>KeypadKeys.includes(key);for(const variable of expr.getVars()){const maybeKey=toKey(variable);if(isKey(maybeKey)){uniqueExtraVariables[maybeKey]=true;}}for(const constant of expr.getConsts()){const maybeKey=toKey(constant);if(isKey(maybeKey)){uniqueExtraConstants[maybeKey]=true;}}}}const extraVariables=Object.keys(uniqueExtraVariables).sort();const extraConstants=Object.keys(uniqueExtraConstants).sort();const extraKeys=[...extraVariables,...extraConstants];if(!extraKeys.length){return defaultKeys}return extraKeys}
|
|
1928
114
|
|
|
1929
|
-
|
|
115
|
+
function convert(f){return (rawValue,ctx)=>ctx.success(f(rawValue))}
|
|
1930
116
|
|
|
1931
|
-
|
|
117
|
+
const parseLegacyButtonSet=enumeration("basic","basic+div","trig","prealgebra","logarithms","basic relations","advanced relations","scientific");const parseLegacyButtonSets=defaulted(array(parseLegacyButtonSet),()=>["basic","trig","prealgebra","logarithms"]);
|
|
1932
118
|
|
|
1933
|
-
|
|
119
|
+
function versionedWidgetOptions(latestMajorVersion,parseLatest){return new VersionedWidgetOptionsParserBuilder(latestMajorVersion,parseLatest,latest=>latest,(raw,ctx)=>ctx.failure("widget options with a known version number",raw))}class VersionedWidgetOptionsParserBuilder{withMigrationFrom(majorVersion,parseOldVersion,migrateToNextVersion){const parseOtherVersions=this.parser;const migrateToLatest=old=>this.migrateToLatest(migrateToNextVersion(old));return new VersionedWidgetOptionsParserBuilder(majorVersion,parseOldVersion,migrateToLatest,parseOtherVersions)}constructor(majorVersion,parseThisVersion,migrateToLatest,parseOtherVersions){this.migrateToLatest=migrateToLatest;this.parseOtherVersions=parseOtherVersions;const parseThisVersionAndMigrateToLatest=pipeParsers(parseThisVersion).then(convert(this.migrateToLatest)).parser;this.parser=(raw,ctx)=>{if(!isObject(raw)){return ctx.failure("object",raw)}const versionParseResult=parseVersionedObject(raw,ctx);if(isFailure(versionParseResult)){return versionParseResult}if(versionParseResult.value.version.major!==majorVersion){return this.parseOtherVersions(raw,ctx)}return parseThisVersionAndMigrateToLatest(raw,ctx)};}}const parseVersionedObject=object({version:defaulted(object({major:number,minor:number}),()=>({major:0,minor:0}))});
|
|
1934
120
|
|
|
1935
|
-
|
|
121
|
+
const stringOrNumberOrNullOrUndefined=union(string).or(number).or(constant(null)).or(constant(undefined)).parser;const parsePossiblyInvalidAnswerForm=object({value:optional(string),form:defaulted(boolean,()=>false),simplify:defaulted(boolean,()=>false),considered:enumeration("correct","wrong","ungraded"),key:pipeParsers(stringOrNumberOrNullOrUndefined).then(convert(String)).parser});function removeInvalidAnswerForms(possiblyInvalid){const valid=[];for(const answerForm of possiblyInvalid){const{value}=answerForm;if(value!=null){valid.push({...answerForm,value});}}return valid}const version2$1=object({major:constant(2),minor:number});const parseExpressionWidgetV2=parseWidgetWithVersion(version2$1,constant("expression"),object({answerForms:pipeParsers(array(parsePossiblyInvalidAnswerForm)).then(convert(removeInvalidAnswerForms)).parser,functions:array(string),times:boolean,visibleLabel:optional(string),ariaLabel:optional(string),buttonSets:parseLegacyButtonSets,buttonsVisible:optional(enumeration("always","never","focused")),extraKeys:array(enumeration(...KeypadKeys))}));const version1$1=object({major:constant(1),minor:number});const parseExpressionWidgetV1=parseWidgetWithVersion(version1$1,constant("expression"),object({answerForms:pipeParsers(array(parsePossiblyInvalidAnswerForm)).then(convert(removeInvalidAnswerForms)).parser,functions:array(string),times:boolean,visibleLabel:optional(string),ariaLabel:optional(string),buttonSets:parseLegacyButtonSets,buttonsVisible:optional(enumeration("always","never","focused"))}));function migrateV1ToV2$1(widget){const{options}=widget;return {...widget,version:{major:2,minor:0},options:{times:options.times,buttonSets:options.buttonSets,functions:options.functions,buttonsVisible:options.buttonsVisible,visibleLabel:options.visibleLabel,ariaLabel:options.ariaLabel,answerForms:options.answerForms,extraKeys:deriveExtraKeys(options)}}}const version0$1=optional(object({major:constant(0),minor:number}));const parseExpressionWidgetV0=parseWidgetWithVersion(version0$1,constant("expression"),object({functions:array(string),times:boolean,visibleLabel:optional(string),ariaLabel:optional(string),form:boolean,simplify:boolean,value:string,buttonSets:parseLegacyButtonSets,buttonsVisible:optional(enumeration("always","never","focused"))}));function migrateV0ToV1$1(widget){const{options}=widget;return {...widget,version:{major:1,minor:0},options:{times:options.times,buttonSets:options.buttonSets,functions:options.functions,buttonsVisible:options.buttonsVisible,visibleLabel:options.visibleLabel,ariaLabel:options.ariaLabel,answerForms:[{considered:"correct",form:options.form,simplify:options.simplify,value:options.value}]}}}const parseExpressionWidget=versionedWidgetOptions(2,parseExpressionWidgetV2).withMigrationFrom(1,parseExpressionWidgetV1,migrateV1ToV2$1).withMigrationFrom(0,parseExpressionWidgetV0,migrateV0ToV1$1).parser;
|
|
1936
122
|
|
|
1937
|
-
|
|
123
|
+
const falseToNull=pipeParsers(constant(false)).then(convert(()=>null)).parser;const parseGradedGroupWidgetOptions=object({title:defaulted(string,()=>""),hasHint:optional(nullable(boolean)),hint:union(falseToNull).or(constant(null)).or(constant(undefined)).or((rawVal,ctx)=>parsePerseusRenderer(rawVal,ctx)).parser,content:string,widgets:(rawVal,ctx)=>parseWidgetsMap(rawVal,ctx),widgetEnabled:optional(nullable(boolean)),immutableWidgets:optional(nullable(boolean)),images:record(string,object({width:number,height:number}))});const parseGradedGroupWidget=parseWidget(constant("graded-group"),parseGradedGroupWidgetOptions);
|
|
1938
124
|
|
|
1939
|
-
|
|
125
|
+
const parseGradedGroupSetWidget=parseWidget(constant("graded-group-set"),object({gradedGroups:array(parseGradedGroupWidgetOptions)}));
|
|
1940
126
|
|
|
1941
|
-
|
|
127
|
+
function discriminatedUnionOn(discriminantKey){const noMoreBranches=(raw,ctx)=>{if(!isObject(raw)){return ctx.failure("object",raw)}return ctx.forSubtree(discriminantKey).failure("a valid value",raw[discriminantKey])};return new DiscriminatedUnionBuilder(discriminantKey,noMoreBranches)}class DiscriminatedUnionBuilder{withBranch(discriminantValue,parseNewVariant){const parseNewBranch=discriminatedUnionBranch(this.discriminantKey,discriminantValue,parseNewVariant,this.parser);return new DiscriminatedUnionBuilder(this.discriminantKey,parseNewBranch)}constructor(discriminantKey,parser){this.discriminantKey=discriminantKey;this.parser=parser;}}function discriminatedUnionBranch(discriminantKey,discriminantValue,parseVariant,parseOtherBranches){return (raw,ctx)=>{if(!isObject(raw)){return ctx.failure("object",raw)}if(raw[discriminantKey]===discriminantValue){return parseVariant(raw,ctx)}return parseOtherBranches(raw,ctx)}}
|
|
1942
128
|
|
|
1943
|
-
|
|
129
|
+
const pairOfNumbers$3=pair(number,number);const pairOfPoints=pair(pairOfNumbers$3,pairOfNumbers$3);const parseGrapherWidget=parseWidget(constant("grapher"),object({availableTypes:array(enumeration("absolute_value","exponential","linear","logarithm","quadratic","sinusoid","tangent")),correct:discriminatedUnionOn("type").withBranch("absolute_value",object({type:constant("absolute_value"),coords:nullable(pairOfPoints)})).withBranch("exponential",object({type:constant("exponential"),asymptote:pairOfPoints,coords:nullable(pairOfPoints)})).withBranch("linear",object({type:constant("linear"),coords:nullable(pairOfPoints)})).withBranch("logarithm",object({type:constant("logarithm"),asymptote:pairOfPoints,coords:nullable(pairOfPoints)})).withBranch("quadratic",object({type:constant("quadratic"),coords:nullable(pairOfPoints)})).withBranch("sinusoid",object({type:constant("sinusoid"),coords:nullable(pairOfPoints)})).withBranch("tangent",object({type:constant("tangent"),coords:nullable(pairOfPoints)})).parser,graph:object({backgroundImage:object({bottom:optional(number),height:optional(number),left:optional(number),scale:optional(number),url:optional(nullable(string)),width:optional(number)}),box:optional(pairOfNumbers$3),editableSettings:optional(array(enumeration("graph","snap","image","measure"))),gridStep:optional(pairOfNumbers$3),labels:pair(string,string),markings:enumeration("graph","none","grid"),range:pair(pairOfNumbers$3,pairOfNumbers$3),rulerLabel:constant(""),rulerTicks:number,showProtractor:optional(boolean),showRuler:optional(boolean),showTooltips:optional(boolean),snapStep:optional(pairOfNumbers$3),step:pairOfNumbers$3,valid:optional(union(boolean).or(string).parser)})}));
|
|
1944
130
|
|
|
1945
|
-
|
|
131
|
+
const parseGroupWidget=parseWidget(constant("group"),(rawVal,ctx)=>parsePerseusRenderer(rawVal,ctx));
|
|
1946
132
|
|
|
1947
|
-
|
|
133
|
+
const parseIframeWidget=parseWidget(constant("iframe"),object({url:string,settings:optional(array(object({name:string,value:string}))),width:union(number).or(string).parser,height:union(number).or(string).parser,allowFullScreen:defaulted(boolean,()=>false),allowTopNavigation:optional(boolean),static:defaulted(boolean,()=>false)}));
|
|
1948
134
|
|
|
1949
|
-
|
|
135
|
+
const stringToNumber=(rawValue,ctx)=>{if(typeof rawValue==="number"){return ctx.success(rawValue)}const parsedNumber=+rawValue;if(rawValue===""||isNaN(parsedNumber)){return ctx.failure("a number or numeric string",rawValue)}return ctx.success(parsedNumber)};
|
|
1950
136
|
|
|
1951
|
-
|
|
137
|
+
function emptyToZero(x){return x===""?0:x}const imageDimensionToNumber=pipeParsers(union(number).or(string).parser).then(convert(emptyToZero)).then(stringToNumber).parser;const dimensionOrUndefined=defaulted(imageDimensionToNumber,()=>undefined);const parsePerseusImageBackground=object({url:optional(nullable(string)),width:dimensionOrUndefined,height:dimensionOrUndefined,top:dimensionOrUndefined,left:dimensionOrUndefined,bottom:dimensionOrUndefined,scale:dimensionOrUndefined});
|
|
1952
138
|
|
|
1953
|
-
|
|
139
|
+
const pairOfNumbers$2=pair(number,number);const parseImageWidget=parseWidget(constant("image"),object({title:optional(string),caption:optional(string),alt:optional(string),backgroundImage:parsePerseusImageBackground,static:optional(boolean),labels:optional(array(object({content:string,alignment:string,coordinates:array(number)}))),range:optional(pair(pairOfNumbers$2,pairOfNumbers$2)),box:optional(pairOfNumbers$2)}));
|
|
1954
140
|
|
|
1955
|
-
|
|
141
|
+
const booleanToString=(rawValue,ctx)=>{if(typeof rawValue==="boolean"){return ctx.success(String(rawValue))}return ctx.failure("boolean",rawValue)};const parseInputNumberWidget=parseWidget(constant("input-number"),object({answerType:optional(enumeration("number","decimal","integer","rational","improper","mixed","percent","pi")),inexact:optional(boolean),maxError:optional(union(number).or(string).parser),rightAlign:optional(boolean),simplify:enumeration("required","optional","enforced"),size:enumeration("normal","small"),value:union(number).or(string).or(booleanToString).parser,customKeypad:optional(boolean)}));
|
|
1956
142
|
|
|
1957
|
-
|
|
143
|
+
const pairOfNumbers$1=pair(number,number);const stringOrEmpty=defaulted(string,()=>"");const parseKey=pipeParsers(optional(string)).then(convert(String)).parser;const parseFunctionElement=object({type:constant("function"),key:parseKey,options:object({value:string,funcName:string,rangeMin:string,rangeMax:string,color:string,strokeDasharray:string,strokeWidth:number})});const parseLabelElement=object({type:constant("label"),key:parseKey,options:object({label:string,color:string,coordX:string,coordY:string})});const parseLineElement=object({type:constant("line"),key:parseKey,options:object({color:string,startX:string,startY:string,endX:string,endY:string,strokeDasharray:string,strokeWidth:number,arrows:string})});const parseMovableLineElement=object({type:constant("movable-line"),key:parseKey,options:object({startX:string,startY:string,startSubscript:number,endX:string,endY:string,endSubscript:number,constraint:string,snap:number,constraintFn:string,constraintXMin:string,constraintXMax:string,constraintYMin:string,constraintYMax:string})});const parseMovablePointElement=object({type:constant("movable-point"),key:parseKey,options:object({startX:string,startY:string,varSubscript:number,constraint:string,snap:number,constraintFn:string,constraintXMin:stringOrEmpty,constraintXMax:stringOrEmpty,constraintYMin:stringOrEmpty,constraintYMax:stringOrEmpty})});const parseParametricElement=object({type:constant("parametric"),key:parseKey,options:object({x:string,y:string,rangeMin:string,rangeMax:string,color:string,strokeDasharray:string,strokeWidth:number})});const parsePointElement=object({type:constant("point"),key:parseKey,options:object({color:string,coordX:string,coordY:string})});const parseRectangleElement=object({type:constant("rectangle"),key:parseKey,options:object({color:string,coordX:string,coordY:string,width:string,height:string})});const parseInteractionWidget=parseWidget(constant("interaction"),object({static:defaulted(boolean,()=>false),graph:object({editableSettings:optional(array(enumeration("canvas","graph"))),box:pairOfNumbers$1,labels:array(string),range:pair(pairOfNumbers$1,pairOfNumbers$1),gridStep:pairOfNumbers$1,markings:enumeration("graph","grid","none"),snapStep:optional(pairOfNumbers$1),valid:optional(union(boolean).or(string).parser),backgroundImage:optional(parsePerseusImageBackground),showProtractor:optional(boolean),showRuler:optional(boolean),rulerLabel:optional(string),rulerTicks:optional(number),tickStep:pairOfNumbers$1}),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)}));
|
|
1958
144
|
|
|
1959
|
-
|
|
145
|
+
const ItemExtras=["calculator","chi2Table","financialCalculatorMonthlyPayment","financialCalculatorTotalAmount","financialCalculatorTimeToPayOff","periodicTable","periodicTableWithKey","tTable","zTable"];const PerseusExpressionAnswerFormConsidered=["correct","wrong","ungraded"];const lockedFigureColorNames=["blue","green","grayH","purple","pink","orange","red"];const lockedFigureColors={blue:"#3D7586",green:"#447A53",grayH:"#3B3D45",purple:"#594094",pink:"#B25071",red:"#D92916",orange:"#946700"};const lockedFigureFillStyles={none:0,white:1,translucent:.4,solid:1};const plotterPlotTypes=["bar","line","pic","histogram","dotplot"];
|
|
1960
146
|
|
|
1961
|
-
|
|
147
|
+
const pairOfNumbers=pair(number,number);const parsePerseusGraphTypeAngle=object({type:constant("angle"),showAngles:optional(boolean),allowReflexAngles:optional(boolean),angleOffsetDeg:optional(number),snapDegrees:optional(number),match:optional(constant("congruent")),coords:optional(trio(pairOfNumbers,pairOfNumbers,pairOfNumbers)),startCoords:optional(trio(pairOfNumbers,pairOfNumbers,pairOfNumbers))});const parsePerseusGraphTypeCircle=object({type:constant("circle"),center:optional(pairOfNumbers),radius:optional(number),startCoords:optional(object({center:pairOfNumbers,radius:number})),coord:optional(pairOfNumbers)});const parsePerseusGraphTypeLinear=object({type:constant("linear"),coords:optional(nullable(pair(pairOfNumbers,pairOfNumbers))),startCoords:optional(pair(pairOfNumbers,pairOfNumbers)),coord:optional(pairOfNumbers)});const parsePerseusGraphTypeLinearSystem=object({type:constant("linear-system"),coords:optional(nullable(array(pair(pairOfNumbers,pairOfNumbers)))),startCoords:optional(array(pair(pairOfNumbers,pairOfNumbers))),coord:optional(pairOfNumbers)});const parsePerseusGraphTypeNone=object({type:constant("none")});const parsePerseusGraphTypePoint=object({type:constant("point"),numPoints:optional(union(number).or(constant("unlimited")).parser),coords:optional(nullable(array(pairOfNumbers))),startCoords:optional(array(pairOfNumbers)),coord:optional(pairOfNumbers)});const parsePerseusGraphTypePolygon=object({type:constant("polygon"),numSides:optional(union(number).or(constant("unlimited")).parser),showAngles:optional(boolean),showSides:optional(boolean),snapTo:optional(enumeration("grid","angles","sides")),match:optional(enumeration("similar","congruent","approx","exact")),startCoords:optional(array(pairOfNumbers)),coord:optional(pairOfNumbers)});const parsePerseusGraphTypeQuadratic=object({type:constant("quadratic"),coords:optional(nullable(trio(pairOfNumbers,pairOfNumbers,pairOfNumbers))),startCoords:optional(trio(pairOfNumbers,pairOfNumbers,pairOfNumbers)),coord:optional(pairOfNumbers)});const parsePerseusGraphTypeRay=object({type:constant("ray"),coords:optional(nullable(pair(pairOfNumbers,pairOfNumbers))),startCoords:optional(pair(pairOfNumbers,pairOfNumbers)),coord:optional(pairOfNumbers)});const parsePerseusGraphTypeSegment=object({type:constant("segment"),numSegments:optional(number),coords:optional(nullable(array(pair(pairOfNumbers,pairOfNumbers)))),startCoords:optional(array(pair(pairOfNumbers,pairOfNumbers))),coord:optional(pairOfNumbers)});const parsePerseusGraphTypeSinusoid=object({type:constant("sinusoid"),coords:optional(nullable(array(pairOfNumbers))),startCoords:optional(array(pairOfNumbers)),coord:optional(pairOfNumbers)});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;const parseLockedFigureColor=enumeration(...lockedFigureColorNames);const parseLockedFigureFillType=enumeration("none","white","translucent","solid");const parseLockedLineStyle=enumeration("solid","dashed");const parseLockedLabelType=object({type:constant("label"),coord:pairOfNumbers,text:string,color:parseLockedFigureColor,size:enumeration("small","medium","large")});const parseLockedPointType=object({type:constant("point"),coord:pairOfNumbers,color:parseLockedFigureColor,filled:boolean,labels:optional(array(parseLockedLabelType)),ariaLabel:optional(string)});const parseLockedLineType=object({type:constant("line"),kind:enumeration("line","ray","segment"),points:pair(parseLockedPointType,parseLockedPointType),color:parseLockedFigureColor,lineStyle:parseLockedLineStyle,showPoint1:defaulted(boolean,()=>false),showPoint2:defaulted(boolean,()=>false),labels:optional(array(parseLockedLabelType)),ariaLabel:optional(string)});const parseLockedVectorType=object({type:constant("vector"),points:pair(pairOfNumbers,pairOfNumbers),color:parseLockedFigureColor,labels:optional(array(parseLockedLabelType)),ariaLabel:optional(string)});const parseLockedEllipseType=object({type:constant("ellipse"),center:pairOfNumbers,radius:pairOfNumbers,angle:number,color:parseLockedFigureColor,fillStyle:parseLockedFigureFillType,strokeStyle:parseLockedLineStyle,labels:optional(array(parseLockedLabelType)),ariaLabel:optional(string)});const parseLockedPolygonType=object({type:constant("polygon"),points:array(pairOfNumbers),color:parseLockedFigureColor,showVertices:boolean,fillStyle:parseLockedFigureFillType,strokeStyle:parseLockedLineStyle,labels:optional(array(parseLockedLabelType)),ariaLabel:optional(string)});const parseLockedFunctionDomain=defaulted(pair(defaulted(number,()=>-Infinity),defaulted(number,()=>Infinity)),()=>[-Infinity,Infinity]);const parseLockedFunctionType=object({type:constant("function"),color:parseLockedFigureColor,strokeStyle:parseLockedLineStyle,equation:string,directionalAxis:enumeration("x","y"),domain:parseLockedFunctionDomain,labels:optional(array(parseLockedLabelType)),ariaLabel:optional(string)});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;const parseInteractiveGraphWidget=parseWidget(constant("interactive-graph"),object({step:pairOfNumbers,gridStep:optional(pairOfNumbers),snapStep:optional(pairOfNumbers),backgroundImage:optional(parsePerseusImageBackground),markings:enumeration("graph","grid","none"),labels:optional(array(string)),labelLocation:optional(enumeration("onAxis","alongEdge")),showProtractor:boolean,showRuler:optional(boolean),showTooltips:optional(boolean),rulerLabel:optional(string),rulerTicks:optional(number),range:pair(pairOfNumbers,pairOfNumbers),graph:defaulted(parsePerseusGraphType,()=>({type:"linear"})),correct:parsePerseusGraphType,lockedFigures:optional(array(parseLockedFigure)),fullGraphLabel:optional(string),fullGraphAriaDescription:optional(string)}));
|
|
1962
148
|
|
|
1963
|
-
|
|
1964
|
-
* A background image applied to various widgets.
|
|
1965
|
-
*/
|
|
149
|
+
const parseLabelImageWidget=parseWidget(constant("label-image"),object({choices:array(string),imageUrl:string,imageAlt:string,imageHeight:number,imageWidth:number,markers:array(object({answers:array(string),label:string,x:number,y:number})),hideChoicesFromInstructions:boolean,multipleAnswers:boolean,static:defaulted(boolean,()=>false)}));
|
|
1966
150
|
|
|
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
|
-
*/
|
|
151
|
+
const parseMatcherWidget=parseWidget(constant("matcher"),object({labels:array(string),left:array(string),right:array(string),orderMatters:boolean,padding:boolean}));
|
|
1974
152
|
|
|
1975
|
-
const
|
|
153
|
+
const numberOrString=union(number).or(string).parser;const numeric=pipeParsers(defaulted(numberOrString,()=>NaN)).then(stringToNumber).parser;const parseMatrixWidget=parseWidget(defaulted(constant("matrix"),()=>"matrix"),object({prefix:optional(string),suffix:optional(string),answers:array(array(numeric)),cursorPosition:optional(array(number)),matrixBoardSize:array(number),static:optional(boolean)}));
|
|
1976
154
|
|
|
1977
|
-
|
|
155
|
+
const parseMeasurerWidget=parseWidget(constant("measurer"),object({image:defaulted(parsePerseusImageBackground,()=>({url:null,top:0,left:0})),showProtractor:boolean,showRuler:boolean,rulerLabel:string,rulerTicks:number,rulerPixels:number,rulerLength:number,box:pair(number,number),static:defaulted(boolean,()=>false)}));
|
|
1978
156
|
|
|
1979
|
-
const
|
|
1980
|
-
const lockedFigureColors = {
|
|
1981
|
-
blue: "#3D7586",
|
|
1982
|
-
green: "#447A53",
|
|
1983
|
-
grayH: "#3B3D45",
|
|
1984
|
-
purple: "#594094",
|
|
1985
|
-
pink: "#B25071",
|
|
1986
|
-
red: "#D92916",
|
|
1987
|
-
orange: "#946700"
|
|
1988
|
-
};
|
|
1989
|
-
const lockedFigureFillStyles = {
|
|
1990
|
-
none: 0,
|
|
1991
|
-
white: 1,
|
|
1992
|
-
translucent: 0.4,
|
|
1993
|
-
solid: 1
|
|
1994
|
-
};
|
|
157
|
+
const parseMoleculeRendererWidget=parseWidget(constant("molecule-renderer"),object({widgetId:string,rotationAngle:optional(number),smiles:optional(string)}));
|
|
1995
158
|
|
|
1996
|
-
|
|
159
|
+
const emptyStringToNull=pipeParsers(constant("")).then(convert(()=>null)).parser;const parseNumberLineWidget=parseWidget(constant("number-line"),object({range:array(number),labelRange:array(nullable(union(number).or(emptyStringToNull).parser)),labelStyle:string,labelTicks:boolean,isTickCtrl:optional(nullable(boolean)),divisionRange:array(number),numDivisions:optional(nullable(number)),snapDivisions:defaulted(number,()=>2),tickStep:optional(nullable(number)),correctRel:optional(nullable(string)),correctX:nullable(number),initialX:optional(nullable(number)),showTooltips:optional(boolean),static:defaulted(boolean,()=>false)}));
|
|
1997
160
|
|
|
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
|
-
*/
|
|
161
|
+
const parseMathFormat=enumeration("integer","mixed","improper","proper","decimal","percent","pi");const parseSimplify=pipeParsers(union(constant(null)).or(constant(undefined)).or(boolean).or(constant("required")).or(constant("correct")).or(constant("enforced")).or(constant("optional")).or(constant("accepted")).parser).then(convert(deprecatedSimplifyValuesToRequired)).parser;function deprecatedSimplifyValuesToRequired(simplify){switch(simplify){case "enforced":case "required":case "optional":return simplify;default:return "required"}}const parseNumericInputWidget=parseWidget(constant("numeric-input"),object({answers:array(object({message:string,value:optional(nullable(number)),status:string,answerForms:defaulted(array(parseMathFormat),()=>undefined),strict:boolean,maxError:optional(nullable(number)),simplify:parseSimplify})),labelText:optional(string),size:string,coefficient:defaulted(boolean,()=>false),rightAlign:optional(boolean),static:defaulted(boolean,()=>false),answerForms:optional(array(object({name:parseMathFormat,simplify:parseSimplify})))}));
|
|
2006
162
|
|
|
2007
|
-
const
|
|
163
|
+
function parseRenderer(rawValue,ctx){return parsePerseusRenderer(rawValue,ctx)}const largeToAuto=(height,ctx)=>{if(height==="large"){return ctx.success("auto")}return ctx.success(height)};const parseOrdererWidget=parseWidget(constant("orderer"),object({options:defaulted(array(parseRenderer),()=>[]),correctOptions:array(parseRenderer),otherOptions:array(parseRenderer),height:pipeParsers(enumeration("normal","auto","large")).then(largeToAuto).parser,layout:defaulted(enumeration("horizontal","vertical"),()=>"horizontal")}));
|
|
2008
164
|
|
|
2009
|
-
|
|
2010
|
-
const pairOfNumbers = pair(number, number);
|
|
2011
|
-
const parsePerseusGraphTypeAngle = object({
|
|
2012
|
-
type: constant("angle"),
|
|
2013
|
-
showAngles: optional(boolean),
|
|
2014
|
-
allowReflexAngles: optional(boolean),
|
|
2015
|
-
angleOffsetDeg: optional(number),
|
|
2016
|
-
snapDegrees: optional(number),
|
|
2017
|
-
match: optional(constant("congruent")),
|
|
2018
|
-
coords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
|
|
2019
|
-
startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))
|
|
2020
|
-
});
|
|
2021
|
-
const parsePerseusGraphTypeCircle = object({
|
|
2022
|
-
type: constant("circle"),
|
|
2023
|
-
center: optional(pairOfNumbers),
|
|
2024
|
-
radius: optional(number),
|
|
2025
|
-
startCoords: optional(object({
|
|
2026
|
-
center: pairOfNumbers,
|
|
2027
|
-
radius: number
|
|
2028
|
-
})),
|
|
2029
|
-
// TODO: remove coord? it's legacy.
|
|
2030
|
-
coord: optional(pairOfNumbers)
|
|
2031
|
-
});
|
|
2032
|
-
const parsePerseusGraphTypeLinear = object({
|
|
2033
|
-
type: constant("linear"),
|
|
2034
|
-
coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
|
|
2035
|
-
startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
|
|
2036
|
-
// TODO: remove coord? it's legacy.
|
|
2037
|
-
coord: optional(pairOfNumbers)
|
|
2038
|
-
});
|
|
2039
|
-
const parsePerseusGraphTypeLinearSystem = object({
|
|
2040
|
-
type: constant("linear-system"),
|
|
2041
|
-
// TODO(benchristel): default coords to empty array?
|
|
2042
|
-
coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
|
|
2043
|
-
startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
|
|
2044
|
-
// TODO: remove coord? it's legacy.
|
|
2045
|
-
coord: optional(pairOfNumbers)
|
|
2046
|
-
});
|
|
2047
|
-
const parsePerseusGraphTypeNone = object({
|
|
2048
|
-
type: constant("none")
|
|
2049
|
-
});
|
|
2050
|
-
const parsePerseusGraphTypePoint = object({
|
|
2051
|
-
type: constant("point"),
|
|
2052
|
-
numPoints: optional(union(number).or(constant("unlimited")).parser),
|
|
2053
|
-
coords: optional(nullable(array(pairOfNumbers))),
|
|
2054
|
-
startCoords: optional(array(pairOfNumbers)),
|
|
2055
|
-
// TODO: remove coord? it's legacy.
|
|
2056
|
-
coord: optional(pairOfNumbers)
|
|
2057
|
-
});
|
|
2058
|
-
const parsePerseusGraphTypePolygon = object({
|
|
2059
|
-
type: constant("polygon"),
|
|
2060
|
-
numSides: optional(union(number).or(constant("unlimited")).parser),
|
|
2061
|
-
showAngles: optional(boolean),
|
|
2062
|
-
showSides: optional(boolean),
|
|
2063
|
-
snapTo: optional(enumeration("grid", "angles", "sides")),
|
|
2064
|
-
match: optional(enumeration("similar", "congruent", "approx", "exact")),
|
|
2065
|
-
startCoords: optional(array(pairOfNumbers)),
|
|
2066
|
-
// TODO: remove coord? it's legacy.
|
|
2067
|
-
coord: optional(pairOfNumbers)
|
|
2068
|
-
});
|
|
2069
|
-
const parsePerseusGraphTypeQuadratic = object({
|
|
2070
|
-
type: constant("quadratic"),
|
|
2071
|
-
coords: optional(nullable(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))),
|
|
2072
|
-
startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
|
|
2073
|
-
// TODO: remove coord? it's legacy.
|
|
2074
|
-
coord: optional(pairOfNumbers)
|
|
2075
|
-
});
|
|
2076
|
-
const parsePerseusGraphTypeRay = object({
|
|
2077
|
-
type: constant("ray"),
|
|
2078
|
-
coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
|
|
2079
|
-
startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
|
|
2080
|
-
// TODO: remove coord? it's legacy.
|
|
2081
|
-
coord: optional(pairOfNumbers)
|
|
2082
|
-
});
|
|
2083
|
-
const parsePerseusGraphTypeSegment = object({
|
|
2084
|
-
type: constant("segment"),
|
|
2085
|
-
// TODO(benchristel): default numSegments?
|
|
2086
|
-
numSegments: optional(number),
|
|
2087
|
-
coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
|
|
2088
|
-
startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
|
|
2089
|
-
// TODO: remove coord? it's legacy.
|
|
2090
|
-
coord: optional(pairOfNumbers)
|
|
2091
|
-
});
|
|
2092
|
-
const parsePerseusGraphTypeSinusoid = object({
|
|
2093
|
-
type: constant("sinusoid"),
|
|
2094
|
-
coords: optional(nullable(array(pairOfNumbers))),
|
|
2095
|
-
startCoords: optional(array(pairOfNumbers)),
|
|
2096
|
-
// TODO: remove coord? it's legacy.
|
|
2097
|
-
coord: optional(pairOfNumbers)
|
|
2098
|
-
});
|
|
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;
|
|
2100
|
-
const parseLockedFigureColor = enumeration(...lockedFigureColorNames);
|
|
2101
|
-
const parseLockedFigureFillType = enumeration("none", "white", "translucent", "solid");
|
|
2102
|
-
const parseLockedLineStyle = enumeration("solid", "dashed");
|
|
2103
|
-
const parseLockedLabelType = object({
|
|
2104
|
-
type: constant("label"),
|
|
2105
|
-
coord: pairOfNumbers,
|
|
2106
|
-
text: string,
|
|
2107
|
-
color: parseLockedFigureColor,
|
|
2108
|
-
size: enumeration("small", "medium", "large")
|
|
2109
|
-
});
|
|
2110
|
-
const parseLockedPointType = object({
|
|
2111
|
-
type: constant("point"),
|
|
2112
|
-
coord: pairOfNumbers,
|
|
2113
|
-
color: parseLockedFigureColor,
|
|
2114
|
-
filled: boolean,
|
|
2115
|
-
// TODO(benchristel): default labels to empty array?
|
|
2116
|
-
labels: optional(array(parseLockedLabelType)),
|
|
2117
|
-
ariaLabel: optional(string)
|
|
2118
|
-
});
|
|
2119
|
-
const parseLockedLineType = object({
|
|
2120
|
-
type: constant("line"),
|
|
2121
|
-
kind: enumeration("line", "ray", "segment"),
|
|
2122
|
-
points: pair(parseLockedPointType, parseLockedPointType),
|
|
2123
|
-
color: parseLockedFigureColor,
|
|
2124
|
-
lineStyle: parseLockedLineStyle,
|
|
2125
|
-
showPoint1: defaulted(boolean, () => false),
|
|
2126
|
-
showPoint2: defaulted(boolean, () => false),
|
|
2127
|
-
// TODO(benchristel): default labels to empty array?
|
|
2128
|
-
labels: optional(array(parseLockedLabelType)),
|
|
2129
|
-
ariaLabel: optional(string)
|
|
2130
|
-
});
|
|
2131
|
-
const parseLockedVectorType = object({
|
|
2132
|
-
type: constant("vector"),
|
|
2133
|
-
points: pair(pairOfNumbers, pairOfNumbers),
|
|
2134
|
-
color: parseLockedFigureColor,
|
|
2135
|
-
// TODO(benchristel): default labels to empty array?
|
|
2136
|
-
labels: optional(array(parseLockedLabelType)),
|
|
2137
|
-
ariaLabel: optional(string)
|
|
2138
|
-
});
|
|
2139
|
-
const parseLockedEllipseType = object({
|
|
2140
|
-
type: constant("ellipse"),
|
|
2141
|
-
center: pairOfNumbers,
|
|
2142
|
-
radius: pairOfNumbers,
|
|
2143
|
-
angle: number,
|
|
2144
|
-
color: parseLockedFigureColor,
|
|
2145
|
-
fillStyle: parseLockedFigureFillType,
|
|
2146
|
-
strokeStyle: parseLockedLineStyle,
|
|
2147
|
-
// TODO(benchristel): default labels to empty array?
|
|
2148
|
-
labels: optional(array(parseLockedLabelType)),
|
|
2149
|
-
ariaLabel: optional(string)
|
|
2150
|
-
});
|
|
2151
|
-
const parseLockedPolygonType = object({
|
|
2152
|
-
type: constant("polygon"),
|
|
2153
|
-
points: array(pairOfNumbers),
|
|
2154
|
-
color: parseLockedFigureColor,
|
|
2155
|
-
showVertices: boolean,
|
|
2156
|
-
fillStyle: parseLockedFigureFillType,
|
|
2157
|
-
strokeStyle: parseLockedLineStyle,
|
|
2158
|
-
// TODO(benchristel): default labels to empty array?
|
|
2159
|
-
labels: optional(array(parseLockedLabelType)),
|
|
2160
|
-
ariaLabel: optional(string)
|
|
2161
|
-
});
|
|
165
|
+
const parsePassageRefWidget=parseWidget(constant("passage-ref"),object({passageNumber:number,referenceNumber:number,summaryText:optional(string)}));
|
|
2162
166
|
|
|
2163
|
-
|
|
2164
|
-
const parseLockedFunctionDomain = defaulted(pair(defaulted(number, () => -Infinity), defaulted(number, () => Infinity)), () => [-Infinity, Infinity]);
|
|
2165
|
-
const parseLockedFunctionType = object({
|
|
2166
|
-
type: constant("function"),
|
|
2167
|
-
color: parseLockedFigureColor,
|
|
2168
|
-
strokeStyle: parseLockedLineStyle,
|
|
2169
|
-
equation: string,
|
|
2170
|
-
directionalAxis: enumeration("x", "y"),
|
|
2171
|
-
domain: parseLockedFunctionDomain,
|
|
2172
|
-
// TODO(benchristel): default labels to empty array?
|
|
2173
|
-
labels: optional(array(parseLockedLabelType)),
|
|
2174
|
-
ariaLabel: optional(string)
|
|
2175
|
-
});
|
|
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;
|
|
2177
|
-
const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
|
|
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.
|
|
2183
|
-
gridStep: optional(pairOfNumbers),
|
|
2184
|
-
snapStep: optional(pairOfNumbers),
|
|
2185
|
-
backgroundImage: optional(parsePerseusImageBackground),
|
|
2186
|
-
markings: enumeration("graph", "grid", "none"),
|
|
2187
|
-
labels: optional(array(string)),
|
|
2188
|
-
showProtractor: boolean,
|
|
2189
|
-
showRuler: optional(boolean),
|
|
2190
|
-
showTooltips: optional(boolean),
|
|
2191
|
-
rulerLabel: optional(string),
|
|
2192
|
-
rulerTicks: optional(number),
|
|
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.
|
|
2197
|
-
graph: defaulted(parsePerseusGraphType, () => ({
|
|
2198
|
-
type: "linear"
|
|
2199
|
-
})),
|
|
2200
|
-
correct: parsePerseusGraphType,
|
|
2201
|
-
// TODO(benchristel): default lockedFigures to empty array
|
|
2202
|
-
lockedFigures: optional(array(parseLockedFigure)),
|
|
2203
|
-
fullGraphLabel: optional(string),
|
|
2204
|
-
fullGraphAriaDescription: optional(string)
|
|
2205
|
-
}));
|
|
2206
|
-
|
|
2207
|
-
const parseLabelImageWidget = parseWidget(constant("label-image"), object({
|
|
2208
|
-
choices: array(string),
|
|
2209
|
-
imageUrl: string,
|
|
2210
|
-
imageAlt: string,
|
|
2211
|
-
imageHeight: number,
|
|
2212
|
-
imageWidth: number,
|
|
2213
|
-
markers: array(object({
|
|
2214
|
-
answers: array(string),
|
|
2215
|
-
label: string,
|
|
2216
|
-
x: number,
|
|
2217
|
-
y: number
|
|
2218
|
-
})),
|
|
2219
|
-
hideChoicesFromInstructions: boolean,
|
|
2220
|
-
multipleAnswers: boolean,
|
|
2221
|
-
static: defaulted(boolean, () => false)
|
|
2222
|
-
}));
|
|
2223
|
-
|
|
2224
|
-
const parseMatcherWidget = parseWidget(constant("matcher"), object({
|
|
2225
|
-
labels: array(string),
|
|
2226
|
-
left: array(string),
|
|
2227
|
-
right: array(string),
|
|
2228
|
-
orderMatters: boolean,
|
|
2229
|
-
padding: boolean
|
|
2230
|
-
}));
|
|
2231
|
-
|
|
2232
|
-
const numberOrString = union(number).or(string).parser;
|
|
2233
|
-
const numeric = pipeParsers(defaulted(numberOrString, () => NaN)).then(stringToNumber).parser;
|
|
2234
|
-
const parseMatrixWidget = parseWidget(defaulted(constant("matrix"), () => "matrix"), object({
|
|
2235
|
-
prefix: optional(string),
|
|
2236
|
-
suffix: optional(string),
|
|
2237
|
-
answers: array(array(numeric)),
|
|
2238
|
-
cursorPosition: optional(array(number)),
|
|
2239
|
-
matrixBoardSize: array(number),
|
|
2240
|
-
static: optional(boolean)
|
|
2241
|
-
}));
|
|
2242
|
-
|
|
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.
|
|
2247
|
-
image: defaulted(parsePerseusImageBackground, () => ({
|
|
2248
|
-
url: null,
|
|
2249
|
-
top: 0,
|
|
2250
|
-
left: 0
|
|
2251
|
-
})),
|
|
2252
|
-
showProtractor: boolean,
|
|
2253
|
-
showRuler: boolean,
|
|
2254
|
-
rulerLabel: string,
|
|
2255
|
-
rulerTicks: number,
|
|
2256
|
-
rulerPixels: number,
|
|
2257
|
-
rulerLength: number,
|
|
2258
|
-
box: pair(number, number),
|
|
2259
|
-
// TODO(benchristel): static is not used. Remove it?
|
|
2260
|
-
static: defaulted(boolean, () => false)
|
|
2261
|
-
}));
|
|
2262
|
-
|
|
2263
|
-
const parseMoleculeRendererWidget = parseWidget(constant("molecule-renderer"), object({
|
|
2264
|
-
widgetId: string,
|
|
2265
|
-
rotationAngle: optional(number),
|
|
2266
|
-
smiles: optional(string)
|
|
2267
|
-
}));
|
|
2268
|
-
|
|
2269
|
-
const emptyStringToNull = pipeParsers(constant("")).then(convert(() => null)).parser;
|
|
2270
|
-
const parseNumberLineWidget = parseWidget(constant("number-line"), object({
|
|
2271
|
-
range: array(number),
|
|
2272
|
-
labelRange: array(nullable(union(number).or(emptyStringToNull).parser)),
|
|
2273
|
-
labelStyle: string,
|
|
2274
|
-
labelTicks: boolean,
|
|
2275
|
-
isTickCtrl: optional(nullable(boolean)),
|
|
2276
|
-
divisionRange: array(number),
|
|
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.
|
|
2281
|
-
snapDivisions: defaulted(number, () => 2),
|
|
2282
|
-
tickStep: optional(nullable(number)),
|
|
2283
|
-
correctRel: optional(nullable(string)),
|
|
2284
|
-
correctX: nullable(number),
|
|
2285
|
-
initialX: optional(nullable(number)),
|
|
2286
|
-
showTooltips: optional(boolean),
|
|
2287
|
-
static: defaulted(boolean, () => false)
|
|
2288
|
-
}));
|
|
2289
|
-
|
|
2290
|
-
const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "decimal", "percent", "pi");
|
|
2291
|
-
const parseSimplify = pipeParsers(union(constant(null)).or(constant(undefined)).or(boolean).or(constant("required")).or(constant("correct")).or(constant("enforced")).or(constant("optional")).or(constant("accepted")).parser).then(convert(deprecatedSimplifyValuesToRequired)).parser;
|
|
2292
|
-
function deprecatedSimplifyValuesToRequired(simplify) {
|
|
2293
|
-
switch (simplify) {
|
|
2294
|
-
case "enforced":
|
|
2295
|
-
case "required":
|
|
2296
|
-
case "optional":
|
|
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
|
|
2302
|
-
default:
|
|
2303
|
-
return "required";
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
|
|
2307
|
-
answers: array(object({
|
|
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.
|
|
2312
|
-
value: optional(nullable(number)),
|
|
2313
|
-
status: string,
|
|
2314
|
-
answerForms: defaulted(array(parseMathFormat), () => undefined),
|
|
2315
|
-
strict: boolean,
|
|
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`.
|
|
2320
|
-
simplify: parseSimplify
|
|
2321
|
-
})),
|
|
2322
|
-
labelText: optional(string),
|
|
2323
|
-
size: string,
|
|
2324
|
-
coefficient: defaulted(boolean, () => false),
|
|
2325
|
-
rightAlign: optional(boolean),
|
|
2326
|
-
static: defaulted(boolean, () => false),
|
|
2327
|
-
answerForms: optional(array(object({
|
|
2328
|
-
name: parseMathFormat,
|
|
2329
|
-
simplify: parseSimplify
|
|
2330
|
-
})))
|
|
2331
|
-
}));
|
|
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.
|
|
2336
|
-
function parseRenderer(rawValue, ctx) {
|
|
2337
|
-
return parsePerseusRenderer(rawValue, ctx);
|
|
2338
|
-
}
|
|
2339
|
-
const largeToAuto = (height, ctx) => {
|
|
2340
|
-
if (height === "large") {
|
|
2341
|
-
return ctx.success("auto");
|
|
2342
|
-
}
|
|
2343
|
-
return ctx.success(height);
|
|
2344
|
-
};
|
|
2345
|
-
const parseOrdererWidget = parseWidget(constant("orderer"), object({
|
|
2346
|
-
options: defaulted(array(parseRenderer), () => []),
|
|
2347
|
-
correctOptions: array(parseRenderer),
|
|
2348
|
-
otherOptions: array(parseRenderer),
|
|
2349
|
-
height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
|
|
2350
|
-
layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
|
|
2351
|
-
}));
|
|
2352
|
-
|
|
2353
|
-
const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
|
|
2354
|
-
passageNumber: number,
|
|
2355
|
-
referenceNumber: number,
|
|
2356
|
-
summaryText: optional(string)
|
|
2357
|
-
}));
|
|
2358
|
-
|
|
2359
|
-
const parsePassageWidget = parseWidget(constant("passage"), object({
|
|
2360
|
-
footnotes: defaulted(string, () => ""),
|
|
2361
|
-
passageText: string,
|
|
2362
|
-
passageTitle: defaulted(string, () => ""),
|
|
2363
|
-
showLineNumbers: boolean,
|
|
2364
|
-
static: defaulted(boolean, () => false)
|
|
2365
|
-
}));
|
|
2366
|
-
|
|
2367
|
-
const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
|
|
2368
|
-
url: string,
|
|
2369
|
-
description: string
|
|
2370
|
-
}));
|
|
2371
|
-
|
|
2372
|
-
const parsePlotterWidget = parseWidget(constant("plotter"), object({
|
|
2373
|
-
labels: array(string),
|
|
2374
|
-
categories: array(string),
|
|
2375
|
-
type: enumeration(...plotterPlotTypes),
|
|
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.
|
|
2380
|
-
scaleY: defaulted(number, () => 1),
|
|
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.
|
|
2385
|
-
snapsPerLine: defaulted(number, () => 2),
|
|
2386
|
-
starting: array(number),
|
|
2387
|
-
correct: array(number),
|
|
2388
|
-
picUrl: optional(nullable(string)),
|
|
2389
|
-
picSize: optional(nullable(number)),
|
|
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.
|
|
2394
|
-
plotDimensions: defaulted(array(number), () => [380, 300])
|
|
2395
|
-
}));
|
|
2396
|
-
|
|
2397
|
-
const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
|
|
2398
|
-
programID: string,
|
|
2399
|
-
height: number
|
|
2400
|
-
}));
|
|
2401
|
-
|
|
2402
|
-
const currentVersion$3 = {
|
|
2403
|
-
major: 2,
|
|
2404
|
-
minor: 0
|
|
2405
|
-
};
|
|
2406
|
-
function deriveNumCorrect(options) {
|
|
2407
|
-
const {
|
|
2408
|
-
choices,
|
|
2409
|
-
numCorrect
|
|
2410
|
-
} = options;
|
|
2411
|
-
return numCorrect ?? choices.filter(c => c.correct).length;
|
|
2412
|
-
}
|
|
2413
|
-
const widgetOptionsUpgrades$2 = {
|
|
2414
|
-
"2": v1props => {
|
|
2415
|
-
const upgraded = {
|
|
2416
|
-
...v1props,
|
|
2417
|
-
numCorrect: deriveNumCorrect(v1props)
|
|
2418
|
-
};
|
|
2419
|
-
return upgraded;
|
|
2420
|
-
},
|
|
2421
|
-
"1": v0props => {
|
|
2422
|
-
const {
|
|
2423
|
-
noneOfTheAbove,
|
|
2424
|
-
...rest
|
|
2425
|
-
} = v0props;
|
|
2426
|
-
if (noneOfTheAbove) {
|
|
2427
|
-
throw new Error("radio widget v0 no longer supports auto noneOfTheAbove");
|
|
2428
|
-
}
|
|
2429
|
-
return {
|
|
2430
|
-
...rest,
|
|
2431
|
-
hasNoneOfTheAbove: false
|
|
2432
|
-
};
|
|
2433
|
-
}
|
|
2434
|
-
};
|
|
2435
|
-
const defaultWidgetOptions$v = {
|
|
2436
|
-
choices: [{}, {}, {}, {}],
|
|
2437
|
-
displayCount: null,
|
|
2438
|
-
randomize: false,
|
|
2439
|
-
hasNoneOfTheAbove: false,
|
|
2440
|
-
multipleSelect: false,
|
|
2441
|
-
countChoices: false,
|
|
2442
|
-
deselectEnabled: false
|
|
2443
|
-
};
|
|
2444
|
-
|
|
2445
|
-
const version2 = optional(object({
|
|
2446
|
-
major: constant(2),
|
|
2447
|
-
minor: number
|
|
2448
|
-
}));
|
|
2449
|
-
const parseRadioWidgetV2 = parseWidgetWithVersion(version2, constant("radio"), object({
|
|
2450
|
-
numCorrect: optional(number),
|
|
2451
|
-
choices: array(object({
|
|
2452
|
-
content: defaulted(string, () => ""),
|
|
2453
|
-
clue: optional(string),
|
|
2454
|
-
correct: optional(boolean),
|
|
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.
|
|
2460
|
-
widgets: optional((rawVal, ctx) => parseWidgetsMap(rawVal, ctx))
|
|
2461
|
-
})),
|
|
2462
|
-
hasNoneOfTheAbove: optional(boolean),
|
|
2463
|
-
countChoices: optional(boolean),
|
|
2464
|
-
randomize: optional(boolean),
|
|
2465
|
-
multipleSelect: optional(boolean),
|
|
2466
|
-
deselectEnabled: optional(boolean),
|
|
2467
|
-
// deprecated
|
|
2468
|
-
onePerLine: optional(boolean),
|
|
2469
|
-
// deprecated
|
|
2470
|
-
displayCount: optional(any),
|
|
2471
|
-
// v0 props
|
|
2472
|
-
// `noneOfTheAbove` is still in use (but only set to `false`).
|
|
2473
|
-
noneOfTheAbove: optional(constant(false))
|
|
2474
|
-
}));
|
|
2475
|
-
const version1 = optional(object({
|
|
2476
|
-
major: constant(1),
|
|
2477
|
-
minor: number
|
|
2478
|
-
}));
|
|
2479
|
-
const parseRadioWidgetV1 = parseWidgetWithVersion(version1, constant("radio"), object({
|
|
2480
|
-
choices: array(object({
|
|
2481
|
-
content: defaulted(string, () => ""),
|
|
2482
|
-
clue: optional(string),
|
|
2483
|
-
correct: optional(boolean),
|
|
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.
|
|
2489
|
-
widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => undefined)
|
|
2490
|
-
})),
|
|
2491
|
-
hasNoneOfTheAbove: optional(boolean),
|
|
2492
|
-
countChoices: optional(boolean),
|
|
2493
|
-
randomize: optional(boolean),
|
|
2494
|
-
multipleSelect: optional(boolean),
|
|
2495
|
-
deselectEnabled: optional(boolean),
|
|
2496
|
-
// deprecated
|
|
2497
|
-
onePerLine: optional(boolean),
|
|
2498
|
-
// deprecated
|
|
2499
|
-
displayCount: optional(any),
|
|
2500
|
-
// v0 props
|
|
2501
|
-
// `noneOfTheAbove` is still in use (but only set to `false`).
|
|
2502
|
-
noneOfTheAbove: optional(constant(false))
|
|
2503
|
-
}));
|
|
2504
|
-
function migrateV1ToV2(widget) {
|
|
2505
|
-
const {
|
|
2506
|
-
options
|
|
2507
|
-
} = widget;
|
|
2508
|
-
return {
|
|
2509
|
-
...widget,
|
|
2510
|
-
version: {
|
|
2511
|
-
major: 2,
|
|
2512
|
-
minor: 0
|
|
2513
|
-
},
|
|
2514
|
-
options: {
|
|
2515
|
-
...options,
|
|
2516
|
-
numCorrect: deriveNumCorrect(options)
|
|
2517
|
-
}
|
|
2518
|
-
};
|
|
2519
|
-
}
|
|
2520
|
-
const version0 = optional(object({
|
|
2521
|
-
major: constant(0),
|
|
2522
|
-
minor: number
|
|
2523
|
-
}));
|
|
2524
|
-
const parseRadioWidgetV0 = parseWidgetWithVersion(version0, constant("radio"), object({
|
|
2525
|
-
choices: array(object({
|
|
2526
|
-
content: defaulted(string, () => ""),
|
|
2527
|
-
clue: optional(string),
|
|
2528
|
-
correct: optional(boolean),
|
|
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.
|
|
2534
|
-
widgets: optional((rawVal, ctx) => parseWidgetsMap(rawVal, ctx))
|
|
2535
|
-
})),
|
|
2536
|
-
hasNoneOfTheAbove: optional(boolean),
|
|
2537
|
-
countChoices: optional(boolean),
|
|
2538
|
-
randomize: optional(boolean),
|
|
2539
|
-
multipleSelect: optional(boolean),
|
|
2540
|
-
deselectEnabled: optional(boolean),
|
|
2541
|
-
// deprecated
|
|
2542
|
-
onePerLine: optional(boolean),
|
|
2543
|
-
// deprecated
|
|
2544
|
-
displayCount: optional(any),
|
|
2545
|
-
// v0 props
|
|
2546
|
-
// `noneOfTheAbove` is still in use (but only set to `false`).
|
|
2547
|
-
noneOfTheAbove: optional(constant(false))
|
|
2548
|
-
}));
|
|
2549
|
-
function migrateV0ToV1(widget) {
|
|
2550
|
-
const {
|
|
2551
|
-
options
|
|
2552
|
-
} = widget;
|
|
2553
|
-
const {
|
|
2554
|
-
noneOfTheAbove: _,
|
|
2555
|
-
...rest
|
|
2556
|
-
} = options;
|
|
2557
|
-
return {
|
|
2558
|
-
...widget,
|
|
2559
|
-
version: {
|
|
2560
|
-
major: 1,
|
|
2561
|
-
minor: 0
|
|
2562
|
-
},
|
|
2563
|
-
options: {
|
|
2564
|
-
...rest,
|
|
2565
|
-
hasNoneOfTheAbove: false
|
|
2566
|
-
}
|
|
2567
|
-
};
|
|
2568
|
-
}
|
|
2569
|
-
const parseRadioWidget = versionedWidgetOptions(2, parseRadioWidgetV2).withMigrationFrom(1, parseRadioWidgetV1, migrateV1ToV2).withMigrationFrom(0, parseRadioWidgetV0, migrateV0ToV1).parser;
|
|
2570
|
-
|
|
2571
|
-
const parseSorterWidget = parseWidget(constant("sorter"), object({
|
|
2572
|
-
correct: array(string),
|
|
2573
|
-
padding: boolean,
|
|
2574
|
-
layout: enumeration("horizontal", "vertical")
|
|
2575
|
-
}));
|
|
2576
|
-
|
|
2577
|
-
const parseTableWidget = parseWidget(constant("table"), object({
|
|
2578
|
-
headers: array(string),
|
|
2579
|
-
rows: number,
|
|
2580
|
-
columns: number,
|
|
2581
|
-
answers: array(array(string))
|
|
2582
|
-
}));
|
|
2583
|
-
|
|
2584
|
-
const parseVideoWidget = parseWidget(constant("video"), object({
|
|
2585
|
-
location: string,
|
|
2586
|
-
static: optional(boolean)
|
|
2587
|
-
}));
|
|
2588
|
-
|
|
2589
|
-
const parseWidgetsMap = (rawValue, ctx) => {
|
|
2590
|
-
if (!isObject(rawValue)) {
|
|
2591
|
-
return ctx.failure("PerseusWidgetsMap", rawValue);
|
|
2592
|
-
}
|
|
2593
|
-
const widgetsMap = {};
|
|
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.
|
|
2598
|
-
const entryResult = parseWidgetsMapEntry([key, rawValue[key]], widgetsMap, ctx.forSubtree(key));
|
|
2599
|
-
if (isFailure(entryResult)) {
|
|
2600
|
-
return entryResult;
|
|
2601
|
-
}
|
|
2602
|
-
}
|
|
2603
|
-
return ctx.success(widgetsMap);
|
|
2604
|
-
};
|
|
2605
|
-
const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
|
|
2606
|
-
let [id, widget] = _ref;
|
|
2607
|
-
const idComponentsResult = parseWidgetIdComponents(id.split(" "), ctx.forSubtree("(widget ID)"));
|
|
2608
|
-
if (isFailure(idComponentsResult)) {
|
|
2609
|
-
return idComponentsResult;
|
|
2610
|
-
}
|
|
2611
|
-
const [type, n] = idComponentsResult.value;
|
|
2612
|
-
function parseAndAssign(key, parse) {
|
|
2613
|
-
const widgetResult = parse(widget, ctx);
|
|
2614
|
-
if (isFailure(widgetResult)) {
|
|
2615
|
-
return widgetResult;
|
|
2616
|
-
}
|
|
2617
|
-
widgetMap[key] = widgetResult.value;
|
|
2618
|
-
return ctx.success(undefined);
|
|
2619
|
-
}
|
|
2620
|
-
switch (type) {
|
|
2621
|
-
case "categorizer":
|
|
2622
|
-
return parseAndAssign(`categorizer ${n}`, parseCategorizerWidget);
|
|
2623
|
-
case "cs-program":
|
|
2624
|
-
return parseAndAssign(`cs-program ${n}`, parseCSProgramWidget);
|
|
2625
|
-
case "definition":
|
|
2626
|
-
return parseAndAssign(`definition ${n}`, parseDefinitionWidget);
|
|
2627
|
-
case "dropdown":
|
|
2628
|
-
return parseAndAssign(`dropdown ${n}`, parseDropdownWidget);
|
|
2629
|
-
case "explanation":
|
|
2630
|
-
return parseAndAssign(`explanation ${n}`, parseExplanationWidget);
|
|
2631
|
-
case "expression":
|
|
2632
|
-
return parseAndAssign(`expression ${n}`, parseExpressionWidget);
|
|
2633
|
-
case "grapher":
|
|
2634
|
-
return parseAndAssign(`grapher ${n}`, parseGrapherWidget);
|
|
2635
|
-
case "group":
|
|
2636
|
-
return parseAndAssign(`group ${n}`, parseGroupWidget);
|
|
2637
|
-
case "graded-group":
|
|
2638
|
-
return parseAndAssign(`graded-group ${n}`, parseGradedGroupWidget);
|
|
2639
|
-
case "graded-group-set":
|
|
2640
|
-
return parseAndAssign(`graded-group-set ${n}`, parseGradedGroupSetWidget);
|
|
2641
|
-
case "iframe":
|
|
2642
|
-
return parseAndAssign(`iframe ${n}`, parseIframeWidget);
|
|
2643
|
-
case "image":
|
|
2644
|
-
return parseAndAssign(`image ${n}`, parseImageWidget);
|
|
2645
|
-
case "input-number":
|
|
2646
|
-
return parseAndAssign(`input-number ${n}`, parseInputNumberWidget);
|
|
2647
|
-
case "interaction":
|
|
2648
|
-
return parseAndAssign(`interaction ${n}`, parseInteractionWidget);
|
|
2649
|
-
case "interactive-graph":
|
|
2650
|
-
return parseAndAssign(`interactive-graph ${n}`, parseInteractiveGraphWidget);
|
|
2651
|
-
case "label-image":
|
|
2652
|
-
return parseAndAssign(`label-image ${n}`, parseLabelImageWidget);
|
|
2653
|
-
case "matcher":
|
|
2654
|
-
return parseAndAssign(`matcher ${n}`, parseMatcherWidget);
|
|
2655
|
-
case "matrix":
|
|
2656
|
-
return parseAndAssign(`matrix ${n}`, parseMatrixWidget);
|
|
2657
|
-
case "measurer":
|
|
2658
|
-
return parseAndAssign(`measurer ${n}`, parseMeasurerWidget);
|
|
2659
|
-
case "molecule-renderer":
|
|
2660
|
-
return parseAndAssign(`molecule-renderer ${n}`, parseMoleculeRendererWidget);
|
|
2661
|
-
case "number-line":
|
|
2662
|
-
return parseAndAssign(`number-line ${n}`, parseNumberLineWidget);
|
|
2663
|
-
case "numeric-input":
|
|
2664
|
-
return parseAndAssign(`numeric-input ${n}`, parseNumericInputWidget);
|
|
2665
|
-
case "orderer":
|
|
2666
|
-
return parseAndAssign(`orderer ${n}`, parseOrdererWidget);
|
|
2667
|
-
case "passage":
|
|
2668
|
-
return parseAndAssign(`passage ${n}`, parsePassageWidget);
|
|
2669
|
-
case "passage-ref":
|
|
2670
|
-
return parseAndAssign(`passage-ref ${n}`, parsePassageRefWidget);
|
|
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
|
|
2675
|
-
return parseAndAssign(`passage-ref-target ${n}`, any);
|
|
2676
|
-
case "phet-simulation":
|
|
2677
|
-
return parseAndAssign(`phet-simulation ${n}`, parsePhetSimulationWidget);
|
|
2678
|
-
case "plotter":
|
|
2679
|
-
return parseAndAssign(`plotter ${n}`, parsePlotterWidget);
|
|
2680
|
-
case "python-program":
|
|
2681
|
-
return parseAndAssign(`python-program ${n}`, parsePythonProgramWidget);
|
|
2682
|
-
case "radio":
|
|
2683
|
-
return parseAndAssign(`radio ${n}`, parseRadioWidget);
|
|
2684
|
-
case "sorter":
|
|
2685
|
-
return parseAndAssign(`sorter ${n}`, parseSorterWidget);
|
|
2686
|
-
case "table":
|
|
2687
|
-
return parseAndAssign(`table ${n}`, parseTableWidget);
|
|
2688
|
-
case "video":
|
|
2689
|
-
return parseAndAssign(`video ${n}`, parseVideoWidget);
|
|
2690
|
-
case "sequence":
|
|
2691
|
-
// sequence is a deprecated widget type, and the corresponding
|
|
2692
|
-
// widget component no longer exists.
|
|
2693
|
-
return parseAndAssign(`sequence ${n}`, parseDeprecatedWidget);
|
|
2694
|
-
case "lights-puzzle":
|
|
2695
|
-
return parseAndAssign(`lights-puzzle ${n}`, parseDeprecatedWidget);
|
|
2696
|
-
case "simulator":
|
|
2697
|
-
return parseAndAssign(`simulator ${n}`, parseDeprecatedWidget);
|
|
2698
|
-
case "transformer":
|
|
2699
|
-
return parseAndAssign(`transformer ${n}`, parseDeprecatedWidget);
|
|
2700
|
-
default:
|
|
2701
|
-
return parseAndAssign(`${type} ${n}`, parseWidget(constant(type), any));
|
|
2702
|
-
}
|
|
2703
|
-
};
|
|
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({}));
|
|
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.
|
|
2714
|
-
if (typeof rawValue !== "string" || !/^(0|[1-9][0-9]*)$/.test(rawValue)) {
|
|
2715
|
-
return ctx.failure("a string representing a non-negative integer", rawValue);
|
|
2716
|
-
}
|
|
2717
|
-
return ctx.success(+rawValue);
|
|
2718
|
-
};
|
|
2719
|
-
const parseWidgetIdComponents = pair(string, parseStringToNonNegativeInt);
|
|
2720
|
-
|
|
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.
|
|
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.
|
|
2729
|
-
widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
|
|
2730
|
-
images: parseImages,
|
|
2731
|
-
// deprecated
|
|
2732
|
-
metadata: any
|
|
2733
|
-
}),
|
|
2734
|
-
// Default value
|
|
2735
|
-
() => ({
|
|
2736
|
-
content: "",
|
|
2737
|
-
widgets: {},
|
|
2738
|
-
images: {}
|
|
2739
|
-
}));
|
|
2740
|
-
|
|
2741
|
-
const parsePerseusArticle = union(parsePerseusRenderer).or(array(parsePerseusRenderer)).parser;
|
|
2742
|
-
|
|
2743
|
-
const parseHint = object({
|
|
2744
|
-
replace: defaulted(boolean, () => undefined),
|
|
2745
|
-
content: string,
|
|
2746
|
-
widgets: defaulted(parseWidgetsMap, () => ({})),
|
|
2747
|
-
images: parseImages,
|
|
2748
|
-
// deprecated
|
|
2749
|
-
metadata: any
|
|
2750
|
-
});
|
|
167
|
+
const parsePassageWidget=parseWidget(constant("passage"),object({footnotes:defaulted(string,()=>""),passageText:string,passageTitle:defaulted(string,()=>""),showLineNumbers:boolean,static:defaulted(boolean,()=>false)}));
|
|
2751
168
|
|
|
2752
|
-
const
|
|
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`.
|
|
2767
|
-
function toAnswerArea(raw) {
|
|
2768
|
-
return {
|
|
2769
|
-
zTable: !!raw.zTable,
|
|
2770
|
-
calculator: !!raw.calculator,
|
|
2771
|
-
chi2Table: !!raw.chi2Table,
|
|
2772
|
-
financialCalculatorMonthlyPayment: !!raw.financialCalculatorMonthlyPayment,
|
|
2773
|
-
financialCalculatorTotalAmount: !!raw.financialCalculatorTotalAmount,
|
|
2774
|
-
financialCalculatorTimeToPayOff: !!raw.financialCalculatorTimeToPayOff,
|
|
2775
|
-
periodicTable: !!raw.periodicTable,
|
|
2776
|
-
periodicTableWithKey: !!raw.periodicTableWithKey,
|
|
2777
|
-
tTable: !!raw.tTable
|
|
2778
|
-
};
|
|
2779
|
-
}
|
|
169
|
+
const parsePhetSimulationWidget=parseWidget(constant("phet-simulation"),object({url:string,description:string}));
|
|
2780
170
|
|
|
2781
|
-
const
|
|
2782
|
-
question: parsePerseusRenderer,
|
|
2783
|
-
hints: defaulted(array(parseHint), () => []),
|
|
2784
|
-
answerArea: parsePerseusAnswerArea,
|
|
2785
|
-
itemDataVersion: optional(nullable(object({
|
|
2786
|
-
major: number,
|
|
2787
|
-
minor: number
|
|
2788
|
-
}))),
|
|
2789
|
-
// Deprecated field
|
|
2790
|
-
answer: any
|
|
2791
|
-
});
|
|
171
|
+
const parsePlotterWidget=parseWidget(constant("plotter"),object({labels:array(string),categories:array(string),type:enumeration(...plotterPlotTypes),maxY:number,scaleY:defaulted(number,()=>1),labelInterval:optional(nullable(number)),snapsPerLine:defaulted(number,()=>2),starting:array(number),correct:array(number),picUrl:optional(nullable(string)),picSize:optional(nullable(number)),picBoxHeight:optional(nullable(number)),plotDimensions:defaulted(array(number),()=>[380,300])}));
|
|
2792
172
|
|
|
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
|
-
*/
|
|
2802
|
-
function parsePerseusItem(json) {
|
|
2803
|
-
// Try to block a cheating vector which relies on monkey-patching
|
|
2804
|
-
// JSON.parse
|
|
2805
|
-
if (isRealJSONParse(JSON.parse)) {
|
|
2806
|
-
return JSON.parse(json);
|
|
2807
|
-
}
|
|
2808
|
-
throw new Error("Something went wrong.");
|
|
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
|
-
*/
|
|
2820
|
-
function parseAndMigratePerseusItem(json) {
|
|
2821
|
-
throwErrorIfCheatingDetected();
|
|
2822
|
-
const object = JSON.parse(json);
|
|
2823
|
-
const result = parse(object, parsePerseusItem$1);
|
|
2824
|
-
if (isFailure(result)) {
|
|
2825
|
-
return failure({
|
|
2826
|
-
message: result.detail,
|
|
2827
|
-
invalidObject: object
|
|
2828
|
-
});
|
|
2829
|
-
}
|
|
2830
|
-
return result;
|
|
2831
|
-
}
|
|
173
|
+
const parsePythonProgramWidget=parseWidget(constant("python-program"),object({programID:string,height:number}));
|
|
2832
174
|
|
|
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
|
-
*/
|
|
2842
|
-
function parseAndMigratePerseusArticle(json) {
|
|
2843
|
-
throwErrorIfCheatingDetected();
|
|
2844
|
-
const object = JSON.parse(json);
|
|
2845
|
-
const result = parse(object, parsePerseusArticle);
|
|
2846
|
-
if (isFailure(result)) {
|
|
2847
|
-
return failure({
|
|
2848
|
-
message: result.detail,
|
|
2849
|
-
invalidObject: object
|
|
2850
|
-
});
|
|
2851
|
-
}
|
|
2852
|
-
return result;
|
|
2853
|
-
}
|
|
175
|
+
const currentVersion$3={major:2,minor:0};function deriveNumCorrect(options){const{choices,numCorrect}=options;return numCorrect??choices.filter(c=>c.correct).length}const widgetOptionsUpgrades$2={"2":v1props=>{const upgraded={...v1props,numCorrect:deriveNumCorrect(v1props)};return upgraded},"1":v0props=>{const{noneOfTheAbove,...rest}=v0props;if(noneOfTheAbove){throw new Error("radio widget v0 no longer supports auto noneOfTheAbove")}return {...rest,hasNoneOfTheAbove:false}}};const defaultWidgetOptions$v={choices:[{},{},{},{}],displayCount:null,randomize:false,hasNoneOfTheAbove:false,multipleSelect:false,countChoices:false,deselectEnabled:false};
|
|
2854
176
|
|
|
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.
|
|
2859
|
-
function throwErrorIfCheatingDetected() {
|
|
2860
|
-
if (!isRealJSONParse(JSON.parse)) {
|
|
2861
|
-
throw new Error("Something went wrong.");
|
|
2862
|
-
}
|
|
2863
|
-
}
|
|
177
|
+
const version2=optional(object({major:constant(2),minor:number}));const parseRadioWidgetV2=parseWidgetWithVersion(version2,constant("radio"),object({numCorrect:optional(number),choices:array(object({content:defaulted(string,()=>""),clue:optional(string),correct:optional(boolean),isNoneOfTheAbove:optional(boolean),widgets:optional((rawVal,ctx)=>parseWidgetsMap(rawVal,ctx))})),hasNoneOfTheAbove:optional(boolean),countChoices:optional(boolean),randomize:optional(boolean),multipleSelect:optional(boolean),deselectEnabled:optional(boolean),onePerLine:optional(boolean),displayCount:optional(any),noneOfTheAbove:optional(constant(false))}));const version1=optional(object({major:constant(1),minor:number}));const parseRadioWidgetV1=parseWidgetWithVersion(version1,constant("radio"),object({choices:array(object({content:defaulted(string,()=>""),clue:optional(string),correct:optional(boolean),isNoneOfTheAbove:optional(boolean),widgets:defaulted((rawVal,ctx)=>parseWidgetsMap(rawVal,ctx),()=>undefined)})),hasNoneOfTheAbove:optional(boolean),countChoices:optional(boolean),randomize:optional(boolean),multipleSelect:optional(boolean),deselectEnabled:optional(boolean),onePerLine:optional(boolean),displayCount:optional(any),noneOfTheAbove:optional(constant(false))}));function migrateV1ToV2(widget){const{options}=widget;return {...widget,version:{major:2,minor:0},options:{...options,numCorrect:deriveNumCorrect(options)}}}const version0=optional(object({major:constant(0),minor:number}));const parseRadioWidgetV0=parseWidgetWithVersion(version0,constant("radio"),object({choices:array(object({content:defaulted(string,()=>""),clue:optional(string),correct:optional(boolean),isNoneOfTheAbove:optional(boolean),widgets:optional((rawVal,ctx)=>parseWidgetsMap(rawVal,ctx))})),hasNoneOfTheAbove:optional(boolean),countChoices:optional(boolean),randomize:optional(boolean),multipleSelect:optional(boolean),deselectEnabled:optional(boolean),onePerLine:optional(boolean),displayCount:optional(any),noneOfTheAbove:optional(constant(false))}));function migrateV0ToV1(widget){const{options}=widget;const{noneOfTheAbove:_,...rest}=options;return {...widget,version:{major:1,minor:0},options:{...rest,hasNoneOfTheAbove:false}}}const parseRadioWidget=versionedWidgetOptions(2,parseRadioWidgetV2).withMigrationFrom(1,parseRadioWidgetV1,migrateV1ToV2).withMigrationFrom(0,parseRadioWidgetV0,migrateV0ToV1).parser;
|
|
2864
178
|
|
|
2865
|
-
|
|
2866
|
-
// version number during the release build.
|
|
2867
|
-
// In dev, you'll never see the version number.
|
|
2868
|
-
|
|
2869
|
-
const libName = "@khanacademy/perseus-core";
|
|
2870
|
-
const libVersion = "7.0.0";
|
|
2871
|
-
perseusUtils.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
2872
|
-
|
|
2873
|
-
/**
|
|
2874
|
-
* @typedef {Object} Errors utility for referencing the Perseus error taxonomy.
|
|
2875
|
-
*/
|
|
2876
|
-
const Errors = Object.freeze({
|
|
2877
|
-
/**
|
|
2878
|
-
* @property {ErrorKind} Unknown The kind of error is not known.
|
|
2879
|
-
*/
|
|
2880
|
-
Unknown: "Unknown",
|
|
2881
|
-
/**
|
|
2882
|
-
* @property {ErrorKind} Internal The error is internal to the executing code.
|
|
2883
|
-
*/
|
|
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
|
-
*/
|
|
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
|
-
*/
|
|
2898
|
-
NotAllowed: "NotAllowed",
|
|
2899
|
-
/**
|
|
2900
|
-
* @property {ErrorKind} TransientService There was a problem when making a
|
|
2901
|
-
* request to a service.
|
|
2902
|
-
*/
|
|
2903
|
-
TransientService: "TransientService",
|
|
2904
|
-
/**
|
|
2905
|
-
* @property {ErrorKind} Service There was a non-transient problem when
|
|
2906
|
-
* making a request to service.
|
|
2907
|
-
*/
|
|
2908
|
-
Service: "Service"
|
|
2909
|
-
});
|
|
179
|
+
const parseSorterWidget=parseWidget(constant("sorter"),object({correct:array(string),padding:boolean,layout:enumeration("horizontal","vertical")}));
|
|
2910
180
|
|
|
2911
|
-
|
|
2912
|
-
* @type {ErrorKind} The kind of error being reported
|
|
2913
|
-
*/
|
|
2914
|
-
|
|
2915
|
-
class PerseusError extends Error {
|
|
2916
|
-
kind;
|
|
2917
|
-
metadata;
|
|
2918
|
-
constructor(message, kind, options) {
|
|
2919
|
-
super(message);
|
|
2920
|
-
this.kind = kind;
|
|
2921
|
-
this.metadata = options?.metadata;
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
181
|
+
const parseTableWidget=parseWidget(constant("table"),object({headers:array(string),rows:number,columns:number,answers:array(array(string))}));
|
|
2924
182
|
|
|
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) {
|
|
2949
|
-
return [key, value[subKey]];
|
|
2950
|
-
}));
|
|
2951
|
-
};
|
|
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) {
|
|
2962
|
-
const result = {};
|
|
2963
|
-
Object.keys(obj).forEach(key => {
|
|
2964
|
-
// @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
|
|
2965
|
-
result[key] = lambda(obj[key], key);
|
|
2966
|
-
});
|
|
2967
|
-
return result;
|
|
2968
|
-
};
|
|
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
|
-
*/
|
|
2979
|
-
function getCategorizerPublicWidgetOptions(options) {
|
|
2980
|
-
return {
|
|
2981
|
-
items: options.items,
|
|
2982
|
-
categories: options.categories,
|
|
2983
|
-
randomizeItems: options.randomizeItems,
|
|
2984
|
-
static: options.static
|
|
2985
|
-
};
|
|
2986
|
-
}
|
|
183
|
+
const parseVideoWidget=parseWidget(constant("video"),object({location:string,static:optional(boolean)}));
|
|
2987
184
|
|
|
2988
|
-
const
|
|
2989
|
-
items: [],
|
|
2990
|
-
categories: [],
|
|
2991
|
-
values: [],
|
|
2992
|
-
randomizeItems: false
|
|
2993
|
-
};
|
|
2994
|
-
const categorizerWidgetLogic = {
|
|
2995
|
-
name: "categorizer",
|
|
2996
|
-
defaultWidgetOptions: defaultWidgetOptions$u,
|
|
2997
|
-
getPublicWidgetOptions: getCategorizerPublicWidgetOptions
|
|
2998
|
-
};
|
|
2999
|
-
|
|
3000
|
-
function getCSProgramPublicWidgetOptions(options) {
|
|
3001
|
-
return options;
|
|
3002
|
-
}
|
|
185
|
+
const parseWidgetsMap=(rawValue,ctx)=>{if(!isObject(rawValue)){return ctx.failure("PerseusWidgetsMap",rawValue)}const widgetsMap={};for(const key of Object.keys(rawValue)){const entryResult=parseWidgetsMapEntry([key,rawValue[key]],widgetsMap,ctx.forSubtree(key));if(isFailure(entryResult)){return entryResult}}return ctx.success(widgetsMap)};const parseWidgetsMapEntry=([id,widget],widgetMap,ctx)=>{const idComponentsResult=parseWidgetIdComponents(id.split(" "),ctx.forSubtree("(widget ID)"));if(isFailure(idComponentsResult)){return idComponentsResult}const[type,n]=idComponentsResult.value;function parseAndAssign(key,parse){const widgetResult=parse(widget,ctx);if(isFailure(widgetResult)){return widgetResult}widgetMap[key]=widgetResult.value;return ctx.success(undefined)}switch(type){case "categorizer":return parseAndAssign(`categorizer ${n}`,parseCategorizerWidget);case "cs-program":return parseAndAssign(`cs-program ${n}`,parseCSProgramWidget);case "definition":return parseAndAssign(`definition ${n}`,parseDefinitionWidget);case "dropdown":return parseAndAssign(`dropdown ${n}`,parseDropdownWidget);case "explanation":return parseAndAssign(`explanation ${n}`,parseExplanationWidget);case "expression":return parseAndAssign(`expression ${n}`,parseExpressionWidget);case "grapher":return parseAndAssign(`grapher ${n}`,parseGrapherWidget);case "group":return parseAndAssign(`group ${n}`,parseGroupWidget);case "graded-group":return parseAndAssign(`graded-group ${n}`,parseGradedGroupWidget);case "graded-group-set":return parseAndAssign(`graded-group-set ${n}`,parseGradedGroupSetWidget);case "iframe":return parseAndAssign(`iframe ${n}`,parseIframeWidget);case "image":return parseAndAssign(`image ${n}`,parseImageWidget);case "input-number":return parseAndAssign(`input-number ${n}`,parseInputNumberWidget);case "interaction":return parseAndAssign(`interaction ${n}`,parseInteractionWidget);case "interactive-graph":return parseAndAssign(`interactive-graph ${n}`,parseInteractiveGraphWidget);case "label-image":return parseAndAssign(`label-image ${n}`,parseLabelImageWidget);case "matcher":return parseAndAssign(`matcher ${n}`,parseMatcherWidget);case "matrix":return parseAndAssign(`matrix ${n}`,parseMatrixWidget);case "measurer":return parseAndAssign(`measurer ${n}`,parseMeasurerWidget);case "molecule-renderer":return parseAndAssign(`molecule-renderer ${n}`,parseMoleculeRendererWidget);case "number-line":return parseAndAssign(`number-line ${n}`,parseNumberLineWidget);case "numeric-input":return parseAndAssign(`numeric-input ${n}`,parseNumericInputWidget);case "orderer":return parseAndAssign(`orderer ${n}`,parseOrdererWidget);case "passage":return parseAndAssign(`passage ${n}`,parsePassageWidget);case "passage-ref":return parseAndAssign(`passage-ref ${n}`,parsePassageRefWidget);case "passage-ref-target":return parseAndAssign(`passage-ref-target ${n}`,any);case "phet-simulation":return parseAndAssign(`phet-simulation ${n}`,parsePhetSimulationWidget);case "plotter":return parseAndAssign(`plotter ${n}`,parsePlotterWidget);case "python-program":return parseAndAssign(`python-program ${n}`,parsePythonProgramWidget);case "radio":return parseAndAssign(`radio ${n}`,parseRadioWidget);case "sorter":return parseAndAssign(`sorter ${n}`,parseSorterWidget);case "table":return parseAndAssign(`table ${n}`,parseTableWidget);case "video":return parseAndAssign(`video ${n}`,parseVideoWidget);case "sequence":return parseAndAssign(`sequence ${n}`,parseDeprecatedWidget);case "lights-puzzle":return parseAndAssign(`lights-puzzle ${n}`,parseDeprecatedWidget);case "simulator":return parseAndAssign(`simulator ${n}`,parseDeprecatedWidget);case "transformer":return parseAndAssign(`transformer ${n}`,parseDeprecatedWidget);default:return parseAndAssign(`${type} ${n}`,parseWidget(constant(type),any))}};const parseDeprecatedWidget=parseWidget((_,ctx)=>ctx.success("deprecated-standin"),object({}));const parseStringToNonNegativeInt=(rawValue,ctx)=>{if(typeof rawValue!=="string"||!/^(0|[1-9][0-9]*)$/.test(rawValue)){return ctx.failure("a string representing a non-negative integer",rawValue)}return ctx.success(+rawValue)};const parseWidgetIdComponents=pair(string,parseStringToNonNegativeInt);
|
|
3003
186
|
|
|
3004
|
-
const
|
|
3005
|
-
const defaultWidgetOptions$t = {
|
|
3006
|
-
programID: "",
|
|
3007
|
-
programType: null,
|
|
3008
|
-
settings: [{
|
|
3009
|
-
name: "",
|
|
3010
|
-
value: ""
|
|
3011
|
-
}],
|
|
3012
|
-
showEditor: false,
|
|
3013
|
-
showButtons: false,
|
|
3014
|
-
height: DEFAULT_HEIGHT
|
|
3015
|
-
};
|
|
3016
|
-
const csProgramWidgetLogic = {
|
|
3017
|
-
name: "cs-program",
|
|
3018
|
-
defaultWidgetOptions: defaultWidgetOptions$t,
|
|
3019
|
-
supportedAlignments: ["block", "full-width"],
|
|
3020
|
-
getPublicWidgetOptions: getCSProgramPublicWidgetOptions
|
|
3021
|
-
};
|
|
3022
|
-
|
|
3023
|
-
const defaultWidgetOptions$s = {
|
|
3024
|
-
togglePrompt: "",
|
|
3025
|
-
definition: ""
|
|
3026
|
-
};
|
|
3027
|
-
const definitionWidgetLogic = {
|
|
3028
|
-
name: "definition",
|
|
3029
|
-
defaultWidgetOptions: defaultWidgetOptions$s,
|
|
3030
|
-
defaultAlignment: "inline"
|
|
3031
|
-
};
|
|
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
|
-
*/
|
|
3042
|
-
function getDropdownPublicWidgetOptions(options) {
|
|
3043
|
-
return {
|
|
3044
|
-
choices: options.choices.map(choice => ({
|
|
3045
|
-
content: choice.content
|
|
3046
|
-
})),
|
|
3047
|
-
placeholder: options.placeholder,
|
|
3048
|
-
static: options.static,
|
|
3049
|
-
visibleLabel: options.visibleLabel,
|
|
3050
|
-
ariaLabel: options.ariaLabel
|
|
3051
|
-
};
|
|
3052
|
-
}
|
|
187
|
+
const parsePerseusRenderer=defaulted(object({content:defaulted(string,()=>""),widgets:defaulted((rawVal,ctx)=>parseWidgetsMap(rawVal,ctx),()=>({})),images:parseImages,metadata:any}),()=>({content:"",widgets:{},images:{}}));
|
|
3053
188
|
|
|
3054
|
-
const
|
|
3055
|
-
placeholder: "",
|
|
3056
|
-
choices: [{
|
|
3057
|
-
content: "",
|
|
3058
|
-
correct: false
|
|
3059
|
-
}]
|
|
3060
|
-
};
|
|
3061
|
-
const dropdownWidgetLogic = {
|
|
3062
|
-
name: "definition",
|
|
3063
|
-
defaultWidgetOptions: defaultWidgetOptions$r,
|
|
3064
|
-
defaultAlignment: "inline-block",
|
|
3065
|
-
getPublicWidgetOptions: getDropdownPublicWidgetOptions
|
|
3066
|
-
};
|
|
3067
|
-
|
|
3068
|
-
const defaultWidgetOptions$q = {
|
|
3069
|
-
showPrompt: "Explain",
|
|
3070
|
-
hidePrompt: "Hide explanation",
|
|
3071
|
-
explanation: "explanation goes here\n\nmore explanation",
|
|
3072
|
-
widgets: {}
|
|
3073
|
-
};
|
|
3074
|
-
const explanationWidgetLogic = {
|
|
3075
|
-
name: "explanation",
|
|
3076
|
-
defaultWidgetOptions: defaultWidgetOptions$q,
|
|
3077
|
-
defaultAlignment: "inline"
|
|
3078
|
-
};
|
|
3079
|
-
|
|
3080
|
-
const currentVersion$2 = {
|
|
3081
|
-
major: 2,
|
|
3082
|
-
minor: 0
|
|
3083
|
-
};
|
|
3084
|
-
const widgetOptionsUpgrades$1 = {
|
|
3085
|
-
"2": v1options => {
|
|
3086
|
-
return {
|
|
3087
|
-
times: v1options.times,
|
|
3088
|
-
buttonSets: v1options.buttonSets,
|
|
3089
|
-
functions: v1options.functions,
|
|
3090
|
-
buttonsVisible: v1options.buttonsVisible,
|
|
3091
|
-
visibleLabel: v1options.visibleLabel,
|
|
3092
|
-
ariaLabel: v1options.ariaLabel,
|
|
3093
|
-
answerForms: v1options.answerForms,
|
|
3094
|
-
extraKeys: v1options.extraKeys || deriveExtraKeys(v1options)
|
|
3095
|
-
};
|
|
3096
|
-
},
|
|
3097
|
-
"1": v0options => {
|
|
3098
|
-
return {
|
|
3099
|
-
times: v0options.times,
|
|
3100
|
-
buttonSets: v0options.buttonSets,
|
|
3101
|
-
functions: v0options.functions,
|
|
3102
|
-
buttonsVisible: v0options.buttonsVisible,
|
|
3103
|
-
visibleLabel: v0options.visibleLabel,
|
|
3104
|
-
ariaLabel: v0options.ariaLabel,
|
|
3105
|
-
extraKeys: v0options.extraKeys,
|
|
3106
|
-
answerForms: [{
|
|
3107
|
-
considered: "correct",
|
|
3108
|
-
form: v0options.form,
|
|
3109
|
-
simplify: v0options.simplify,
|
|
3110
|
-
value: v0options.value
|
|
3111
|
-
}]
|
|
3112
|
-
};
|
|
3113
|
-
}
|
|
3114
|
-
};
|
|
3115
|
-
const defaultWidgetOptions$p = {
|
|
3116
|
-
answerForms: [],
|
|
3117
|
-
times: false,
|
|
3118
|
-
buttonSets: ["basic"],
|
|
3119
|
-
functions: ["f", "g", "h"]
|
|
3120
|
-
};
|
|
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
|
-
*/
|
|
3131
|
-
function getExpressionPublicWidgetOptions(options) {
|
|
3132
|
-
return {
|
|
3133
|
-
buttonSets: options.buttonSets,
|
|
3134
|
-
functions: options.functions,
|
|
3135
|
-
times: options.times,
|
|
3136
|
-
visibleLabel: options.visibleLabel,
|
|
3137
|
-
ariaLabel: options.ariaLabel,
|
|
3138
|
-
buttonsVisible: options.buttonsVisible,
|
|
3139
|
-
extraKeys: options.extraKeys
|
|
3140
|
-
};
|
|
3141
|
-
}
|
|
189
|
+
const parsePerseusArticle=union(parsePerseusRenderer).or(array(parsePerseusRenderer)).parser;
|
|
3142
190
|
|
|
3143
|
-
const
|
|
3144
|
-
name: "expression",
|
|
3145
|
-
version: currentVersion$2,
|
|
3146
|
-
widgetOptionsUpgrades: widgetOptionsUpgrades$1,
|
|
3147
|
-
defaultWidgetOptions: defaultWidgetOptions$p,
|
|
3148
|
-
defaultAlignment: "inline-block",
|
|
3149
|
-
getPublicWidgetOptions: getExpressionPublicWidgetOptions
|
|
3150
|
-
};
|
|
3151
|
-
|
|
3152
|
-
const defaultWidgetOptions$o = {
|
|
3153
|
-
title: "",
|
|
3154
|
-
content: "",
|
|
3155
|
-
widgets: {},
|
|
3156
|
-
images: {},
|
|
3157
|
-
hint: null
|
|
3158
|
-
};
|
|
3159
|
-
const gradedGroupWidgetLogic = {
|
|
3160
|
-
name: "graded-group",
|
|
3161
|
-
defaultWidgetOptions: defaultWidgetOptions$o
|
|
3162
|
-
};
|
|
3163
|
-
|
|
3164
|
-
const defaultWidgetOptions$n = {
|
|
3165
|
-
gradedGroups: []
|
|
3166
|
-
};
|
|
3167
|
-
const gradedGroupSetWidgetLogic = {
|
|
3168
|
-
name: "graded-group-set",
|
|
3169
|
-
defaultWidgetOptions: defaultWidgetOptions$n
|
|
3170
|
-
};
|
|
3171
|
-
|
|
3172
|
-
function getGrapherPublicWidgetOptions(options) {
|
|
3173
|
-
const {
|
|
3174
|
-
correct: _,
|
|
3175
|
-
...publicOptions
|
|
3176
|
-
} = options;
|
|
3177
|
-
return publicOptions;
|
|
3178
|
-
}
|
|
191
|
+
const parseHint=object({replace:defaulted(boolean,()=>undefined),content:string,widgets:defaulted(parseWidgetsMap,()=>({})),images:parseImages,metadata:any});
|
|
3179
192
|
|
|
3180
|
-
const
|
|
3181
|
-
graph: {
|
|
3182
|
-
labels: ["x", "y"],
|
|
3183
|
-
range: [[-10, 10], [-10, 10]],
|
|
3184
|
-
step: [1, 1],
|
|
3185
|
-
backgroundImage: {
|
|
3186
|
-
url: null
|
|
3187
|
-
},
|
|
3188
|
-
markings: "graph",
|
|
3189
|
-
rulerLabel: "",
|
|
3190
|
-
rulerTicks: 10,
|
|
3191
|
-
valid: true,
|
|
3192
|
-
showTooltips: false
|
|
3193
|
-
},
|
|
3194
|
-
correct: {
|
|
3195
|
-
type: "linear",
|
|
3196
|
-
coords: null
|
|
3197
|
-
},
|
|
3198
|
-
availableTypes: ["linear"]
|
|
3199
|
-
};
|
|
3200
|
-
const grapherWidgetLogic = {
|
|
3201
|
-
name: "grapher",
|
|
3202
|
-
defaultWidgetOptions: defaultWidgetOptions$m,
|
|
3203
|
-
getPublicWidgetOptions: getGrapherPublicWidgetOptions
|
|
3204
|
-
};
|
|
3205
|
-
|
|
3206
|
-
const defaultWidgetOptions$l = {
|
|
3207
|
-
content: "",
|
|
3208
|
-
widgets: {},
|
|
3209
|
-
images: {}
|
|
3210
|
-
};
|
|
3211
|
-
const groupWidgetLogic = {
|
|
3212
|
-
name: "group",
|
|
3213
|
-
defaultWidgetOptions: defaultWidgetOptions$l
|
|
3214
|
-
};
|
|
3215
|
-
|
|
3216
|
-
function getIFramePublicWidgetOptions(options) {
|
|
3217
|
-
return options;
|
|
3218
|
-
}
|
|
193
|
+
const parsePerseusAnswerArea=pipeParsers(defaulted(object({}),()=>({}))).then(convert(toAnswerArea)).parser;function toAnswerArea(raw){return {zTable:!!raw.zTable,calculator:!!raw.calculator,chi2Table:!!raw.chi2Table,financialCalculatorMonthlyPayment:!!raw.financialCalculatorMonthlyPayment,financialCalculatorTotalAmount:!!raw.financialCalculatorTotalAmount,financialCalculatorTimeToPayOff:!!raw.financialCalculatorTimeToPayOff,periodicTable:!!raw.periodicTable,periodicTableWithKey:!!raw.periodicTableWithKey,tTable:!!raw.tTable}}
|
|
3219
194
|
|
|
3220
|
-
const
|
|
3221
|
-
url: "",
|
|
3222
|
-
settings: [{
|
|
3223
|
-
name: "",
|
|
3224
|
-
value: ""
|
|
3225
|
-
}],
|
|
3226
|
-
width: "400",
|
|
3227
|
-
height: "400",
|
|
3228
|
-
allowFullScreen: false,
|
|
3229
|
-
allowTopNavigation: false
|
|
3230
|
-
};
|
|
3231
|
-
const iframeWidgetLogic = {
|
|
3232
|
-
name: "iframe",
|
|
3233
|
-
defaultWidgetOptions: defaultWidgetOptions$k,
|
|
3234
|
-
getPublicWidgetOptions: getIFramePublicWidgetOptions
|
|
3235
|
-
};
|
|
3236
|
-
|
|
3237
|
-
const defaultWidgetOptions$j = {
|
|
3238
|
-
title: "",
|
|
3239
|
-
range: [[0, 10], [0, 10]],
|
|
3240
|
-
box: [400, 400],
|
|
3241
|
-
backgroundImage: {
|
|
3242
|
-
url: null,
|
|
3243
|
-
width: 0,
|
|
3244
|
-
height: 0
|
|
3245
|
-
},
|
|
3246
|
-
labels: [],
|
|
3247
|
-
alt: "",
|
|
3248
|
-
caption: ""
|
|
3249
|
-
};
|
|
3250
|
-
const imageWidgetLogic = {
|
|
3251
|
-
name: "image",
|
|
3252
|
-
defaultWidgetOptions: defaultWidgetOptions$j,
|
|
3253
|
-
supportedAlignments: ["block", "full-width"],
|
|
3254
|
-
defaultAlignment: "block"
|
|
3255
|
-
};
|
|
3256
|
-
|
|
3257
|
-
const defaultWidgetOptions$i = {
|
|
3258
|
-
value: 0,
|
|
3259
|
-
simplify: "required",
|
|
3260
|
-
size: "normal",
|
|
3261
|
-
inexact: false,
|
|
3262
|
-
maxError: 0.1,
|
|
3263
|
-
answerType: "number",
|
|
3264
|
-
rightAlign: false
|
|
3265
|
-
};
|
|
3266
|
-
const inputNumberWidgetLogic = {
|
|
3267
|
-
name: "input-number",
|
|
3268
|
-
defaultWidgetOptions: defaultWidgetOptions$i,
|
|
3269
|
-
defaultAlignment: "inline-block"
|
|
3270
|
-
};
|
|
3271
|
-
|
|
3272
|
-
const defaultWidgetOptions$h = {
|
|
3273
|
-
graph: {
|
|
3274
|
-
box: [400, 400],
|
|
3275
|
-
labels: ["x", "y"],
|
|
3276
|
-
range: [[-10, 10], [-10, 10]],
|
|
3277
|
-
tickStep: [1, 1],
|
|
3278
|
-
gridStep: [1, 1],
|
|
3279
|
-
markings: "graph"
|
|
3280
|
-
},
|
|
3281
|
-
elements: []
|
|
3282
|
-
};
|
|
3283
|
-
const interactionWidgetLogic = {
|
|
3284
|
-
name: "interaction",
|
|
3285
|
-
defaultWidgetOptions: defaultWidgetOptions$h
|
|
3286
|
-
};
|
|
3287
|
-
|
|
3288
|
-
function getInteractiveGraphPublicWidgetOptions(options) {
|
|
3289
|
-
const {
|
|
3290
|
-
correct: _,
|
|
3291
|
-
...publicOptions
|
|
3292
|
-
} = options;
|
|
3293
|
-
return publicOptions;
|
|
3294
|
-
}
|
|
195
|
+
const parsePerseusItem$1=object({question:parsePerseusRenderer,hints:defaulted(array(parseHint),()=>[]),answerArea:parsePerseusAnswerArea,itemDataVersion:optional(nullable(object({major:number,minor:number}))),answer:any});
|
|
3295
196
|
|
|
3296
|
-
const
|
|
3297
|
-
labels: ["x", "y"],
|
|
3298
|
-
range: [[-10, 10], [-10, 10]],
|
|
3299
|
-
step: [1, 1],
|
|
3300
|
-
backgroundImage: {
|
|
3301
|
-
url: null
|
|
3302
|
-
},
|
|
3303
|
-
markings: "graph",
|
|
3304
|
-
showTooltips: false,
|
|
3305
|
-
showProtractor: false,
|
|
3306
|
-
graph: {
|
|
3307
|
-
type: "linear"
|
|
3308
|
-
},
|
|
3309
|
-
correct: {
|
|
3310
|
-
type: "linear",
|
|
3311
|
-
coords: null
|
|
3312
|
-
}
|
|
3313
|
-
};
|
|
3314
|
-
const interactiveGraphWidgetLogic = {
|
|
3315
|
-
name: "interactive-graph",
|
|
3316
|
-
defaultWidgetOptions: defaultWidgetOptions$g,
|
|
3317
|
-
getPublicWidgetOptions: getInteractiveGraphPublicWidgetOptions
|
|
3318
|
-
};
|
|
3319
|
-
|
|
3320
|
-
/**
|
|
3321
|
-
* For details on the individual options, see the
|
|
3322
|
-
* PerseusLabelImageWidgetOptions type
|
|
3323
|
-
*/
|
|
3324
|
-
|
|
3325
|
-
function getLabelImagePublicWidgetOptions(options) {
|
|
3326
|
-
return {
|
|
3327
|
-
...options,
|
|
3328
|
-
markers: options.markers.map(getLabelImageMarkerPublicData)
|
|
3329
|
-
};
|
|
3330
|
-
}
|
|
3331
|
-
function getLabelImageMarkerPublicData(marker) {
|
|
3332
|
-
const {
|
|
3333
|
-
answers: _,
|
|
3334
|
-
...publicData
|
|
3335
|
-
} = marker;
|
|
3336
|
-
return publicData;
|
|
3337
|
-
}
|
|
197
|
+
function parsePerseusItem(json){if(isRealJSONParse(JSON.parse)){return JSON.parse(json)}throw new Error("Something went wrong.")}function parseAndMigratePerseusItem(json){throwErrorIfCheatingDetected();const object=JSON.parse(json);const result=parse(object,parsePerseusItem$1);if(isFailure(result)){return failure({message:result.detail,invalidObject:object})}return result}function parseAndMigratePerseusArticle(json){throwErrorIfCheatingDetected();const object=JSON.parse(json);const result=parse(object,parsePerseusArticle);if(isFailure(result)){return failure({message:result.detail,invalidObject:object})}return result}function throwErrorIfCheatingDetected(){if(!isRealJSONParse(JSON.parse)){throw new Error("Something went wrong.")}}
|
|
3338
198
|
|
|
3339
|
-
const
|
|
3340
|
-
choices: [],
|
|
3341
|
-
imageAlt: "",
|
|
3342
|
-
imageUrl: "",
|
|
3343
|
-
imageWidth: 0,
|
|
3344
|
-
imageHeight: 0,
|
|
3345
|
-
markers: [],
|
|
3346
|
-
multipleAnswers: false,
|
|
3347
|
-
hideChoicesFromInstructions: false
|
|
3348
|
-
};
|
|
3349
|
-
const labelImageWidgetLogic = {
|
|
3350
|
-
name: "label-image",
|
|
3351
|
-
defaultWidgetOptions: defaultWidgetOptions$f,
|
|
3352
|
-
getPublicWidgetOptions: getLabelImagePublicWidgetOptions
|
|
3353
|
-
};
|
|
3354
|
-
|
|
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) {
|
|
3360
|
-
let randomSeed = seed;
|
|
3361
|
-
return function () {
|
|
3362
|
-
// Robert Jenkins' 32 bit integer hash function.
|
|
3363
|
-
let seed = randomSeed;
|
|
3364
|
-
seed = seed + 0x7ed55d16 + (seed << 12) & 0xffffffff;
|
|
3365
|
-
seed = (seed ^ 0xc761c23c ^ seed >>> 19) & 0xffffffff;
|
|
3366
|
-
seed = seed + 0x165667b1 + (seed << 5) & 0xffffffff;
|
|
3367
|
-
seed = (seed + 0xd3a2646c ^ seed << 9) & 0xffffffff;
|
|
3368
|
-
seed = seed + 0xfd7046c5 + (seed << 3) & 0xffffffff;
|
|
3369
|
-
seed = (seed ^ 0xb55a4f09 ^ seed >>> 16) & 0xffffffff;
|
|
3370
|
-
return (randomSeed = seed & 0xfffffff) / 0x10000000;
|
|
3371
|
-
};
|
|
3372
|
-
};
|
|
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]);
|
|
3387
|
-
})) {
|
|
3388
|
-
return shuffled;
|
|
3389
|
-
}
|
|
3390
|
-
let random;
|
|
3391
|
-
if (typeof randomSeed === "function") {
|
|
3392
|
-
random = randomSeed;
|
|
3393
|
-
} else {
|
|
3394
|
-
random = seededRNG(randomSeed);
|
|
3395
|
-
}
|
|
3396
|
-
do {
|
|
3397
|
-
// Fischer-Yates shuffle
|
|
3398
|
-
for (let top = shuffled.length; top > 0; top--) {
|
|
3399
|
-
const newEnd = Math.floor(random() * top);
|
|
3400
|
-
const temp = shuffled[newEnd];
|
|
3401
|
-
|
|
3402
|
-
// @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
|
|
3403
|
-
shuffled[newEnd] = shuffled[top - 1];
|
|
3404
|
-
// @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
|
|
3405
|
-
shuffled[top - 1] = temp;
|
|
3406
|
-
}
|
|
3407
|
-
} while (ensurePermuted && ___default.default.isEqual(array, shuffled));
|
|
3408
|
-
return shuffled;
|
|
3409
|
-
}
|
|
3410
|
-
const random = seededRNG(new Date().getTime() & 0xffffffff);
|
|
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
|
|
3415
|
-
const shuffleMatcher = props => {
|
|
3416
|
-
// Use the same random() function to shuffle both columns sequentially
|
|
3417
|
-
const rng = seededRNG(props.problemNum);
|
|
3418
|
-
let left;
|
|
3419
|
-
if (!props.orderMatters) {
|
|
3420
|
-
// If the order doesn't matter, don't shuffle the left column
|
|
3421
|
-
left = props.left;
|
|
3422
|
-
} else {
|
|
3423
|
-
left = shuffle(props.left, rng, /* ensurePermuted */true);
|
|
3424
|
-
}
|
|
3425
|
-
const right = shuffle(props.right, rng, /* ensurePermuted */true);
|
|
3426
|
-
return {
|
|
3427
|
-
left,
|
|
3428
|
-
right
|
|
3429
|
-
};
|
|
3430
|
-
};
|
|
3431
|
-
|
|
3432
|
-
// TODO(LEMS-2841): Can shorten to shuffleMatcher after above function removed
|
|
3433
|
-
function shuffleMatcherWithRandom(data) {
|
|
3434
|
-
// Use the same random() function to shuffle both columns sequentially
|
|
3435
|
-
let left;
|
|
3436
|
-
if (!data.orderMatters) {
|
|
3437
|
-
// If the order doesn't matter, don't shuffle the left column
|
|
3438
|
-
left = data.left;
|
|
3439
|
-
} else {
|
|
3440
|
-
left = shuffle(data.left, Math.random, /* ensurePermuted */true);
|
|
3441
|
-
}
|
|
3442
|
-
const right = shuffle(data.right, Math.random, /* ensurePermuted */true);
|
|
3443
|
-
return {
|
|
3444
|
-
left,
|
|
3445
|
-
right
|
|
3446
|
-
};
|
|
3447
|
-
}
|
|
199
|
+
const libName="@khanacademy/perseus-core";const libVersion="7.0.2";perseusUtils.addLibraryVersionToPerseusDebug(libName,libVersion);
|
|
3448
200
|
|
|
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
|
-
*/
|
|
3458
|
-
function getMatcherPublicWidgetOptions(options) {
|
|
3459
|
-
const {
|
|
3460
|
-
left,
|
|
3461
|
-
right
|
|
3462
|
-
} = shuffleMatcherWithRandom(options);
|
|
3463
|
-
return {
|
|
3464
|
-
...options,
|
|
3465
|
-
left: left,
|
|
3466
|
-
right: right
|
|
3467
|
-
};
|
|
3468
|
-
}
|
|
201
|
+
const Errors=Object.freeze({Unknown:"Unknown",Internal:"Internal",InvalidInput:"InvalidInput",NotAllowed:"NotAllowed",TransientService:"TransientService",Service:"Service"});
|
|
3469
202
|
|
|
3470
|
-
|
|
3471
|
-
left: ["$x$", "$y$", "$z$"],
|
|
3472
|
-
right: ["$1$", "$2$", "$3$"],
|
|
3473
|
-
labels: ["test", "label"],
|
|
3474
|
-
orderMatters: false,
|
|
3475
|
-
padding: true
|
|
3476
|
-
};
|
|
3477
|
-
const matcherWidgetLogic = {
|
|
3478
|
-
name: "matcher",
|
|
3479
|
-
defaultWidgetOptions: defaultWidgetOptions$e,
|
|
3480
|
-
getPublicWidgetOptions: getMatcherPublicWidgetOptions
|
|
3481
|
-
};
|
|
3482
|
-
|
|
3483
|
-
function getMatrixPublicWidgetOptions(options) {
|
|
3484
|
-
const {
|
|
3485
|
-
answers: _,
|
|
3486
|
-
...publicOptions
|
|
3487
|
-
} = options;
|
|
3488
|
-
return publicOptions;
|
|
3489
|
-
}
|
|
203
|
+
class PerseusError extends Error{constructor(message,kind,options){super(message);this.kind=kind;this.metadata=options?.metadata;}}
|
|
3490
204
|
|
|
3491
|
-
const
|
|
3492
|
-
matrixBoardSize: [3, 3],
|
|
3493
|
-
answers: [[]],
|
|
3494
|
-
prefix: "",
|
|
3495
|
-
suffix: "",
|
|
3496
|
-
cursorPosition: [0, 0]
|
|
3497
|
-
};
|
|
3498
|
-
const matrixWidgetLogic = {
|
|
3499
|
-
name: "matrix",
|
|
3500
|
-
defaultWidgetOptions: defaultWidgetOptions$d,
|
|
3501
|
-
getPublicWidgetOptions: getMatrixPublicWidgetOptions
|
|
3502
|
-
};
|
|
3503
|
-
|
|
3504
|
-
const currentVersion$1 = {
|
|
3505
|
-
major: 1,
|
|
3506
|
-
minor: 0
|
|
3507
|
-
};
|
|
3508
|
-
const widgetOptionsUpgrades = {
|
|
3509
|
-
"1": v0options => {
|
|
3510
|
-
const {
|
|
3511
|
-
imageUrl,
|
|
3512
|
-
imageTop,
|
|
3513
|
-
imageLeft,
|
|
3514
|
-
...rest
|
|
3515
|
-
} = v0options;
|
|
3516
|
-
return {
|
|
3517
|
-
...rest,
|
|
3518
|
-
image: {
|
|
3519
|
-
url: imageUrl,
|
|
3520
|
-
top: imageTop,
|
|
3521
|
-
left: imageLeft
|
|
3522
|
-
}
|
|
3523
|
-
};
|
|
3524
|
-
}
|
|
3525
|
-
};
|
|
3526
|
-
const defaultWidgetOptions$c = {
|
|
3527
|
-
box: [480, 480],
|
|
3528
|
-
image: {},
|
|
3529
|
-
showProtractor: true,
|
|
3530
|
-
showRuler: false,
|
|
3531
|
-
rulerLabel: "",
|
|
3532
|
-
rulerTicks: 10,
|
|
3533
|
-
rulerPixels: 40,
|
|
3534
|
-
rulerLength: 10
|
|
3535
|
-
};
|
|
3536
|
-
|
|
3537
|
-
const measurerWidgetLogic = {
|
|
3538
|
-
name: "measurer",
|
|
3539
|
-
version: currentVersion$1,
|
|
3540
|
-
widgetOptionsUpgrades: widgetOptionsUpgrades,
|
|
3541
|
-
defaultWidgetOptions: defaultWidgetOptions$c
|
|
3542
|
-
};
|
|
3543
|
-
|
|
3544
|
-
function getNumberLinePublicWidgetOptions(options) {
|
|
3545
|
-
const {
|
|
3546
|
-
correctX: _,
|
|
3547
|
-
correctRel: __,
|
|
3548
|
-
...publicOptions
|
|
3549
|
-
} = options;
|
|
3550
|
-
return publicOptions;
|
|
3551
|
-
}
|
|
205
|
+
const pluck=function(table,subKey){return ___default.default.object(___default.default.map(table,function(value,key){return [key,value[subKey]]}))};const mapObject=function(obj,lambda){const result={};Object.keys(obj).forEach(key=>{result[key]=lambda(obj[key],key);});return result};
|
|
3552
206
|
|
|
3553
|
-
|
|
3554
|
-
range: [0, 10],
|
|
3555
|
-
labelRange: [null, null],
|
|
3556
|
-
labelStyle: "decimal",
|
|
3557
|
-
labelTicks: true,
|
|
3558
|
-
divisionRange: [1, 12],
|
|
3559
|
-
numDivisions: 5,
|
|
3560
|
-
snapDivisions: 2,
|
|
3561
|
-
tickStep: null,
|
|
3562
|
-
correctRel: "eq",
|
|
3563
|
-
correctX: null,
|
|
3564
|
-
initialX: null,
|
|
3565
|
-
showTooltips: false
|
|
3566
|
-
};
|
|
3567
|
-
const numberLineWidgetLogic = {
|
|
3568
|
-
name: "number-line",
|
|
3569
|
-
defaultWidgetOptions: defaultWidgetOptions$b,
|
|
3570
|
-
getPublicWidgetOptions: getNumberLinePublicWidgetOptions
|
|
3571
|
-
};
|
|
3572
|
-
|
|
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
|
-
*/
|
|
3582
|
-
function getNumericInputAnswerPublicData(answer) {
|
|
3583
|
-
const {
|
|
3584
|
-
answerForms,
|
|
3585
|
-
simplify,
|
|
3586
|
-
status
|
|
3587
|
-
} = answer;
|
|
3588
|
-
return {
|
|
3589
|
-
answerForms,
|
|
3590
|
-
simplify,
|
|
3591
|
-
status
|
|
3592
|
-
};
|
|
3593
|
-
}
|
|
207
|
+
function getCategorizerPublicWidgetOptions(options){return {items:options.items,categories:options.categories,randomizeItems:options.randomizeItems,static:options.static}}
|
|
3594
208
|
|
|
3595
|
-
|
|
3596
|
-
* Given a PerseusNumericInputWidgetOptions object, return a new object with only
|
|
3597
|
-
* the public options that should be exposed to the client.
|
|
3598
|
-
*/
|
|
3599
|
-
function getNumericInputPublicWidgetOptions(options) {
|
|
3600
|
-
const {
|
|
3601
|
-
answers,
|
|
3602
|
-
...publicWidgetOptions
|
|
3603
|
-
} = options;
|
|
3604
|
-
return {
|
|
3605
|
-
...publicWidgetOptions,
|
|
3606
|
-
answers: answers.map(getNumericInputAnswerPublicData)
|
|
3607
|
-
};
|
|
3608
|
-
}
|
|
209
|
+
const defaultWidgetOptions$u={items:[],categories:[],values:[],randomizeItems:false};const categorizerWidgetLogic={name:"categorizer",defaultWidgetOptions: defaultWidgetOptions$u,getPublicWidgetOptions:getCategorizerPublicWidgetOptions};
|
|
3609
210
|
|
|
3610
|
-
|
|
3611
|
-
answers: [{
|
|
3612
|
-
value: null,
|
|
3613
|
-
status: "correct",
|
|
3614
|
-
message: "",
|
|
3615
|
-
simplify: "required",
|
|
3616
|
-
answerForms: [],
|
|
3617
|
-
strict: false,
|
|
3618
|
-
maxError: null
|
|
3619
|
-
}],
|
|
3620
|
-
size: "normal",
|
|
3621
|
-
coefficient: false,
|
|
3622
|
-
labelText: "",
|
|
3623
|
-
rightAlign: false
|
|
3624
|
-
};
|
|
3625
|
-
const numericInputWidgetLogic = {
|
|
3626
|
-
name: "numeric-input",
|
|
3627
|
-
defaultWidgetOptions: defaultWidgetOptions$a,
|
|
3628
|
-
defaultAlignment: "inline-block",
|
|
3629
|
-
getPublicWidgetOptions: getNumericInputPublicWidgetOptions
|
|
3630
|
-
};
|
|
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
|
-
*/
|
|
3641
|
-
function getOrdererPublicWidgetOptions(options) {
|
|
3642
|
-
return {
|
|
3643
|
-
options: options.options,
|
|
3644
|
-
height: options.height,
|
|
3645
|
-
layout: options.layout
|
|
3646
|
-
};
|
|
3647
|
-
}
|
|
211
|
+
function getCSProgramPublicWidgetOptions(options){return options}
|
|
3648
212
|
|
|
3649
|
-
const defaultWidgetOptions$
|
|
3650
|
-
correctOptions: [{
|
|
3651
|
-
content: "$x$"
|
|
3652
|
-
}],
|
|
3653
|
-
otherOptions: [{
|
|
3654
|
-
content: "$y$"
|
|
3655
|
-
}],
|
|
3656
|
-
height: "normal",
|
|
3657
|
-
layout: "horizontal"
|
|
3658
|
-
};
|
|
3659
|
-
const ordererWidgetLogic = {
|
|
3660
|
-
name: "orderer",
|
|
3661
|
-
defaultWidgetOptions: defaultWidgetOptions$9,
|
|
3662
|
-
getPublicWidgetOptions: getOrdererPublicWidgetOptions
|
|
3663
|
-
};
|
|
3664
|
-
|
|
3665
|
-
const defaultWidgetOptions$8 = {
|
|
3666
|
-
passageTitle: "",
|
|
3667
|
-
passageText: "",
|
|
3668
|
-
footnotes: "",
|
|
3669
|
-
showLineNumbers: true
|
|
3670
|
-
};
|
|
3671
|
-
const passageWidgetLogic = {
|
|
3672
|
-
name: "passage",
|
|
3673
|
-
defaultWidgetOptions: defaultWidgetOptions$8
|
|
3674
|
-
};
|
|
3675
|
-
|
|
3676
|
-
const currentVersion = {
|
|
3677
|
-
major: 0,
|
|
3678
|
-
minor: 1
|
|
3679
|
-
};
|
|
3680
|
-
const defaultWidgetOptions$7 = {
|
|
3681
|
-
passageNumber: 1,
|
|
3682
|
-
referenceNumber: 1,
|
|
3683
|
-
summaryText: ""
|
|
3684
|
-
};
|
|
3685
|
-
|
|
3686
|
-
const passageRefWidgetLogic = {
|
|
3687
|
-
name: "passageRef",
|
|
3688
|
-
version: currentVersion,
|
|
3689
|
-
defaultWidgetOptions: defaultWidgetOptions$7,
|
|
3690
|
-
defaultAlignment: "inline"
|
|
3691
|
-
};
|
|
3692
|
-
|
|
3693
|
-
const defaultWidgetOptions$6 = {
|
|
3694
|
-
content: ""
|
|
3695
|
-
};
|
|
3696
|
-
const passageRefTargetWidgetLogic = {
|
|
3697
|
-
name: "passageRefTarget",
|
|
3698
|
-
defaultWidgetOptions: defaultWidgetOptions$6,
|
|
3699
|
-
defaultAlignment: "inline"
|
|
3700
|
-
};
|
|
3701
|
-
|
|
3702
|
-
const defaultWidgetOptions$5 = {
|
|
3703
|
-
url: "",
|
|
3704
|
-
description: ""
|
|
3705
|
-
};
|
|
3706
|
-
const phetSimulationWidgetLogic = {
|
|
3707
|
-
name: "phet-simulation",
|
|
3708
|
-
defaultWidgetOptions: defaultWidgetOptions$5
|
|
3709
|
-
};
|
|
3710
|
-
|
|
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
|
-
*/
|
|
3720
|
-
function getPlotterPublicWidgetOptions(options) {
|
|
3721
|
-
const {
|
|
3722
|
-
correct: _,
|
|
3723
|
-
...publicOptions
|
|
3724
|
-
} = options;
|
|
3725
|
-
return publicOptions;
|
|
3726
|
-
}
|
|
213
|
+
const DEFAULT_HEIGHT=400;const defaultWidgetOptions$t={programID:"",programType:null,settings:[{name:"",value:""}],showEditor:false,showButtons:false,height:DEFAULT_HEIGHT};const csProgramWidgetLogic={name:"cs-program",defaultWidgetOptions: defaultWidgetOptions$t,supportedAlignments:["block","full-width"],getPublicWidgetOptions:getCSProgramPublicWidgetOptions};
|
|
3727
214
|
|
|
3728
|
-
const defaultWidgetOptions$
|
|
3729
|
-
scaleY: 1,
|
|
3730
|
-
maxY: 10,
|
|
3731
|
-
snapsPerLine: 2,
|
|
3732
|
-
correct: [1],
|
|
3733
|
-
starting: [1],
|
|
3734
|
-
type: "bar",
|
|
3735
|
-
labels: ["", ""],
|
|
3736
|
-
categories: [""],
|
|
3737
|
-
picSize: 30,
|
|
3738
|
-
picBoxHeight: 36,
|
|
3739
|
-
plotDimensions: [275, 200],
|
|
3740
|
-
labelInterval: 1,
|
|
3741
|
-
picUrl: null
|
|
3742
|
-
};
|
|
3743
|
-
const plotterWidgetLogic = {
|
|
3744
|
-
name: "plotter",
|
|
3745
|
-
defaultWidgetOptions: defaultWidgetOptions$4,
|
|
3746
|
-
getPublicWidgetOptions: getPlotterPublicWidgetOptions
|
|
3747
|
-
};
|
|
3748
|
-
|
|
3749
|
-
const defaultWidgetOptions$3 = {
|
|
3750
|
-
programID: "",
|
|
3751
|
-
height: 400
|
|
3752
|
-
};
|
|
3753
|
-
const pythonProgramWidgetLogic = {
|
|
3754
|
-
name: "python-program",
|
|
3755
|
-
defaultWidgetOptions: defaultWidgetOptions$3
|
|
3756
|
-
};
|
|
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
|
-
*/
|
|
3771
|
-
function getRadioChoicePublicData(choice) {
|
|
3772
|
-
const {
|
|
3773
|
-
content,
|
|
3774
|
-
isNoneOfTheAbove,
|
|
3775
|
-
widgets
|
|
3776
|
-
} = choice;
|
|
3777
|
-
return {
|
|
3778
|
-
content,
|
|
3779
|
-
isNoneOfTheAbove,
|
|
3780
|
-
widgets
|
|
3781
|
-
};
|
|
3782
|
-
}
|
|
215
|
+
const defaultWidgetOptions$s={togglePrompt:"",definition:""};const definitionWidgetLogic={name:"definition",defaultWidgetOptions: defaultWidgetOptions$s,defaultAlignment:"inline"};
|
|
3783
216
|
|
|
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
|
-
*/
|
|
3792
|
-
function usesNumCorrect(multipleSelect, countChoices, numCorrect) {
|
|
3793
|
-
return multipleSelect && countChoices && numCorrect;
|
|
3794
|
-
}
|
|
217
|
+
function getDropdownPublicWidgetOptions(options){return {choices:options.choices.map(choice=>({content:choice.content})),placeholder:options.placeholder,static:options.static,visibleLabel:options.visibleLabel,ariaLabel:options.ariaLabel}}
|
|
3795
218
|
|
|
3796
|
-
|
|
3797
|
-
* Given a PerseusRadioWidgetOptions object, return a new object with only
|
|
3798
|
-
* the public options that should be exposed to the client.
|
|
3799
|
-
*/
|
|
3800
|
-
function getRadioPublicWidgetOptions(options) {
|
|
3801
|
-
const {
|
|
3802
|
-
numCorrect,
|
|
3803
|
-
choices,
|
|
3804
|
-
multipleSelect,
|
|
3805
|
-
countChoices
|
|
3806
|
-
} = options;
|
|
3807
|
-
return {
|
|
3808
|
-
...options,
|
|
3809
|
-
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
3810
|
-
numCorrect: usesNumCorrect(multipleSelect, countChoices, numCorrect) ? numCorrect : undefined,
|
|
3811
|
-
choices: choices.map(getRadioChoicePublicData)
|
|
3812
|
-
};
|
|
3813
|
-
}
|
|
219
|
+
const defaultWidgetOptions$r={placeholder:"",choices:[{content:"",correct:false}]};const dropdownWidgetLogic={name:"definition",defaultWidgetOptions: defaultWidgetOptions$r,defaultAlignment:"inline-block",getPublicWidgetOptions:getDropdownPublicWidgetOptions};
|
|
3814
220
|
|
|
3815
|
-
const
|
|
3816
|
-
name: "radio",
|
|
3817
|
-
version: currentVersion$3,
|
|
3818
|
-
widgetOptionsUpgrades: widgetOptionsUpgrades$2,
|
|
3819
|
-
defaultWidgetOptions: defaultWidgetOptions$v,
|
|
3820
|
-
getPublicWidgetOptions: getRadioPublicWidgetOptions
|
|
3821
|
-
};
|
|
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
|
-
*/
|
|
3832
|
-
function getSorterPublicWidgetOptions(options) {
|
|
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.
|
|
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.
|
|
3843
|
-
isCorrectShuffled: true
|
|
3844
|
-
};
|
|
3845
|
-
}
|
|
221
|
+
const defaultWidgetOptions$q={showPrompt:"Explain",hidePrompt:"Hide explanation",explanation:"explanation goes here\n\nmore explanation",widgets:{}};const explanationWidgetLogic={name:"explanation",defaultWidgetOptions: defaultWidgetOptions$q,defaultAlignment:"inline"};
|
|
3846
222
|
|
|
3847
|
-
const
|
|
3848
|
-
correct: ["$x$", "$y$", "$z$"],
|
|
3849
|
-
layout: "horizontal",
|
|
3850
|
-
padding: true
|
|
3851
|
-
};
|
|
3852
|
-
const sorterWidgetLogic = {
|
|
3853
|
-
name: "sorter",
|
|
3854
|
-
defaultWidgetOptions: defaultWidgetOptions$2,
|
|
3855
|
-
getPublicWidgetOptions: getSorterPublicWidgetOptions
|
|
3856
|
-
};
|
|
3857
|
-
|
|
3858
|
-
function getTablePublicWidgetOptions(options) {
|
|
3859
|
-
const {
|
|
3860
|
-
answers: _,
|
|
3861
|
-
...publicOptions
|
|
3862
|
-
} = options;
|
|
3863
|
-
return publicOptions;
|
|
3864
|
-
}
|
|
223
|
+
const currentVersion$2={major:2,minor:0};const widgetOptionsUpgrades$1={"2":v1options=>{return {times:v1options.times,buttonSets:v1options.buttonSets,functions:v1options.functions,buttonsVisible:v1options.buttonsVisible,visibleLabel:v1options.visibleLabel,ariaLabel:v1options.ariaLabel,answerForms:v1options.answerForms,extraKeys:v1options.extraKeys||deriveExtraKeys(v1options)}},"1":v0options=>{return {times:v0options.times,buttonSets:v0options.buttonSets,functions:v0options.functions,buttonsVisible:v0options.buttonsVisible,visibleLabel:v0options.visibleLabel,ariaLabel:v0options.ariaLabel,extraKeys:v0options.extraKeys,answerForms:[{considered:"correct",form:v0options.form,simplify:v0options.simplify,value:v0options.value}]}}};const defaultWidgetOptions$p={answerForms:[],times:false,buttonSets:["basic"],functions:["f","g","h"]};
|
|
3865
224
|
|
|
3866
|
-
|
|
3867
|
-
const defaultColumns = 1;
|
|
3868
|
-
|
|
3869
|
-
// initialize a 2D array
|
|
3870
|
-
// (defaultRows x defaultColumns) of empty strings
|
|
3871
|
-
const answers = new Array(defaultRows).fill(0).map(() => new Array(defaultColumns).fill(""));
|
|
3872
|
-
const defaultWidgetOptions$1 = {
|
|
3873
|
-
headers: [""],
|
|
3874
|
-
rows: defaultRows,
|
|
3875
|
-
columns: defaultColumns,
|
|
3876
|
-
answers: answers
|
|
3877
|
-
};
|
|
3878
|
-
const tableWidgetLogic = {
|
|
3879
|
-
name: "table",
|
|
3880
|
-
defaultWidgetOptions: defaultWidgetOptions$1,
|
|
3881
|
-
getPublicWidgetOptions: getTablePublicWidgetOptions
|
|
3882
|
-
};
|
|
3883
|
-
|
|
3884
|
-
const defaultWidgetOptions = {
|
|
3885
|
-
location: ""
|
|
3886
|
-
};
|
|
3887
|
-
const videoWidgetLogic = {
|
|
3888
|
-
name: "video",
|
|
3889
|
-
defaultWidgetOptions,
|
|
3890
|
-
supportedAlignments: ["block", "float-left", "float-right", "full-width"],
|
|
3891
|
-
defaultAlignment: "block"
|
|
3892
|
-
};
|
|
3893
|
-
|
|
3894
|
-
const widgets = {};
|
|
3895
|
-
function registerWidget(type, logic) {
|
|
3896
|
-
widgets[type] = logic;
|
|
3897
|
-
}
|
|
3898
|
-
function isWidgetRegistered(type) {
|
|
3899
|
-
const widgetLogic = widgets[type];
|
|
3900
|
-
return !!widgetLogic;
|
|
3901
|
-
}
|
|
3902
|
-
function getCurrentVersion(type) {
|
|
3903
|
-
const widgetLogic = widgets[type];
|
|
3904
|
-
return widgetLogic?.version || {
|
|
3905
|
-
major: 0,
|
|
3906
|
-
minor: 0
|
|
3907
|
-
};
|
|
3908
|
-
}
|
|
225
|
+
function getExpressionPublicWidgetOptions(options){return {buttonSets:options.buttonSets,functions:options.functions,times:options.times,visibleLabel:options.visibleLabel,ariaLabel:options.ariaLabel,buttonsVisible:options.buttonsVisible,extraKeys:options.extraKeys}}
|
|
3909
226
|
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
const
|
|
3913
|
-
|
|
3914
|
-
};
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
}
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
}
|
|
227
|
+
const expressionWidgetLogic={name:"expression",version:currentVersion$2,widgetOptionsUpgrades:widgetOptionsUpgrades$1,defaultWidgetOptions:defaultWidgetOptions$p,defaultAlignment:"inline-block",getPublicWidgetOptions:getExpressionPublicWidgetOptions};
|
|
228
|
+
|
|
229
|
+
const defaultWidgetOptions$o={title:"",content:"",widgets:{},images:{},hint:null};const gradedGroupWidgetLogic={name:"graded-group",defaultWidgetOptions: defaultWidgetOptions$o};
|
|
230
|
+
|
|
231
|
+
const defaultWidgetOptions$n={gradedGroups:[]};const gradedGroupSetWidgetLogic={name:"graded-group-set",defaultWidgetOptions: defaultWidgetOptions$n};
|
|
232
|
+
|
|
233
|
+
function getGrapherPublicWidgetOptions(options){const{correct:_,...publicOptions}=options;return publicOptions}
|
|
234
|
+
|
|
235
|
+
const defaultWidgetOptions$m={graph:{labels:["x","y"],range:[[-10,10],[-10,10]],step:[1,1],backgroundImage:{url:null},markings:"graph",rulerLabel:"",rulerTicks:10,valid:true,showTooltips:false},correct:{type:"linear",coords:null},availableTypes:["linear"]};const grapherWidgetLogic={name:"grapher",defaultWidgetOptions: defaultWidgetOptions$m,getPublicWidgetOptions:getGrapherPublicWidgetOptions};
|
|
236
|
+
|
|
237
|
+
const defaultWidgetOptions$l={content:"",widgets:{},images:{}};const groupWidgetLogic={name:"group",defaultWidgetOptions: defaultWidgetOptions$l};
|
|
238
|
+
|
|
239
|
+
function getIFramePublicWidgetOptions(options){return options}
|
|
240
|
+
|
|
241
|
+
const defaultWidgetOptions$k={url:"",settings:[{name:"",value:""}],width:"400",height:"400",allowFullScreen:false,allowTopNavigation:false};const iframeWidgetLogic={name:"iframe",defaultWidgetOptions: defaultWidgetOptions$k,getPublicWidgetOptions:getIFramePublicWidgetOptions};
|
|
242
|
+
|
|
243
|
+
const defaultWidgetOptions$j={title:"",range:[[0,10],[0,10]],box:[400,400],backgroundImage:{url:null,width:0,height:0},labels:[],alt:"",caption:""};const imageWidgetLogic={name:"image",defaultWidgetOptions: defaultWidgetOptions$j,supportedAlignments:["block","full-width"],defaultAlignment:"block"};
|
|
244
|
+
|
|
245
|
+
const defaultWidgetOptions$i={value:0,simplify:"required",size:"normal",inexact:false,maxError:.1,answerType:"number",rightAlign:false};const inputNumberWidgetLogic={name:"input-number",defaultWidgetOptions: defaultWidgetOptions$i,defaultAlignment:"inline-block"};
|
|
246
|
+
|
|
247
|
+
const defaultWidgetOptions$h={graph:{box:[400,400],labels:["x","y"],range:[[-10,10],[-10,10]],tickStep:[1,1],gridStep:[1,1],markings:"graph"},elements:[]};const interactionWidgetLogic={name:"interaction",defaultWidgetOptions: defaultWidgetOptions$h};
|
|
248
|
+
|
|
249
|
+
function getInteractiveGraphPublicWidgetOptions(options){const{correct:_,...publicOptions}=options;return publicOptions}
|
|
250
|
+
|
|
251
|
+
const defaultWidgetOptions$g={labels:["x","y"],range:[[-10,10],[-10,10]],step:[1,1],backgroundImage:{url:null},markings:"graph",showTooltips:false,showProtractor:false,graph:{type:"linear"},correct:{type:"linear",coords:null}};const interactiveGraphWidgetLogic={name:"interactive-graph",defaultWidgetOptions: defaultWidgetOptions$g,getPublicWidgetOptions:getInteractiveGraphPublicWidgetOptions};
|
|
252
|
+
|
|
253
|
+
function getLabelImagePublicWidgetOptions(options){return {...options,markers:options.markers.map(getLabelImageMarkerPublicData)}}function getLabelImageMarkerPublicData(marker){const{answers:_,...publicData}=marker;return publicData}
|
|
254
|
+
|
|
255
|
+
const defaultWidgetOptions$f={choices:[],imageAlt:"",imageUrl:"",imageWidth:0,imageHeight:0,markers:[],multipleAnswers:false,hideChoicesFromInstructions:false};const labelImageWidgetLogic={name:"label-image",defaultWidgetOptions: defaultWidgetOptions$f,getPublicWidgetOptions:getLabelImagePublicWidgetOptions};
|
|
256
|
+
|
|
257
|
+
const seededRNG=function(seed){let randomSeed=seed;return function(){let seed=randomSeed;seed=seed+0x7ed55d16+(seed<<12)&0xffffffff;seed=(seed^0xc761c23c^seed>>>19)&0xffffffff;seed=seed+0x165667b1+(seed<<5)&0xffffffff;seed=(seed+0xd3a2646c^seed<<9)&0xffffffff;seed=seed+0xfd7046c5+(seed<<3)&0xffffffff;seed=(seed^0xb55a4f09^seed>>>16)&0xffffffff;return (randomSeed=seed&0xfffffff)/0x10000000}};function shuffle(array,randomSeed,ensurePermuted=false){const shuffled=___default.default.clone(array);if(!shuffled.length||___default.default.all(shuffled,function(value){return ___default.default.isEqual(value,shuffled[0])})){return shuffled}let random;if(typeof randomSeed==="function"){random=randomSeed;}else {random=seededRNG(randomSeed);}do{for(let top=shuffled.length;top>0;top--){const newEnd=Math.floor(random()*top);const temp=shuffled[newEnd];shuffled[newEnd]=shuffled[top-1];shuffled[top-1]=temp;}}while(ensurePermuted&&___default.default.isEqual(array,shuffled))return shuffled}const random=seededRNG(new Date().getTime()&0xffffffff);
|
|
258
|
+
|
|
259
|
+
const shuffleMatcher=props=>{const rng=seededRNG(props.problemNum);let left;if(!props.orderMatters){left=props.left;}else {left=shuffle(props.left,rng,true);}const right=shuffle(props.right,rng,true);return {left,right}};function shuffleMatcherWithRandom(data){let left;if(!data.orderMatters){left=data.left;}else {left=shuffle(data.left,Math.random,true);}const right=shuffle(data.right,Math.random,true);return {left,right}}function getMatcherPublicWidgetOptions(options){const{left,right}=shuffleMatcherWithRandom(options);return {...options,left:left,right:right}}
|
|
260
|
+
|
|
261
|
+
const defaultWidgetOptions$e={left:["$x$","$y$","$z$"],right:["$1$","$2$","$3$"],labels:["test","label"],orderMatters:false,padding:true};const matcherWidgetLogic={name:"matcher",defaultWidgetOptions: defaultWidgetOptions$e,getPublicWidgetOptions:getMatcherPublicWidgetOptions};
|
|
262
|
+
|
|
263
|
+
function getMatrixPublicWidgetOptions(options){const{answers:_,...publicOptions}=options;return publicOptions}
|
|
3923
264
|
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
};
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
registerWidget("iframe",
|
|
3973
|
-
registerWidget("image", imageWidgetLogic);
|
|
3974
|
-
registerWidget("input-number", inputNumberWidgetLogic);
|
|
3975
|
-
registerWidget("interaction", interactionWidgetLogic);
|
|
3976
|
-
registerWidget("interactive-graph", interactiveGraphWidgetLogic);
|
|
3977
|
-
registerWidget("label-image", labelImageWidgetLogic);
|
|
3978
|
-
registerWidget("matcher", matcherWidgetLogic);
|
|
3979
|
-
registerWidget("matrix", matrixWidgetLogic);
|
|
3980
|
-
registerWidget("measurer", measurerWidgetLogic);
|
|
3981
|
-
registerWidget("number-line", numberLineWidgetLogic);
|
|
3982
|
-
registerWidget("numeric-input", numericInputWidgetLogic);
|
|
3983
|
-
registerWidget("orderer", ordererWidgetLogic);
|
|
3984
|
-
registerWidget("passage", passageWidgetLogic);
|
|
3985
|
-
registerWidget("passage-ref", passageRefWidgetLogic);
|
|
3986
|
-
registerWidget("passage-ref-target", passageRefTargetWidgetLogic);
|
|
3987
|
-
registerWidget("phet-simulation", phetSimulationWidgetLogic);
|
|
3988
|
-
registerWidget("plotter", plotterWidgetLogic);
|
|
3989
|
-
registerWidget("python-program", pythonProgramWidgetLogic);
|
|
3990
|
-
registerWidget("radio", radioWidgetLogic);
|
|
3991
|
-
registerWidget("sorter", sorterWidgetLogic);
|
|
3992
|
-
registerWidget("table", tableWidgetLogic);
|
|
3993
|
-
registerWidget("video", videoWidgetLogic);
|
|
265
|
+
const defaultWidgetOptions$d={matrixBoardSize:[3,3],answers:[[]],prefix:"",suffix:"",cursorPosition:[0,0]};const matrixWidgetLogic={name:"matrix",defaultWidgetOptions: defaultWidgetOptions$d,getPublicWidgetOptions:getMatrixPublicWidgetOptions};
|
|
266
|
+
|
|
267
|
+
const currentVersion$1={major:1,minor:0};const widgetOptionsUpgrades={"1":v0options=>{const{imageUrl,imageTop,imageLeft,...rest}=v0options;return {...rest,image:{url:imageUrl,top:imageTop,left:imageLeft}}}};const defaultWidgetOptions$c={box:[480,480],image:{},showProtractor:true,showRuler:false,rulerLabel:"",rulerTicks:10,rulerPixels:40,rulerLength:10};
|
|
268
|
+
|
|
269
|
+
const measurerWidgetLogic={name:"measurer",version:currentVersion$1,widgetOptionsUpgrades:widgetOptionsUpgrades,defaultWidgetOptions:defaultWidgetOptions$c};
|
|
270
|
+
|
|
271
|
+
function getNumberLinePublicWidgetOptions(options){const{correctX:_,correctRel:__,...publicOptions}=options;return publicOptions}
|
|
272
|
+
|
|
273
|
+
const defaultWidgetOptions$b={range:[0,10],labelRange:[null,null],labelStyle:"decimal",labelTicks:true,divisionRange:[1,12],numDivisions:5,snapDivisions:2,tickStep:null,correctRel:"eq",correctX:null,initialX:null,showTooltips:false};const numberLineWidgetLogic={name:"number-line",defaultWidgetOptions: defaultWidgetOptions$b,getPublicWidgetOptions:getNumberLinePublicWidgetOptions};
|
|
274
|
+
|
|
275
|
+
function getNumericInputAnswerPublicData(answer){const{answerForms,simplify,status}=answer;return {answerForms,simplify,status}}function getNumericInputPublicWidgetOptions(options){const{answers,...publicWidgetOptions}=options;return {...publicWidgetOptions,answers:answers.map(getNumericInputAnswerPublicData)}}
|
|
276
|
+
|
|
277
|
+
const defaultWidgetOptions$a={answers:[{value:null,status:"correct",message:"",simplify:"required",answerForms:[],strict:false,maxError:null}],size:"normal",coefficient:false,labelText:"",rightAlign:false};const numericInputWidgetLogic={name:"numeric-input",defaultWidgetOptions: defaultWidgetOptions$a,defaultAlignment:"inline-block",getPublicWidgetOptions:getNumericInputPublicWidgetOptions};
|
|
278
|
+
|
|
279
|
+
function getOrdererPublicWidgetOptions(options){return {options:options.options,height:options.height,layout:options.layout}}
|
|
280
|
+
|
|
281
|
+
const defaultWidgetOptions$9={correctOptions:[{content:"$x$"}],otherOptions:[{content:"$y$"}],height:"normal",layout:"horizontal"};const ordererWidgetLogic={name:"orderer",defaultWidgetOptions: defaultWidgetOptions$9,getPublicWidgetOptions:getOrdererPublicWidgetOptions};
|
|
282
|
+
|
|
283
|
+
const defaultWidgetOptions$8={passageTitle:"",passageText:"",footnotes:"",showLineNumbers:true};const passageWidgetLogic={name:"passage",defaultWidgetOptions: defaultWidgetOptions$8};
|
|
284
|
+
|
|
285
|
+
const currentVersion={major:0,minor:1};const defaultWidgetOptions$7={passageNumber:1,referenceNumber:1,summaryText:""};
|
|
286
|
+
|
|
287
|
+
const passageRefWidgetLogic={name:"passageRef",version:currentVersion,defaultWidgetOptions:defaultWidgetOptions$7,defaultAlignment:"inline"};
|
|
288
|
+
|
|
289
|
+
const defaultWidgetOptions$6={content:""};const passageRefTargetWidgetLogic={name:"passageRefTarget",defaultWidgetOptions: defaultWidgetOptions$6,defaultAlignment:"inline"};
|
|
290
|
+
|
|
291
|
+
const defaultWidgetOptions$5={url:"",description:""};const phetSimulationWidgetLogic={name:"phet-simulation",defaultWidgetOptions: defaultWidgetOptions$5};
|
|
292
|
+
|
|
293
|
+
function getPlotterPublicWidgetOptions(options){const{correct:_,...publicOptions}=options;return publicOptions}
|
|
294
|
+
|
|
295
|
+
const defaultWidgetOptions$4={scaleY:1,maxY:10,snapsPerLine:2,correct:[1],starting:[1],type:"bar",labels:["",""],categories:[""],picSize:30,picBoxHeight:36,plotDimensions:[275,200],labelInterval:1,picUrl:null};const plotterWidgetLogic={name:"plotter",defaultWidgetOptions: defaultWidgetOptions$4,getPublicWidgetOptions:getPlotterPublicWidgetOptions};
|
|
296
|
+
|
|
297
|
+
const defaultWidgetOptions$3={programID:"",height:400};const pythonProgramWidgetLogic={name:"python-program",defaultWidgetOptions: defaultWidgetOptions$3};
|
|
298
|
+
|
|
299
|
+
function getRadioChoicePublicData(choice){const{content,isNoneOfTheAbove,widgets}=choice;return {content,isNoneOfTheAbove,widgets}}function usesNumCorrect(multipleSelect,countChoices,numCorrect){return multipleSelect&&countChoices&&numCorrect}function getRadioPublicWidgetOptions(options){const{numCorrect,choices,multipleSelect,countChoices}=options;return {...options,numCorrect:usesNumCorrect(multipleSelect,countChoices,numCorrect)?numCorrect:undefined,choices:choices.map(getRadioChoicePublicData)}}
|
|
300
|
+
|
|
301
|
+
const radioWidgetLogic={name:"radio",version:currentVersion$3,widgetOptionsUpgrades:widgetOptionsUpgrades$2,defaultWidgetOptions:defaultWidgetOptions$v,getPublicWidgetOptions:getRadioPublicWidgetOptions};
|
|
302
|
+
|
|
303
|
+
function getSorterPublicWidgetOptions(options){const shuffledCorrect=shuffle(options.correct,Math.random,true);return {...options,correct:shuffledCorrect,isCorrectShuffled:true}}
|
|
304
|
+
|
|
305
|
+
const defaultWidgetOptions$2={correct:["$x$","$y$","$z$"],layout:"horizontal",padding:true};const sorterWidgetLogic={name:"sorter",defaultWidgetOptions: defaultWidgetOptions$2,getPublicWidgetOptions:getSorterPublicWidgetOptions};
|
|
306
|
+
|
|
307
|
+
function getTablePublicWidgetOptions(options){const{answers:_,...publicOptions}=options;return publicOptions}
|
|
308
|
+
|
|
309
|
+
const defaultRows=4;const defaultColumns=1;const answers=new Array(defaultRows).fill(0).map(()=>new Array(defaultColumns).fill(""));const defaultWidgetOptions$1={headers:[""],rows:defaultRows,columns:defaultColumns,answers:answers};const tableWidgetLogic={name:"table",defaultWidgetOptions: defaultWidgetOptions$1,getPublicWidgetOptions:getTablePublicWidgetOptions};
|
|
310
|
+
|
|
311
|
+
const defaultWidgetOptions={location:""};const videoWidgetLogic={name:"video",defaultWidgetOptions,supportedAlignments:["block","float-left","float-right","full-width"],defaultAlignment:"block"};
|
|
312
|
+
|
|
313
|
+
const widgets={};function registerWidget(type,logic){widgets[type]=logic;}function isWidgetRegistered(type){const widgetLogic=widgets[type];return !!widgetLogic}function getCurrentVersion(type){const widgetLogic=widgets[type];return widgetLogic?.version||{major:0,minor:0}}const getPublicWidgetOptionsFunction=name=>{return widgets[name]?.getPublicWidgetOptions??(i=>i)};function getWidgetOptionsUpgrades(type){const widgetLogic=widgets[type];return widgetLogic?.widgetOptionsUpgrades||{}}function getDefaultWidgetOptions(type){const widgetLogic=widgets[type];return widgetLogic?.defaultWidgetOptions||{}}const getSupportedAlignments=type=>{const widgetLogic=widgets[type];if(!widgetLogic?.supportedAlignments?.[0]){return ["default"]}return widgetLogic?.supportedAlignments};const getDefaultAlignment=type=>{const widgetLogic=widgets[type];if(!widgetLogic?.defaultAlignment){return "block"}return widgetLogic.defaultAlignment};registerWidget("categorizer",categorizerWidgetLogic);registerWidget("cs-program",csProgramWidgetLogic);registerWidget("definition",definitionWidgetLogic);registerWidget("dropdown",dropdownWidgetLogic);registerWidget("explanation",explanationWidgetLogic);registerWidget("expression",expressionWidgetLogic);registerWidget("graded-group",gradedGroupWidgetLogic);registerWidget("graded-group-set",gradedGroupSetWidgetLogic);registerWidget("grapher",grapherWidgetLogic);registerWidget("group",groupWidgetLogic);registerWidget("iframe",iframeWidgetLogic);registerWidget("image",imageWidgetLogic);registerWidget("input-number",inputNumberWidgetLogic);registerWidget("interaction",interactionWidgetLogic);registerWidget("interactive-graph",interactiveGraphWidgetLogic);registerWidget("label-image",labelImageWidgetLogic);registerWidget("matcher",matcherWidgetLogic);registerWidget("matrix",matrixWidgetLogic);registerWidget("measurer",measurerWidgetLogic);registerWidget("number-line",numberLineWidgetLogic);registerWidget("numeric-input",numericInputWidgetLogic);registerWidget("orderer",ordererWidgetLogic);registerWidget("passage",passageWidgetLogic);registerWidget("passage-ref",passageRefWidgetLogic);registerWidget("passage-ref-target",passageRefTargetWidgetLogic);registerWidget("phet-simulation",phetSimulationWidgetLogic);registerWidget("plotter",plotterWidgetLogic);registerWidget("python-program",pythonProgramWidgetLogic);registerWidget("radio",radioWidgetLogic);registerWidget("sorter",sorterWidgetLogic);registerWidget("table",tableWidgetLogic);registerWidget("video",videoWidgetLogic);
|
|
3994
314
|
|
|
3995
315
|
var coreWidgetRegistry = /*#__PURE__*/Object.freeze({
|
|
3996
316
|
__proto__: null,
|
|
@@ -4003,155 +323,9 @@ var coreWidgetRegistry = /*#__PURE__*/Object.freeze({
|
|
|
4003
323
|
isWidgetRegistered: isWidgetRegistered
|
|
4004
324
|
});
|
|
4005
325
|
|
|
4006
|
-
const DEFAULT_STATIC =
|
|
4007
|
-
const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
|
|
4008
|
-
const type = oldWidgetInfo.type;
|
|
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)) {
|
|
4014
|
-
throw new PerseusError("widget type must be a string, but was: " + type, Errors.Internal);
|
|
4015
|
-
}
|
|
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
|
|
4019
|
-
return oldWidgetInfo;
|
|
4020
|
-
}
|
|
4021
|
-
|
|
4022
|
-
// Unversioned widgets (pre-July 2014) are all implicitly 0.0
|
|
4023
|
-
const initialVersion = oldWidgetInfo.version || {
|
|
4024
|
-
major: 0,
|
|
4025
|
-
minor: 0
|
|
4026
|
-
};
|
|
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.
|
|
4032
|
-
if (initialVersion.major > latestVersion.major || initialVersion.major === latestVersion.major && initialVersion.minor > latestVersion.minor) {
|
|
4033
|
-
return oldWidgetInfo;
|
|
4034
|
-
}
|
|
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) || {};
|
|
4041
|
-
const upgradePropsMap = getWidgetOptionsUpgrades(type);
|
|
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).
|
|
4055
|
-
for (let nextVersion = initialVersion.major + 1; nextVersion <= latestVersion.major; nextVersion++) {
|
|
4056
|
-
if (upgradePropsMap[String(nextVersion)]) {
|
|
4057
|
-
newEditorOptions = upgradePropsMap[String(nextVersion)](newEditorOptions);
|
|
4058
|
-
} else {
|
|
4059
|
-
throw new PerseusError("No upgrade found for widget. Cannot render.", Errors.Internal, {
|
|
4060
|
-
metadata: {
|
|
4061
|
-
type,
|
|
4062
|
-
fromMajorVersion: nextVersion - 1,
|
|
4063
|
-
toMajorVersion: nextVersion,
|
|
4064
|
-
oldWidgetInfo: JSON.stringify(oldWidgetInfo)
|
|
4065
|
-
}
|
|
4066
|
-
});
|
|
4067
|
-
}
|
|
4068
|
-
}
|
|
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.
|
|
4074
|
-
const defaultOptions = getDefaultWidgetOptions(type);
|
|
4075
|
-
newEditorOptions = {
|
|
4076
|
-
...defaultOptions,
|
|
4077
|
-
...newEditorOptions
|
|
4078
|
-
};
|
|
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".
|
|
4085
|
-
if (alignment == null || alignment === "default") {
|
|
4086
|
-
alignment = getSupportedAlignments(type)?.[0];
|
|
4087
|
-
if (!alignment) {
|
|
4088
|
-
throw new PerseusError("No default alignment found when upgrading widget", Errors.Internal, {
|
|
4089
|
-
metadata: {
|
|
4090
|
-
widgetType: type
|
|
4091
|
-
}
|
|
4092
|
-
});
|
|
4093
|
-
}
|
|
4094
|
-
}
|
|
4095
|
-
let widgetStatic = oldWidgetInfo.static;
|
|
4096
|
-
if (widgetStatic == null) {
|
|
4097
|
-
widgetStatic = DEFAULT_STATIC;
|
|
4098
|
-
}
|
|
4099
|
-
return {
|
|
4100
|
-
...oldWidgetInfo,
|
|
4101
|
-
// maintain other info, like type
|
|
4102
|
-
// After upgrading we guarantee that the version is up-to-date
|
|
4103
|
-
version: latestVersion,
|
|
4104
|
-
// Default graded to true (so null/undefined becomes true):
|
|
4105
|
-
graded: oldWidgetInfo.graded != null ? oldWidgetInfo.graded : true,
|
|
4106
|
-
alignment: alignment,
|
|
4107
|
-
static: widgetStatic,
|
|
4108
|
-
options: newEditorOptions
|
|
4109
|
-
};
|
|
4110
|
-
};
|
|
4111
|
-
function getUpgradedWidgetOptions(oldWidgetOptions) {
|
|
4112
|
-
return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
|
|
4113
|
-
if (!widgetInfo.type || !widgetInfo.alignment) {
|
|
4114
|
-
const newValues = {};
|
|
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
|
|
4119
|
-
newValues.type = widgetId.split(" ")[0];
|
|
4120
|
-
}
|
|
4121
|
-
if (!widgetInfo.alignment) {
|
|
4122
|
-
newValues.alignment = "default";
|
|
4123
|
-
}
|
|
4124
|
-
widgetInfo = {
|
|
4125
|
-
...widgetInfo,
|
|
4126
|
-
...newValues
|
|
4127
|
-
};
|
|
4128
|
-
}
|
|
4129
|
-
return upgradeWidgetInfoToLatestVersion(widgetInfo);
|
|
4130
|
-
});
|
|
4131
|
-
}
|
|
326
|
+
const DEFAULT_STATIC=false;const upgradeWidgetInfoToLatestVersion=oldWidgetInfo=>{const type=oldWidgetInfo.type;if(!___default.default.isString(type)){throw new PerseusError("widget type must be a string, but was: "+type,Errors.Internal)}if(!isWidgetRegistered(type)){return oldWidgetInfo}const initialVersion=oldWidgetInfo.version||{major:0,minor:0};const latestVersion=getCurrentVersion(type);if(initialVersion.major>latestVersion.major||initialVersion.major===latestVersion.major&&initialVersion.minor>latestVersion.minor){return oldWidgetInfo}let newEditorOptions=___default.default.clone(oldWidgetInfo.options)||{};const upgradePropsMap=getWidgetOptionsUpgrades(type);if(___default.default.keys(newEditorOptions).length!==0){for(let nextVersion=initialVersion.major+1;nextVersion<=latestVersion.major;nextVersion++){if(upgradePropsMap[String(nextVersion)]){newEditorOptions=upgradePropsMap[String(nextVersion)](newEditorOptions);}else {throw new PerseusError("No upgrade found for widget. Cannot render.",Errors.Internal,{metadata:{type,fromMajorVersion:nextVersion-1,toMajorVersion:nextVersion,oldWidgetInfo:JSON.stringify(oldWidgetInfo)}})}}}const defaultOptions=getDefaultWidgetOptions(type);newEditorOptions={...defaultOptions,...newEditorOptions};let alignment=oldWidgetInfo.alignment;if(alignment==null||alignment==="default"){alignment=getSupportedAlignments(type)?.[0];if(!alignment){throw new PerseusError("No default alignment found when upgrading widget",Errors.Internal,{metadata:{widgetType:type}})}}let widgetStatic=oldWidgetInfo.static;if(widgetStatic==null){widgetStatic=DEFAULT_STATIC;}return {...oldWidgetInfo,version:latestVersion,graded:oldWidgetInfo.graded!=null?oldWidgetInfo.graded:true,alignment:alignment,static:widgetStatic,options:newEditorOptions}};function getUpgradedWidgetOptions(oldWidgetOptions){return mapObject(oldWidgetOptions,(widgetInfo,widgetId)=>{if(!widgetInfo.type||!widgetInfo.alignment){const newValues={};if(!widgetInfo.type){newValues.type=widgetId.split(" ")[0];}if(!widgetInfo.alignment){newValues.alignment="default";}widgetInfo={...widgetInfo,...newValues};}return upgradeWidgetInfoToLatestVersion(widgetInfo)})}
|
|
4132
327
|
|
|
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
|
-
*/
|
|
4138
|
-
function splitPerseusItem(originalItem) {
|
|
4139
|
-
const item = ___default.default.clone(originalItem);
|
|
4140
|
-
const originalWidgets = item.widgets ?? {};
|
|
4141
|
-
const upgradedWidgets = getUpgradedWidgetOptions(originalWidgets);
|
|
4142
|
-
const splitWidgets = {};
|
|
4143
|
-
for (const [id, widget] of Object.entries(upgradedWidgets)) {
|
|
4144
|
-
const publicWidgetOptionsFun = getPublicWidgetOptionsFunction(widget.type);
|
|
4145
|
-
splitWidgets[id] = {
|
|
4146
|
-
...widget,
|
|
4147
|
-
options: publicWidgetOptionsFun(widget.options)
|
|
4148
|
-
};
|
|
4149
|
-
}
|
|
4150
|
-
return {
|
|
4151
|
-
...item,
|
|
4152
|
-
widgets: splitWidgets
|
|
4153
|
-
};
|
|
4154
|
-
}
|
|
328
|
+
function splitPerseusItem(originalItem){const item=___default.default.clone(originalItem);const originalWidgets=item.widgets??{};const upgradedWidgets=getUpgradedWidgetOptions(originalWidgets);const splitWidgets={};for(const[id,widget]of Object.entries(upgradedWidgets)){const publicWidgetOptionsFun=getPublicWidgetOptionsFunction(widget.type);splitWidgets[id]={...widget,options:publicWidgetOptionsFun(widget.options)};}return {...item,widgets:splitWidgets}}
|
|
4155
329
|
|
|
4156
330
|
exports.CoreWidgetRegistry = coreWidgetRegistry;
|
|
4157
331
|
exports.Errors = Errors;
|