@khanacademy/perseus 45.0.0 → 46.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/text-input.d.ts +1 -0
- package/dist/es/index.css +169 -12
- package/dist/es/index.js +413 -290
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +169 -12
- package/dist/index.d.ts +5 -1
- package/dist/index.js +391 -264
- package/dist/index.js.map +1 -1
- package/dist/interactive2/objective_.d.ts +27 -6
- package/dist/perseus-types.d.ts +123 -72
- package/dist/renderer.d.ts +7 -2
- package/dist/types.d.ts +58 -17
- package/dist/util/parse-perseus-json/general-purpose-parsers/string-to-number.d.ts +2 -0
- package/dist/util/scoring.d.ts +20 -0
- package/dist/util.d.ts +0 -30
- package/dist/validation.types.d.ts +8 -3
- package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +1 -1
- package/dist/widgets/group/group.d.ts +2 -0
- package/dist/widgets/group/score-group.d.ts +5 -0
- package/dist/widgets/input-number/input-number.d.ts +1 -1
- package/dist/widgets/interactive-graph.d.ts +96 -3
- package/dist/widgets/interactive-graphs/mafs-graph.d.ts +2 -2
- package/dist/widgets/interactive-graphs/stateful-mafs-graph.d.ts +3 -3
- package/dist/widgets/number-line/score-number-line.d.ts +2 -2
- package/dist/widgets/number-line/validate-number-line.d.ts +12 -0
- package/dist/widgets/numeric-input/numeric-input.d.ts +7 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2915,12 +2915,6 @@ const rWidgetRule = /^\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]/;
|
|
|
2915
2915
|
const rTypeFromWidgetId = /^([a-z-]+) ([0-9]+)$/;
|
|
2916
2916
|
const rWidgetParts = new RegExp(rWidgetRule.source + "$");
|
|
2917
2917
|
const snowman = "\u2603";
|
|
2918
|
-
const noScore = {
|
|
2919
|
-
type: "points",
|
|
2920
|
-
earned: 0,
|
|
2921
|
-
total: 0,
|
|
2922
|
-
message: null
|
|
2923
|
-
};
|
|
2924
2918
|
const seededRNG$1 = function (seed) {
|
|
2925
2919
|
let randomSeed = seed;
|
|
2926
2920
|
return function () {
|
|
@@ -2998,97 +2992,6 @@ const split = "x".split(/(.)/g).length ? function (str, r) {
|
|
|
2998
2992
|
return output;
|
|
2999
2993
|
};
|
|
3000
2994
|
|
|
3001
|
-
/**
|
|
3002
|
-
* Combine two score objects.
|
|
3003
|
-
*
|
|
3004
|
-
* Given two score objects for two different widgets, combine them so that
|
|
3005
|
-
* if one is wrong, the total score is wrong, etc.
|
|
3006
|
-
*/
|
|
3007
|
-
function combineScores(scoreA, scoreB) {
|
|
3008
|
-
let message;
|
|
3009
|
-
if (scoreA.type === "points" && scoreB.type === "points") {
|
|
3010
|
-
if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
|
|
3011
|
-
// TODO(alpert): Figure out how to combine messages usefully
|
|
3012
|
-
message = null;
|
|
3013
|
-
} else {
|
|
3014
|
-
message = scoreA.message || scoreB.message;
|
|
3015
|
-
}
|
|
3016
|
-
return {
|
|
3017
|
-
type: "points",
|
|
3018
|
-
earned: scoreA.earned + scoreB.earned,
|
|
3019
|
-
total: scoreA.total + scoreB.total,
|
|
3020
|
-
message: message
|
|
3021
|
-
};
|
|
3022
|
-
}
|
|
3023
|
-
if (scoreA.type === "points" && scoreB.type === "invalid") {
|
|
3024
|
-
return scoreB;
|
|
3025
|
-
}
|
|
3026
|
-
if (scoreA.type === "invalid" && scoreB.type === "points") {
|
|
3027
|
-
return scoreA;
|
|
3028
|
-
}
|
|
3029
|
-
if (scoreA.type === "invalid" && scoreB.type === "invalid") {
|
|
3030
|
-
if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
|
|
3031
|
-
// TODO(alpert): Figure out how to combine messages usefully
|
|
3032
|
-
message = null;
|
|
3033
|
-
} else {
|
|
3034
|
-
message = scoreA.message || scoreB.message;
|
|
3035
|
-
}
|
|
3036
|
-
return {
|
|
3037
|
-
type: "invalid",
|
|
3038
|
-
message: message
|
|
3039
|
-
};
|
|
3040
|
-
}
|
|
3041
|
-
|
|
3042
|
-
/**
|
|
3043
|
-
* The above checks cover all combinations of score type, so if we get here
|
|
3044
|
-
* then something is amiss with our inputs.
|
|
3045
|
-
*/
|
|
3046
|
-
throw new perseusCore.PerseusError("PerseusScore with unknown type encountered", perseusCore.Errors.InvalidInput, {
|
|
3047
|
-
metadata: {
|
|
3048
|
-
scoreA: JSON.stringify(scoreA),
|
|
3049
|
-
scoreB: JSON.stringify(scoreB)
|
|
3050
|
-
}
|
|
3051
|
-
});
|
|
3052
|
-
}
|
|
3053
|
-
function flattenScores(widgetScoreMap) {
|
|
3054
|
-
return Object.values(widgetScoreMap).reduce(combineScores, noScore);
|
|
3055
|
-
}
|
|
3056
|
-
function isCorrect(score) {
|
|
3057
|
-
return score.type === "points" && score.earned >= score.total;
|
|
3058
|
-
}
|
|
3059
|
-
function keScoreFromPerseusScore(score,
|
|
3060
|
-
// It's weird, but this is what we're passing it
|
|
3061
|
-
guess, state) {
|
|
3062
|
-
if (score.type === "points") {
|
|
3063
|
-
return {
|
|
3064
|
-
empty: false,
|
|
3065
|
-
correct: isCorrect(score),
|
|
3066
|
-
message: score.message,
|
|
3067
|
-
guess: guess,
|
|
3068
|
-
state: state
|
|
3069
|
-
};
|
|
3070
|
-
}
|
|
3071
|
-
if (score.type === "invalid") {
|
|
3072
|
-
return {
|
|
3073
|
-
empty: true,
|
|
3074
|
-
correct: false,
|
|
3075
|
-
message: score.message,
|
|
3076
|
-
suppressAlmostThere: score.suppressAlmostThere,
|
|
3077
|
-
guess: guess,
|
|
3078
|
-
state: state
|
|
3079
|
-
};
|
|
3080
|
-
}
|
|
3081
|
-
throw new perseusCore.PerseusError(
|
|
3082
|
-
// @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
|
|
3083
|
-
"Invalid score type: " + score.type, perseusCore.Errors.InvalidInput, {
|
|
3084
|
-
metadata: {
|
|
3085
|
-
score: JSON.stringify(score),
|
|
3086
|
-
guess: JSON.stringify(guess),
|
|
3087
|
-
state: JSON.stringify(state)
|
|
3088
|
-
}
|
|
3089
|
-
});
|
|
3090
|
-
}
|
|
3091
|
-
|
|
3092
2995
|
/**
|
|
3093
2996
|
* Return the first valid interpretation of 'text' as a number, in the form
|
|
3094
2997
|
* {value: 2.3, exact: true}.
|
|
@@ -3386,28 +3289,6 @@ function strongEncodeURIComponent(str) {
|
|
|
3386
3289
|
.replace(/\*/g, "%2A");
|
|
3387
3290
|
}
|
|
3388
3291
|
|
|
3389
|
-
/**
|
|
3390
|
-
* If a widget says that it is empty once it is graded.
|
|
3391
|
-
* Trying to encapsulate references to the score format.
|
|
3392
|
-
*/
|
|
3393
|
-
function scoreIsEmpty(score) {
|
|
3394
|
-
// HACK(benkomalo): ugh. this isn't great; the Perseus score objects
|
|
3395
|
-
// overload the type "invalid" for what should probably be three
|
|
3396
|
-
// distinct cases:
|
|
3397
|
-
// - truly empty or not fully filled out
|
|
3398
|
-
// - invalid or malformed inputs
|
|
3399
|
-
// - "almost correct" like inputs where the widget wants to give
|
|
3400
|
-
// feedback (e.g. a fraction needs to be reduced, or `pi` should
|
|
3401
|
-
// be used instead of 3.14)
|
|
3402
|
-
//
|
|
3403
|
-
// Unfortunately the coercion happens all over the place, as these
|
|
3404
|
-
// Perseus style score objects are created *everywhere* (basically
|
|
3405
|
-
// in every widget), so it's hard to change now. We assume that
|
|
3406
|
-
// anything with a "message" is not truly empty, and one of the
|
|
3407
|
-
// latter two cases for now.
|
|
3408
|
-
return score.type === "invalid" && (!score.message || score.message.length === 0);
|
|
3409
|
-
}
|
|
3410
|
-
|
|
3411
3292
|
/*
|
|
3412
3293
|
* The touchHandlers are used to track the current state of the touch
|
|
3413
3294
|
* event, such as whether or not the user is currently pressed down (either
|
|
@@ -3580,13 +3461,9 @@ const Util = {
|
|
|
3580
3461
|
rTypeFromWidgetId,
|
|
3581
3462
|
rWidgetParts,
|
|
3582
3463
|
snowman,
|
|
3583
|
-
noScore,
|
|
3584
3464
|
seededRNG: seededRNG$1,
|
|
3585
3465
|
shuffle: shuffle$3,
|
|
3586
3466
|
split,
|
|
3587
|
-
combineScores,
|
|
3588
|
-
flattenScores,
|
|
3589
|
-
keScoreFromPerseusScore,
|
|
3590
3467
|
firstNumericalParse: firstNumericalParse$1,
|
|
3591
3468
|
stringArrayOfSize: stringArrayOfSize$1,
|
|
3592
3469
|
gridDimensionConfig,
|
|
@@ -3602,7 +3479,6 @@ const Util = {
|
|
|
3602
3479
|
parseQueryString,
|
|
3603
3480
|
updateQueryString: updateQueryString$2,
|
|
3604
3481
|
strongEncodeURIComponent,
|
|
3605
|
-
scoreIsEmpty,
|
|
3606
3482
|
touchHandlers,
|
|
3607
3483
|
resetTouchHandlers,
|
|
3608
3484
|
extractPointerLocation,
|
|
@@ -3918,6 +3794,7 @@ Zoom.prototype.zoomImage = function () {
|
|
|
3918
3794
|
this._zoomOriginal();
|
|
3919
3795
|
}.bind(this);
|
|
3920
3796
|
img.src = this._targetImage.src;
|
|
3797
|
+
img.alt = this._targetImage.alt;
|
|
3921
3798
|
this.$zoomedImage = $zoomedImage;
|
|
3922
3799
|
};
|
|
3923
3800
|
Zoom.prototype._zoomOriginal = function () {
|
|
@@ -8993,7 +8870,10 @@ var Tex$1 = {
|
|
|
8993
8870
|
// We use createElement instead of JSX here because we can't name this file tex.tsx;
|
|
8994
8871
|
// that name is already taken.
|
|
8995
8872
|
// eslint-disable-next-line import/no-deprecated
|
|
8996
|
-
render(
|
|
8873
|
+
render(
|
|
8874
|
+
/*#__PURE__*/
|
|
8875
|
+
// eslint-disable-next-line react/no-children-prop
|
|
8876
|
+
React__namespace.createElement(TeX, {
|
|
8997
8877
|
children: text,
|
|
8998
8878
|
onRender: callback
|
|
8999
8879
|
}), $texHolder[0]);
|
|
@@ -14757,7 +14637,7 @@ const pluck = function (table, subKey) {
|
|
|
14757
14637
|
* });
|
|
14758
14638
|
* {a: 2, b: 3}
|
|
14759
14639
|
*/
|
|
14760
|
-
const mapObject
|
|
14640
|
+
const mapObject = function (obj, lambda) {
|
|
14761
14641
|
const result = {};
|
|
14762
14642
|
Object.keys(obj).forEach(key => {
|
|
14763
14643
|
// @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
|
|
@@ -14766,27 +14646,6 @@ const mapObject$2 = function (obj, lambda) {
|
|
|
14766
14646
|
return result;
|
|
14767
14647
|
};
|
|
14768
14648
|
|
|
14769
|
-
/**
|
|
14770
|
-
* Maps an array to an object
|
|
14771
|
-
*
|
|
14772
|
-
* > mapObjectFromArray(['a', 'b'], function(elem) {
|
|
14773
|
-
* return elem + elem;
|
|
14774
|
-
* });
|
|
14775
|
-
* {a: 'aa', b: 'bb'}
|
|
14776
|
-
*/
|
|
14777
|
-
const mapObjectFromArray = function (arr, lambda) {
|
|
14778
|
-
const result = {};
|
|
14779
|
-
arr.forEach(elem => {
|
|
14780
|
-
result[elem] = lambda(elem);
|
|
14781
|
-
});
|
|
14782
|
-
return result;
|
|
14783
|
-
};
|
|
14784
|
-
var objective_ = {
|
|
14785
|
-
pluck: pluck,
|
|
14786
|
-
mapObject: mapObject$2,
|
|
14787
|
-
mapObjectFromArray: mapObjectFromArray
|
|
14788
|
-
};
|
|
14789
|
-
|
|
14790
14649
|
/**
|
|
14791
14650
|
* MovableLine
|
|
14792
14651
|
*/
|
|
@@ -14851,7 +14710,7 @@ ___default["default"].extend(MovableLine$4.prototype, {
|
|
|
14851
14710
|
// - are objects, not primitives (and need a deeper copy)
|
|
14852
14711
|
// - they don't need getters created for them
|
|
14853
14712
|
// TODO(jack): Consider "default" once we es3ify perseus
|
|
14854
|
-
|
|
14713
|
+
pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
|
|
14855
14714
|
},
|
|
14856
14715
|
/**
|
|
14857
14716
|
* Resets the object to its state as if it were constructed with
|
|
@@ -15358,7 +15217,7 @@ class MovablePoint$5 {
|
|
|
15358
15217
|
// - are objects, not primitives (and need a deeper copy)
|
|
15359
15218
|
// - they don't need getters created for them
|
|
15360
15219
|
// TODO(jack): Consider "default" once we es3ify perseus
|
|
15361
|
-
|
|
15220
|
+
pluck(MovablePointOptions, "standard")
|
|
15362
15221
|
// We only update props here, because we want things on state to
|
|
15363
15222
|
// be persistent, and updated appropriately in modify()
|
|
15364
15223
|
),
|
|
@@ -16259,7 +16118,7 @@ ___default["default"].extend(MovablePolygon.prototype, {
|
|
|
16259
16118
|
// - are objects, not primitives (and need a deeper copy)
|
|
16260
16119
|
// - they don't need getters created for them
|
|
16261
16120
|
// TODO(jack): Consider "default" once we es3ify perseus
|
|
16262
|
-
|
|
16121
|
+
pluck(MovablePolygonOptions, "standard")
|
|
16263
16122
|
|
|
16264
16123
|
// We only update props here, because we want things on state to
|
|
16265
16124
|
// be persistent, and updated appropriately in modify()
|
|
@@ -18919,6 +18778,126 @@ class QuestionParagraph extends React__namespace.Component {
|
|
|
18919
18778
|
}
|
|
18920
18779
|
}
|
|
18921
18780
|
|
|
18781
|
+
const noScore = {
|
|
18782
|
+
type: "points",
|
|
18783
|
+
earned: 0,
|
|
18784
|
+
total: 0,
|
|
18785
|
+
message: null
|
|
18786
|
+
};
|
|
18787
|
+
|
|
18788
|
+
/**
|
|
18789
|
+
* If a widget says that it is empty once it is graded.
|
|
18790
|
+
* Trying to encapsulate references to the score format.
|
|
18791
|
+
*/
|
|
18792
|
+
function scoreIsEmpty(score) {
|
|
18793
|
+
// HACK(benkomalo): ugh. this isn't great; the Perseus score objects
|
|
18794
|
+
// overload the type "invalid" for what should probably be three
|
|
18795
|
+
// distinct cases:
|
|
18796
|
+
// - truly empty or not fully filled out
|
|
18797
|
+
// - invalid or malformed inputs
|
|
18798
|
+
// - "almost correct" like inputs where the widget wants to give
|
|
18799
|
+
// feedback (e.g. a fraction needs to be reduced, or `pi` should
|
|
18800
|
+
// be used instead of 3.14)
|
|
18801
|
+
//
|
|
18802
|
+
// Unfortunately the coercion happens all over the place, as these
|
|
18803
|
+
// Perseus style score objects are created *everywhere* (basically
|
|
18804
|
+
// in every widget), so it's hard to change now. We assume that
|
|
18805
|
+
// anything with a "message" is not truly empty, and one of the
|
|
18806
|
+
// latter two cases for now.
|
|
18807
|
+
return score.type === "invalid" && (!score.message || score.message.length === 0);
|
|
18808
|
+
}
|
|
18809
|
+
|
|
18810
|
+
/**
|
|
18811
|
+
* Combine two score objects.
|
|
18812
|
+
*
|
|
18813
|
+
* Given two score objects for two different widgets, combine them so that
|
|
18814
|
+
* if one is wrong, the total score is wrong, etc.
|
|
18815
|
+
*/
|
|
18816
|
+
function combineScores(scoreA, scoreB) {
|
|
18817
|
+
let message;
|
|
18818
|
+
if (scoreA.type === "points" && scoreB.type === "points") {
|
|
18819
|
+
if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
|
|
18820
|
+
// TODO(alpert): Figure out how to combine messages usefully
|
|
18821
|
+
message = null;
|
|
18822
|
+
} else {
|
|
18823
|
+
message = scoreA.message || scoreB.message;
|
|
18824
|
+
}
|
|
18825
|
+
return {
|
|
18826
|
+
type: "points",
|
|
18827
|
+
earned: scoreA.earned + scoreB.earned,
|
|
18828
|
+
total: scoreA.total + scoreB.total,
|
|
18829
|
+
message: message
|
|
18830
|
+
};
|
|
18831
|
+
}
|
|
18832
|
+
if (scoreA.type === "points" && scoreB.type === "invalid") {
|
|
18833
|
+
return scoreB;
|
|
18834
|
+
}
|
|
18835
|
+
if (scoreA.type === "invalid" && scoreB.type === "points") {
|
|
18836
|
+
return scoreA;
|
|
18837
|
+
}
|
|
18838
|
+
if (scoreA.type === "invalid" && scoreB.type === "invalid") {
|
|
18839
|
+
if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
|
|
18840
|
+
// TODO(alpert): Figure out how to combine messages usefully
|
|
18841
|
+
message = null;
|
|
18842
|
+
} else {
|
|
18843
|
+
message = scoreA.message || scoreB.message;
|
|
18844
|
+
}
|
|
18845
|
+
return {
|
|
18846
|
+
type: "invalid",
|
|
18847
|
+
message: message
|
|
18848
|
+
};
|
|
18849
|
+
}
|
|
18850
|
+
|
|
18851
|
+
/**
|
|
18852
|
+
* The above checks cover all combinations of score type, so if we get here
|
|
18853
|
+
* then something is amiss with our inputs.
|
|
18854
|
+
*/
|
|
18855
|
+
throw new perseusCore.PerseusError("PerseusScore with unknown type encountered", perseusCore.Errors.InvalidInput, {
|
|
18856
|
+
metadata: {
|
|
18857
|
+
scoreA: JSON.stringify(scoreA),
|
|
18858
|
+
scoreB: JSON.stringify(scoreB)
|
|
18859
|
+
}
|
|
18860
|
+
});
|
|
18861
|
+
}
|
|
18862
|
+
function flattenScores(widgetScoreMap) {
|
|
18863
|
+
return Object.values(widgetScoreMap).reduce(combineScores, noScore);
|
|
18864
|
+
}
|
|
18865
|
+
function isCorrect(score) {
|
|
18866
|
+
return score.type === "points" && score.earned >= score.total;
|
|
18867
|
+
}
|
|
18868
|
+
function keScoreFromPerseusScore(score,
|
|
18869
|
+
// It's weird, but this is what we're passing it
|
|
18870
|
+
guess, state) {
|
|
18871
|
+
if (score.type === "points") {
|
|
18872
|
+
return {
|
|
18873
|
+
empty: false,
|
|
18874
|
+
correct: isCorrect(score),
|
|
18875
|
+
message: score.message,
|
|
18876
|
+
guess: guess,
|
|
18877
|
+
state: state
|
|
18878
|
+
};
|
|
18879
|
+
}
|
|
18880
|
+
if (score.type === "invalid") {
|
|
18881
|
+
return {
|
|
18882
|
+
empty: true,
|
|
18883
|
+
correct: false,
|
|
18884
|
+
message: score.message,
|
|
18885
|
+
suppressAlmostThere: score.suppressAlmostThere,
|
|
18886
|
+
guess: guess,
|
|
18887
|
+
state: state
|
|
18888
|
+
};
|
|
18889
|
+
}
|
|
18890
|
+
throw new perseusCore.PerseusError(
|
|
18891
|
+
// @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
|
|
18892
|
+
"Invalid score type: " + score.type, perseusCore.Errors.InvalidInput, {
|
|
18893
|
+
metadata: {
|
|
18894
|
+
score: JSON.stringify(score),
|
|
18895
|
+
guess: JSON.stringify(guess),
|
|
18896
|
+
state: JSON.stringify(state)
|
|
18897
|
+
}
|
|
18898
|
+
});
|
|
18899
|
+
}
|
|
18900
|
+
|
|
18922
18901
|
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
18923
18902
|
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
18924
18903
|
|
|
@@ -19481,12 +19460,9 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
|
|
|
19481
19460
|
isLintable: isLintable
|
|
19482
19461
|
});
|
|
19483
19462
|
|
|
19484
|
-
const {
|
|
19485
|
-
mapObject: mapObject$1
|
|
19486
|
-
} = objective_;
|
|
19487
19463
|
function getUpgradedWidgetOptions(oldWidgetOptions) {
|
|
19488
19464
|
// @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
|
|
19489
|
-
return mapObject
|
|
19465
|
+
return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
|
|
19490
19466
|
if (!widgetInfo.type || !widgetInfo.alignment) {
|
|
19491
19467
|
const newValues = {};
|
|
19492
19468
|
if (!widgetInfo.type) {
|
|
@@ -19517,17 +19493,10 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19517
19493
|
// Static widgets shouldn't count as empty
|
|
19518
19494
|
return false;
|
|
19519
19495
|
}
|
|
19520
|
-
let score = null;
|
|
19521
|
-
const userInput = userInputMap[id];
|
|
19522
19496
|
const scorer = getWidgetScorer(widget.type);
|
|
19523
|
-
|
|
19524
|
-
const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
|
|
19525
|
-
score = Util.flattenScores(scores);
|
|
19526
|
-
} else if (scorer) {
|
|
19527
|
-
score = scorer(userInput, widget.options, strings, locale);
|
|
19528
|
-
}
|
|
19497
|
+
const score = scorer?.(userInputMap[id], widget.options, strings, locale);
|
|
19529
19498
|
if (score) {
|
|
19530
|
-
return
|
|
19499
|
+
return scoreIsEmpty(score);
|
|
19531
19500
|
}
|
|
19532
19501
|
});
|
|
19533
19502
|
}
|
|
@@ -19543,7 +19512,7 @@ strings, locale) {
|
|
|
19543
19512
|
// so this checks that the widgets are being used before scoring them
|
|
19544
19513
|
const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
|
|
19545
19514
|
const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
|
|
19546
|
-
return
|
|
19515
|
+
return flattenScores(scores);
|
|
19547
19516
|
}
|
|
19548
19517
|
function scoreWidgetsFunctional(widgets,
|
|
19549
19518
|
// This is a port of old code, I'm not sure why
|
|
@@ -19565,11 +19534,9 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19565
19534
|
}
|
|
19566
19535
|
const userInput = userInputMap[id];
|
|
19567
19536
|
const scorer = getWidgetScorer(widget.type);
|
|
19568
|
-
|
|
19569
|
-
|
|
19570
|
-
widgetScores[id] =
|
|
19571
|
-
} else if (scorer) {
|
|
19572
|
-
widgetScores[id] = scorer(userInput, widget.options, strings, locale);
|
|
19537
|
+
const score = scorer?.(userInput, widget.options, strings, locale);
|
|
19538
|
+
if (score != null) {
|
|
19539
|
+
widgetScores[id] = score;
|
|
19573
19540
|
}
|
|
19574
19541
|
});
|
|
19575
19542
|
return widgetScores;
|
|
@@ -19876,9 +19843,6 @@ class WidgetContainer extends React__namespace.Component {
|
|
|
19876
19843
|
}
|
|
19877
19844
|
}
|
|
19878
19845
|
|
|
19879
|
-
const {
|
|
19880
|
-
mapObject
|
|
19881
|
-
} = objective_;
|
|
19882
19846
|
const rContainsNonWhitespace = /\S/;
|
|
19883
19847
|
const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
|
|
19884
19848
|
const noopOnRender = () => {};
|
|
@@ -20995,6 +20959,13 @@ class Renderer extends React__namespace.Component {
|
|
|
20995
20959
|
}, this);
|
|
20996
20960
|
return state;
|
|
20997
20961
|
};
|
|
20962
|
+
|
|
20963
|
+
/**
|
|
20964
|
+
* Returns an array of widget ids that are empty (meaning widgets where the
|
|
20965
|
+
* learner has not interacted with the widget yet or has not filled in all
|
|
20966
|
+
* fields). For example, the `interactive-graph` widget is considered
|
|
20967
|
+
* empty if the graph is in the starting state.
|
|
20968
|
+
*/
|
|
20998
20969
|
emptyWidgets() {
|
|
20999
20970
|
return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
21000
20971
|
}
|
|
@@ -21048,7 +21019,7 @@ class Renderer extends React__namespace.Component {
|
|
|
21048
21019
|
}, 0);
|
|
21049
21020
|
});
|
|
21050
21021
|
};
|
|
21051
|
-
setInputValue = (path, newValue,
|
|
21022
|
+
setInputValue = (path, newValue, cb) => {
|
|
21052
21023
|
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
21053
21024
|
const widgetId = ___default["default"].first(path);
|
|
21054
21025
|
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
@@ -21056,7 +21027,7 @@ class Renderer extends React__namespace.Component {
|
|
|
21056
21027
|
const widget = this.getWidgetInstance(widgetId);
|
|
21057
21028
|
|
|
21058
21029
|
// Widget handles parsing of the interWidgetPath.
|
|
21059
|
-
widget?.setInputValue?.(interWidgetPath, newValue,
|
|
21030
|
+
widget?.setInputValue?.(interWidgetPath, newValue, cb);
|
|
21060
21031
|
};
|
|
21061
21032
|
|
|
21062
21033
|
/**
|
|
@@ -21127,7 +21098,7 @@ class Renderer extends React__namespace.Component {
|
|
|
21127
21098
|
*/
|
|
21128
21099
|
score() {
|
|
21129
21100
|
const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
21130
|
-
const combinedScore =
|
|
21101
|
+
const combinedScore = flattenScores(scores);
|
|
21131
21102
|
return combinedScore;
|
|
21132
21103
|
}
|
|
21133
21104
|
|
|
@@ -21139,28 +21110,6 @@ class Renderer extends React__namespace.Component {
|
|
|
21139
21110
|
const totalScore = this.score();
|
|
21140
21111
|
return [totalGuess, totalScore];
|
|
21141
21112
|
};
|
|
21142
|
-
examples = () => {
|
|
21143
|
-
const widgetIds = this.widgetIds;
|
|
21144
|
-
const examples = widgetIds.map(widgetId => {
|
|
21145
|
-
const widget = this.getWidgetInstance(widgetId);
|
|
21146
|
-
return widget != null && widget.examples ? widget.examples() : null;
|
|
21147
|
-
}).filter(Boolean);
|
|
21148
|
-
|
|
21149
|
-
// no widgets with examples
|
|
21150
|
-
if (!examples.length) {
|
|
21151
|
-
return null;
|
|
21152
|
-
}
|
|
21153
|
-
const allEqual = ___default["default"].all(examples, function (example) {
|
|
21154
|
-
return ___default["default"].isEqual(examples[0], example);
|
|
21155
|
-
});
|
|
21156
|
-
|
|
21157
|
-
// some widgets have different examples
|
|
21158
|
-
// TODO(alex): handle this better
|
|
21159
|
-
if (!allEqual) {
|
|
21160
|
-
return null;
|
|
21161
|
-
}
|
|
21162
|
-
return examples[0];
|
|
21163
|
-
};
|
|
21164
21113
|
|
|
21165
21114
|
// TranslationLinter callback
|
|
21166
21115
|
handletranslationLintErrors = lintErrors => {
|
|
@@ -21354,6 +21303,7 @@ class TextInput extends React__namespace.Component {
|
|
|
21354
21303
|
value: formattedValue,
|
|
21355
21304
|
type: "text",
|
|
21356
21305
|
"aria-label": labelText,
|
|
21306
|
+
"aria-describedby": this.props["aria-describedby"],
|
|
21357
21307
|
onChange: value => this.props.onChange(value),
|
|
21358
21308
|
placeholder: placeholder,
|
|
21359
21309
|
onFocus: onFocus,
|
|
@@ -21612,6 +21562,7 @@ class Tooltip extends React__namespace.Component {
|
|
|
21612
21562
|
}, /*#__PURE__*/React__namespace.createElement("div", {
|
|
21613
21563
|
ref: this.tooltipContainerRef,
|
|
21614
21564
|
className: "tooltipContainer",
|
|
21565
|
+
role: "tooltip",
|
|
21615
21566
|
style: {
|
|
21616
21567
|
position: "absolute",
|
|
21617
21568
|
// height must start out undefined, not null, so that
|
|
@@ -21688,9 +21639,15 @@ class InputWithExamples extends React__namespace.Component {
|
|
|
21688
21639
|
};
|
|
21689
21640
|
_renderInput = () => {
|
|
21690
21641
|
const id = this._getUniqueId();
|
|
21642
|
+
const ariaId = `aria-for-${id}`;
|
|
21643
|
+
// Generate text from a known set of format options that will read well in a screen reader
|
|
21644
|
+
const examplesAria = this.props.examples.length === 7 ? "" : `${this.props.examples[0]}
|
|
21645
|
+
${this.props.examples.slice(1).join(", or\n")}`
|
|
21646
|
+
// @ts-expect-error TS2550: Property replaceAll does not exist on type string.
|
|
21647
|
+
.replaceAll("*", "").replaceAll("$", "").replaceAll("\\ \\text{pi}", " pi").replaceAll("\\ ", " and ");
|
|
21691
21648
|
const inputProps = {
|
|
21692
21649
|
id: id,
|
|
21693
|
-
"aria-describedby":
|
|
21650
|
+
"aria-describedby": ariaId,
|
|
21694
21651
|
ref: "input",
|
|
21695
21652
|
className: this._getInputClassName(),
|
|
21696
21653
|
labelText: this.props.labelText,
|
|
@@ -21706,7 +21663,12 @@ class InputWithExamples extends React__namespace.Component {
|
|
|
21706
21663
|
autoCorrect: "off",
|
|
21707
21664
|
spellCheck: "false"
|
|
21708
21665
|
};
|
|
21709
|
-
return /*#__PURE__*/React__namespace.createElement(TextInput, inputProps)
|
|
21666
|
+
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(TextInput, inputProps), /*#__PURE__*/React__namespace.createElement("span", {
|
|
21667
|
+
id: ariaId,
|
|
21668
|
+
style: {
|
|
21669
|
+
display: "none"
|
|
21670
|
+
}
|
|
21671
|
+
}, examplesAria));
|
|
21710
21672
|
};
|
|
21711
21673
|
_handleFocus = () => {
|
|
21712
21674
|
this.props.onFocus();
|
|
@@ -21747,8 +21709,12 @@ class InputWithExamples extends React__namespace.Component {
|
|
|
21747
21709
|
};
|
|
21748
21710
|
render() {
|
|
21749
21711
|
const input = this._renderInput();
|
|
21750
|
-
const examplesContent =
|
|
21751
|
-
|
|
21712
|
+
const examplesContent = this.props.examples.length <= 2 ? this.props.examples.join(" ") // A single item (with or without leading text) is not a "list"
|
|
21713
|
+
: this.props.examples // 2 or more items should display as a list
|
|
21714
|
+
.map((example, index) => {
|
|
21715
|
+
// If the first example is bold, then it is most likely a heading/leading text.
|
|
21716
|
+
// So, it shouldn't be part of the list.
|
|
21717
|
+
return index === 0 && example.startsWith("**") ? `${example}\n` : `- ${example}`;
|
|
21752
21718
|
}).join("\n");
|
|
21753
21719
|
const showExamples = this.props.shouldShowExamples && this.state.showExamples;
|
|
21754
21720
|
return /*#__PURE__*/React__namespace.createElement(Tooltip, {
|
|
@@ -22188,7 +22154,7 @@ class InputNumber extends React__namespace.Component {
|
|
|
22188
22154
|
getPromptJSON() {
|
|
22189
22155
|
return getPromptJSON$s(this.props, this.getUserInput());
|
|
22190
22156
|
}
|
|
22191
|
-
examples
|
|
22157
|
+
examples() {
|
|
22192
22158
|
const {
|
|
22193
22159
|
strings
|
|
22194
22160
|
} = this.context;
|
|
@@ -22196,7 +22162,7 @@ class InputNumber extends React__namespace.Component {
|
|
|
22196
22162
|
const forms = answerTypes[type].forms.split(/\s*,\s*/);
|
|
22197
22163
|
const examples = ___default["default"].map(forms, form => formExamples$1[form](this.props, strings));
|
|
22198
22164
|
return [strings.yourAnswer].concat(examples);
|
|
22199
|
-
}
|
|
22165
|
+
}
|
|
22200
22166
|
render() {
|
|
22201
22167
|
if (this.props.apiOptions.customKeypad) {
|
|
22202
22168
|
// TODO(charlie): Support "Review Mode".
|
|
@@ -22402,7 +22368,7 @@ function scoreNumericInput(userInput, rubric, strings) {
|
|
|
22402
22368
|
// We may have received TeX; try to parse it before grading.
|
|
22403
22369
|
// If `currentValue` is not TeX, this should be a no-op.
|
|
22404
22370
|
const currentValue = ParseTex(userInput.currentValue);
|
|
22405
|
-
const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => Math.abs(answer.value) <= 1);
|
|
22371
|
+
const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value !== undefined && Math.abs(answer.value) <= 1);
|
|
22406
22372
|
|
|
22407
22373
|
// The coefficient is an attribute of the widget
|
|
22408
22374
|
let localValue = currentValue;
|
|
@@ -22478,11 +22444,15 @@ class NumericInput extends React__namespace.Component {
|
|
|
22478
22444
|
state = {
|
|
22479
22445
|
// keeps track of the other set of values when switching
|
|
22480
22446
|
// between 0 and finite solutions
|
|
22481
|
-
previousValues: [""]
|
|
22447
|
+
previousValues: [""],
|
|
22448
|
+
isFocused: false
|
|
22482
22449
|
};
|
|
22483
22450
|
|
|
22484
|
-
|
|
22485
|
-
|
|
22451
|
+
/**
|
|
22452
|
+
* Generates a string that demonstrates how to input the various supported
|
|
22453
|
+
* answer forms.
|
|
22454
|
+
*/
|
|
22455
|
+
examples() {
|
|
22486
22456
|
// if the set of specified forms are empty, allow all forms
|
|
22487
22457
|
const forms = this.props.answerForms?.length !== 0 ? this.props.answerForms : Object.keys(formExamples).map(name => {
|
|
22488
22458
|
return {
|
|
@@ -22497,7 +22467,7 @@ class NumericInput extends React__namespace.Component {
|
|
|
22497
22467
|
// versions of the same format
|
|
22498
22468
|
examples = ___default["default"].uniq(examples);
|
|
22499
22469
|
return [this.context.strings.yourAnswer].concat(examples);
|
|
22500
|
-
}
|
|
22470
|
+
}
|
|
22501
22471
|
shouldShowExamples = () => {
|
|
22502
22472
|
const noFormsAccepted = this.props.answerForms?.length === 0;
|
|
22503
22473
|
// To check if all answer forms are accepted, we must first
|
|
@@ -22543,9 +22513,21 @@ class NumericInput extends React__namespace.Component {
|
|
|
22543
22513
|
};
|
|
22544
22514
|
_handleFocus = () => {
|
|
22545
22515
|
this.props.onFocus([]);
|
|
22516
|
+
this.setState(currentState => {
|
|
22517
|
+
return {
|
|
22518
|
+
...currentState,
|
|
22519
|
+
isFocused: true
|
|
22520
|
+
};
|
|
22521
|
+
});
|
|
22546
22522
|
};
|
|
22547
22523
|
_handleBlur = () => {
|
|
22548
22524
|
this.props.onBlur([]);
|
|
22525
|
+
this.setState(currentState => {
|
|
22526
|
+
return {
|
|
22527
|
+
...currentState,
|
|
22528
|
+
isFocused: false
|
|
22529
|
+
};
|
|
22530
|
+
});
|
|
22549
22531
|
};
|
|
22550
22532
|
render() {
|
|
22551
22533
|
let labelText = this.props.labelText;
|
|
@@ -22576,13 +22558,20 @@ class NumericInput extends React__namespace.Component {
|
|
|
22576
22558
|
// component.
|
|
22577
22559
|
const styles = aphrodite.StyleSheet.create({
|
|
22578
22560
|
input: {
|
|
22561
|
+
borderRadius: "3px",
|
|
22562
|
+
borderWidth: this.state.isFocused ? "2px" : "1px",
|
|
22563
|
+
display: "inline-block",
|
|
22564
|
+
fontFamily: `Symbola, "Times New Roman", serif`,
|
|
22565
|
+
fontSize: "18px",
|
|
22566
|
+
height: "32px",
|
|
22567
|
+
lineHeight: "18px",
|
|
22568
|
+
padding: this.state.isFocused ? "4px" : "4px 5px",
|
|
22569
|
+
// account for added focus border thickness
|
|
22579
22570
|
textAlign: this.props.rightAlign ? "right" : "left",
|
|
22580
|
-
width: this.props.size === "small" ? 40 : 80
|
|
22581
|
-
padding: 0,
|
|
22582
|
-
height: "auto"
|
|
22571
|
+
width: this.props.size === "small" ? 40 : 80
|
|
22583
22572
|
}
|
|
22584
22573
|
});
|
|
22585
|
-
return /*#__PURE__*/React__namespace.createElement(
|
|
22574
|
+
return /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
|
|
22586
22575
|
ref: ref => this.inputRef = ref,
|
|
22587
22576
|
value: this.props.currentValue,
|
|
22588
22577
|
onChange: this.handleChange,
|
|
@@ -22594,7 +22583,7 @@ class NumericInput extends React__namespace.Component {
|
|
|
22594
22583
|
id: this.props.widgetId,
|
|
22595
22584
|
disabled: this.props.apiOptions.readOnly,
|
|
22596
22585
|
style: styles.input
|
|
22597
|
-
})
|
|
22586
|
+
});
|
|
22598
22587
|
}
|
|
22599
22588
|
}
|
|
22600
22589
|
|
|
@@ -27522,8 +27511,8 @@ var Definition$1 = {
|
|
|
27522
27511
|
scorer: () => scoreNoop()
|
|
27523
27512
|
};
|
|
27524
27513
|
|
|
27525
|
-
//
|
|
27526
|
-
//
|
|
27514
|
+
// There are no required props, but this component can receive props from any
|
|
27515
|
+
// deprecated widget
|
|
27527
27516
|
class DeprecatedStandin extends React__default["default"].Component {
|
|
27528
27517
|
static contextType = PerseusI18nContext;
|
|
27529
27518
|
// this just helps with TS weak typing when a Widget
|
|
@@ -29646,6 +29635,13 @@ const getPromptJSON$f = rendererJSON => {
|
|
|
29646
29635
|
};
|
|
29647
29636
|
};
|
|
29648
29637
|
|
|
29638
|
+
// The `group` widget is basically a widget hosting a full Perseus system in
|
|
29639
|
+
// it. As such, scoring a group means scoring all widgets it contains.
|
|
29640
|
+
function scoreGroup(userInput, options, strings, locale) {
|
|
29641
|
+
const scores = scoreWidgetsFunctional(options.widgets, Object.keys(options.widgets), userInput, strings, locale);
|
|
29642
|
+
return flattenScores(scores);
|
|
29643
|
+
}
|
|
29644
|
+
|
|
29649
29645
|
// exports has no 'transform'
|
|
29650
29646
|
|
|
29651
29647
|
class Group extends React__namespace.Component {
|
|
@@ -29797,6 +29793,9 @@ var Group$1 = {
|
|
|
29797
29793
|
displayName: "Group (SAT only)",
|
|
29798
29794
|
widget: Group,
|
|
29799
29795
|
traverseChildWidgets: traverseChildWidgets,
|
|
29796
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
29797
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
|
|
29798
|
+
scorer: scoreGroup,
|
|
29800
29799
|
hidden: true,
|
|
29801
29800
|
isLintable: true
|
|
29802
29801
|
};
|
|
@@ -32540,6 +32539,61 @@ const LegacyGrid = _ref => {
|
|
|
32540
32539
|
|
|
32541
32540
|
// Range is replaced within this file with Interval, but it is used elsewhere
|
|
32542
32541
|
// and exported from the package, so we need to keep it around.
|
|
32542
|
+
/**
|
|
32543
|
+
* Our core set of Perseus widgets.
|
|
32544
|
+
*
|
|
32545
|
+
* This interface is the basis for "registering" all Perseus widget types.
|
|
32546
|
+
* There should be one key/value pair for each supported widget. If you create
|
|
32547
|
+
* a new widget, an entry should be added to this interface. Note that this
|
|
32548
|
+
* only registers the widget options type, you'll also need to register the
|
|
32549
|
+
* widget so that it's available at runtime (@see
|
|
32550
|
+
* {@link file://./widgets.ts#registerWidget}).
|
|
32551
|
+
*
|
|
32552
|
+
* Importantly, the key should be the name that is used in widget IDs. For most
|
|
32553
|
+
* widgets that is the same as the widget option's `type` field. In cases where
|
|
32554
|
+
* a widget has been deprecated and replaced with the deprecated-standin
|
|
32555
|
+
* widget, it should be the original widget type!
|
|
32556
|
+
*
|
|
32557
|
+
* If you define the widget outside of this package, you can still add the new
|
|
32558
|
+
* widget to this interface by writing the following in that package that
|
|
32559
|
+
* contains the widget. TypeScript will merge that definition of the
|
|
32560
|
+
* `PerseusWidgets` with the one defined below.
|
|
32561
|
+
*
|
|
32562
|
+
* ```typescript
|
|
32563
|
+
* declare module "@khanacademy/perseus" {
|
|
32564
|
+
* interface PerseusWidgetTypes {
|
|
32565
|
+
* // A new widget
|
|
32566
|
+
* "new-awesomeness": MyAwesomeNewWidget;
|
|
32567
|
+
*
|
|
32568
|
+
* // A deprecated widget
|
|
32569
|
+
* "super-old-widget": AutoCorrectWidget;
|
|
32570
|
+
* }
|
|
32571
|
+
* }
|
|
32572
|
+
*
|
|
32573
|
+
* // The new widget's options definition
|
|
32574
|
+
* type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>;
|
|
32575
|
+
*
|
|
32576
|
+
* // The deprecated widget's options definition
|
|
32577
|
+
* type SuperOldWidget = WidgetOptions<'super-old-widget', object>;
|
|
32578
|
+
* ```
|
|
32579
|
+
*
|
|
32580
|
+
* This interface can be extended through the magic of TypeScript "Declaration
|
|
32581
|
+
* merging". Specifically, we augment this module and extend this interface.
|
|
32582
|
+
*
|
|
32583
|
+
* @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
|
|
32584
|
+
*/
|
|
32585
|
+
/**
|
|
32586
|
+
* A map of widget IDs to widget options. This is most often used as the type
|
|
32587
|
+
* for a set of widgets defined in a `PerseusItem` but can also be useful to
|
|
32588
|
+
* represent a function parameter where only `widgets` from a `PerseusItem` are
|
|
32589
|
+
* needed. Today Widget IDs are made up of the widget type and an incrementing
|
|
32590
|
+
* integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid
|
|
32591
|
+
* reading/parsing the widget id to derive any information from it, except in
|
|
32592
|
+
* the case of this map.
|
|
32593
|
+
*
|
|
32594
|
+
* @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
|
|
32595
|
+
* by augmenting the PerseusWidgetTypes with new widget types!
|
|
32596
|
+
*/
|
|
32543
32597
|
/**
|
|
32544
32598
|
* A "PerseusItem" is a classic Perseus item. It is rendered by the
|
|
32545
32599
|
* `ServerItemRenderer` and the layout is pre-set.
|
|
@@ -32555,6 +32609,9 @@ const LegacyGrid = _ref => {
|
|
|
32555
32609
|
* A "MultiItem" is an advanced Perseus item. It is rendered by the
|
|
32556
32610
|
* `MultiRenderer` and you can control the layout of individual parts of the
|
|
32557
32611
|
* item.
|
|
32612
|
+
*
|
|
32613
|
+
* @deprecated MultiItem support is slated for removal in a future Perseus
|
|
32614
|
+
* release.
|
|
32558
32615
|
*/
|
|
32559
32616
|
const ItemExtras = [
|
|
32560
32617
|
// The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
|
|
@@ -32576,6 +32633,11 @@ const ItemExtras = [
|
|
|
32576
32633
|
// The user might benefit from using a statistics Z Table like https://www.ztable.net/
|
|
32577
32634
|
"zTable"];
|
|
32578
32635
|
|
|
32636
|
+
/**
|
|
32637
|
+
* The type representing the common structure of all widget's options. The
|
|
32638
|
+
* `Options` generic type represents the widget-specific option data.
|
|
32639
|
+
*/
|
|
32640
|
+
|
|
32579
32641
|
// prettier-ignore
|
|
32580
32642
|
|
|
32581
32643
|
// prettier-ignore
|
|
@@ -32644,7 +32706,9 @@ const ItemExtras = [
|
|
|
32644
32706
|
|
|
32645
32707
|
//prettier-ignore
|
|
32646
32708
|
|
|
32647
|
-
|
|
32709
|
+
/**
|
|
32710
|
+
* A background image applied to various widgets.
|
|
32711
|
+
*/
|
|
32648
32712
|
|
|
32649
32713
|
const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
|
|
32650
32714
|
|
|
@@ -37288,8 +37352,6 @@ const makeInvalidTypeError = (functionName, graphType) => {
|
|
|
37288
37352
|
});
|
|
37289
37353
|
};
|
|
37290
37354
|
|
|
37291
|
-
// There's no transform function in exports
|
|
37292
|
-
|
|
37293
37355
|
// TODO: there's another, very similar getSinusoidCoefficients function
|
|
37294
37356
|
// they should probably be merged
|
|
37295
37357
|
function getSinusoidCoefficients(coords) {
|
|
@@ -42407,23 +42469,37 @@ const getPromptJSON$6 = (renderProps, userInput) => {
|
|
|
42407
42469
|
};
|
|
42408
42470
|
};
|
|
42409
42471
|
|
|
42410
|
-
|
|
42411
|
-
|
|
42412
|
-
|
|
42413
|
-
|
|
42414
|
-
|
|
42415
|
-
|
|
42416
|
-
|
|
42417
|
-
const
|
|
42472
|
+
/**
|
|
42473
|
+
* Checks user input is within the allowed range and not the same as the initial
|
|
42474
|
+
* state.
|
|
42475
|
+
* @param userInput
|
|
42476
|
+
* @see 'scoreNumberLine' for the scoring logic.
|
|
42477
|
+
*/
|
|
42478
|
+
function validateNumberLine(userInput) {
|
|
42479
|
+
const divisionRange = userInput.divisionRange;
|
|
42480
|
+
const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
|
|
42418
42481
|
|
|
42419
42482
|
// TODO: I don't think isTickCrtl is a thing anymore
|
|
42420
|
-
if (
|
|
42483
|
+
if (userInput.isTickCrtl && outsideAllowedRange) {
|
|
42421
42484
|
return {
|
|
42422
42485
|
type: "invalid",
|
|
42423
42486
|
message: "Number of divisions is outside the allowed range."
|
|
42424
42487
|
};
|
|
42425
42488
|
}
|
|
42426
|
-
|
|
42489
|
+
return null;
|
|
42490
|
+
}
|
|
42491
|
+
|
|
42492
|
+
function scoreNumberLine(userInput, scoringData) {
|
|
42493
|
+
const validationError = validateNumberLine(userInput);
|
|
42494
|
+
if (validationError) {
|
|
42495
|
+
return validationError;
|
|
42496
|
+
}
|
|
42497
|
+
const range = scoringData.range;
|
|
42498
|
+
const start = scoringData.initialX != null ? scoringData.initialX : range[0];
|
|
42499
|
+
const startRel = scoringData.isInequality ? "ge" : "eq";
|
|
42500
|
+
const correctRel = scoringData.correctRel || "eq";
|
|
42501
|
+
const correctPos = kmath.number.equal(userInput.numLinePosition, scoringData.correctX || 0);
|
|
42502
|
+
if (correctPos && correctRel === userInput.rel) {
|
|
42427
42503
|
return {
|
|
42428
42504
|
type: "points",
|
|
42429
42505
|
earned: 1,
|
|
@@ -42431,7 +42507,7 @@ function scoreNumberLine(state, rubric) {
|
|
|
42431
42507
|
message: null
|
|
42432
42508
|
};
|
|
42433
42509
|
}
|
|
42434
|
-
if (
|
|
42510
|
+
if (userInput.numLinePosition === start && userInput.rel === startRel) {
|
|
42435
42511
|
// We're where we started.
|
|
42436
42512
|
return {
|
|
42437
42513
|
type: "invalid",
|
|
@@ -45380,7 +45456,7 @@ const init = function () {
|
|
|
45380
45456
|
|
|
45381
45457
|
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
45382
45458
|
const libName = "@khanacademy/perseus";
|
|
45383
|
-
const libVersion = "
|
|
45459
|
+
const libVersion = "46.0.0";
|
|
45384
45460
|
perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
45385
45461
|
|
|
45386
45462
|
const apiVersion = {
|
|
@@ -45968,14 +46044,10 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
45968
46044
|
});
|
|
45969
46045
|
}
|
|
45970
46046
|
componentDidUpdate(prevProps, prevState) {
|
|
45971
|
-
|
|
46047
|
+
const answerableCallback = this.props.apiOptions.answerableCallback;
|
|
46048
|
+
if (answerableCallback != null) {
|
|
45972
46049
|
const isAnswerable = this.questionRenderer.emptyWidgets().length === 0;
|
|
45973
|
-
|
|
45974
|
-
answerableCallback
|
|
45975
|
-
} = this.props.apiOptions;
|
|
45976
|
-
if (answerableCallback) {
|
|
45977
|
-
answerableCallback(isAnswerable);
|
|
45978
|
-
}
|
|
46050
|
+
answerableCallback(isAnswerable);
|
|
45979
46051
|
}
|
|
45980
46052
|
if (!this._fullyRendered) {
|
|
45981
46053
|
const assetsLoaded = Object.values(this.state.assetStatuses).every(Boolean);
|
|
@@ -46157,7 +46229,7 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
46157
46229
|
// TODO(alex): Check whether we rely on the format here for
|
|
46158
46230
|
// analyzing ProblemLogs. If not, remove this layer.
|
|
46159
46231
|
const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
|
|
46160
|
-
const keScore =
|
|
46232
|
+
const keScore = keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
|
|
46161
46233
|
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
46162
46234
|
this.setState({
|
|
46163
46235
|
questionCompleted: keScore.correct,
|
|
@@ -46346,7 +46418,7 @@ const traverseRenderer$1 = function (rendererOptions, contentCallback, deepWidge
|
|
|
46346
46418
|
newContent = modifiedContent;
|
|
46347
46419
|
}
|
|
46348
46420
|
}
|
|
46349
|
-
const newWidgets =
|
|
46421
|
+
const newWidgets = mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
|
|
46350
46422
|
// Widgets without info or a type are empty widgets, and
|
|
46351
46423
|
// should always be renderable. It's also annoying to write
|
|
46352
46424
|
// checks for this everywhere, so we just filter them out once and
|
|
@@ -47253,7 +47325,7 @@ class MultiRenderer extends React__namespace.Component {
|
|
|
47253
47325
|
if (ref.getSerializedState) {
|
|
47254
47326
|
state = ref.getSerializedState();
|
|
47255
47327
|
}
|
|
47256
|
-
return
|
|
47328
|
+
return keScoreFromPerseusScore(score, guess, state);
|
|
47257
47329
|
}
|
|
47258
47330
|
|
|
47259
47331
|
/**
|
|
@@ -47282,8 +47354,8 @@ class MultiRenderer extends React__namespace.Component {
|
|
|
47282
47354
|
scores.push(data.ref.score());
|
|
47283
47355
|
return data.ref?.getUserInput();
|
|
47284
47356
|
});
|
|
47285
|
-
const combinedScore = scores.reduce(
|
|
47286
|
-
return
|
|
47357
|
+
const combinedScore = scores.reduce(combineScores);
|
|
47358
|
+
return keScoreFromPerseusScore(combinedScore, guess, state);
|
|
47287
47359
|
}
|
|
47288
47360
|
|
|
47289
47361
|
/**
|
|
@@ -48956,7 +49028,7 @@ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
|
|
|
48956
49028
|
const parseDefinitionWidget = parseWidget(constant("definition"), object({
|
|
48957
49029
|
togglePrompt: string,
|
|
48958
49030
|
definition: string,
|
|
48959
|
-
static: boolean
|
|
49031
|
+
static: defaulted(boolean, () => false)
|
|
48960
49032
|
}));
|
|
48961
49033
|
|
|
48962
49034
|
const parseDropdownWidget = parseWidget(constant("dropdown"), object({
|
|
@@ -49159,15 +49231,26 @@ const parseIframeWidget = parseWidget(constant("iframe"), object({
|
|
|
49159
49231
|
static: defaulted(boolean, () => false)
|
|
49160
49232
|
}));
|
|
49161
49233
|
|
|
49234
|
+
const stringToNumber = (rawValue, ctx) => {
|
|
49235
|
+
if (typeof rawValue === "number") {
|
|
49236
|
+
return ctx.success(rawValue);
|
|
49237
|
+
}
|
|
49238
|
+
const parsedNumber = +rawValue;
|
|
49239
|
+
if (rawValue === "" || isNaN(parsedNumber)) {
|
|
49240
|
+
return ctx.failure("a number or numeric string", rawValue);
|
|
49241
|
+
}
|
|
49242
|
+
return ctx.success(parsedNumber);
|
|
49243
|
+
};
|
|
49244
|
+
|
|
49245
|
+
const numericToNumber = pipeParsers(union(number).or(string).parser).then(stringToNumber).parser;
|
|
49162
49246
|
const parsePerseusImageBackground = object({
|
|
49163
49247
|
url: optional(nullable(string)),
|
|
49164
|
-
width: optional(
|
|
49165
|
-
height: optional(
|
|
49166
|
-
top: optional(
|
|
49167
|
-
left: optional(
|
|
49168
|
-
bottom: optional(
|
|
49169
|
-
|
|
49170
|
-
scale: optional(union(number).or(string).parser)
|
|
49248
|
+
width: optional(numericToNumber),
|
|
49249
|
+
height: optional(numericToNumber),
|
|
49250
|
+
top: optional(numericToNumber),
|
|
49251
|
+
left: optional(numericToNumber),
|
|
49252
|
+
bottom: optional(numericToNumber),
|
|
49253
|
+
scale: optional(numericToNumber)
|
|
49171
49254
|
});
|
|
49172
49255
|
|
|
49173
49256
|
const pairOfNumbers$2 = pair(number, number);
|
|
@@ -49186,6 +49269,12 @@ const parseImageWidget = parseWidget(constant("image"), object({
|
|
|
49186
49269
|
box: optional(pairOfNumbers$2)
|
|
49187
49270
|
}));
|
|
49188
49271
|
|
|
49272
|
+
const booleanToString = (rawValue, ctx) => {
|
|
49273
|
+
if (typeof rawValue === "boolean") {
|
|
49274
|
+
return ctx.success(String(rawValue));
|
|
49275
|
+
}
|
|
49276
|
+
return ctx.failure("boolean", rawValue);
|
|
49277
|
+
};
|
|
49189
49278
|
const parseInputNumberWidget = parseWidget(constant("input-number"), object({
|
|
49190
49279
|
answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
|
|
49191
49280
|
inexact: optional(boolean),
|
|
@@ -49193,7 +49282,11 @@ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
|
|
|
49193
49282
|
rightAlign: optional(boolean),
|
|
49194
49283
|
simplify: enumeration("required", "optional", "enforced"),
|
|
49195
49284
|
size: enumeration("normal", "small"),
|
|
49196
|
-
|
|
49285
|
+
// TODO(benchristel): there are some content items where value is a
|
|
49286
|
+
// boolean, even though that makes no sense. We should figure out if
|
|
49287
|
+
// those content items are actually published anywhere, and consider
|
|
49288
|
+
// updating them.
|
|
49289
|
+
value: union(number).or(string).or(booleanToString).parser,
|
|
49197
49290
|
customKeypad: optional(boolean)
|
|
49198
49291
|
}));
|
|
49199
49292
|
|
|
@@ -49491,18 +49584,27 @@ const parseLockedFunctionType = object({
|
|
|
49491
49584
|
const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
|
|
49492
49585
|
const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
|
|
49493
49586
|
step: pairOfNumbers,
|
|
49494
|
-
|
|
49495
|
-
|
|
49587
|
+
// TODO(benchristel): rather than making gridStep and snapStep
|
|
49588
|
+
// optional, we should duplicate the defaulting logic from the
|
|
49589
|
+
// InteractiveGraph component. See parse-perseus-json/README.md for
|
|
49590
|
+
// why.
|
|
49591
|
+
gridStep: optional(pairOfNumbers),
|
|
49592
|
+
snapStep: optional(pairOfNumbers),
|
|
49496
49593
|
backgroundImage: optional(parsePerseusImageBackground),
|
|
49497
49594
|
markings: enumeration("graph", "grid", "none"),
|
|
49498
|
-
labels: array(string),
|
|
49595
|
+
labels: optional(array(string)),
|
|
49499
49596
|
showProtractor: boolean,
|
|
49500
49597
|
showRuler: optional(boolean),
|
|
49501
49598
|
showTooltips: optional(boolean),
|
|
49502
49599
|
rulerLabel: optional(string),
|
|
49503
49600
|
rulerTicks: optional(number),
|
|
49504
49601
|
range: pair(pairOfNumbers, pairOfNumbers),
|
|
49505
|
-
|
|
49602
|
+
// NOTE(benchristel): I copied the default graph from
|
|
49603
|
+
// interactive-graph.tsx. See the parse-perseus-json/README.md for
|
|
49604
|
+
// an explanation of why we want to duplicate the default here.
|
|
49605
|
+
graph: defaulted(parsePerseusGraphType, () => ({
|
|
49606
|
+
type: "linear"
|
|
49607
|
+
})),
|
|
49506
49608
|
correct: parsePerseusGraphType,
|
|
49507
49609
|
// TODO(benchristel): default lockedFigures to empty array
|
|
49508
49610
|
lockedFigures: optional(array(parseLockedFigure)),
|
|
@@ -49570,7 +49672,10 @@ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
|
|
|
49570
49672
|
isTickCtrl: optional(nullable(boolean)),
|
|
49571
49673
|
divisionRange: array(number),
|
|
49572
49674
|
numDivisions: optional(nullable(number)),
|
|
49573
|
-
|
|
49675
|
+
// NOTE(benchristel): I copied the default snapDivisions from
|
|
49676
|
+
// number-line.tsx. See the parse-perseus-json/README.md for
|
|
49677
|
+
// an explanation of why we want to duplicate the default here.
|
|
49678
|
+
snapDivisions: defaulted(number, () => 2),
|
|
49574
49679
|
tickStep: optional(nullable(number)),
|
|
49575
49680
|
correctRel: optional(nullable(string)),
|
|
49576
49681
|
correctX: number,
|
|
@@ -49583,18 +49688,18 @@ const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "d
|
|
|
49583
49688
|
const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
|
|
49584
49689
|
answers: array(object({
|
|
49585
49690
|
message: string,
|
|
49586
|
-
value: number,
|
|
49691
|
+
value: optional(number),
|
|
49587
49692
|
status: string,
|
|
49588
49693
|
answerForms: optional(array(parseMathFormat)),
|
|
49589
49694
|
strict: boolean,
|
|
49590
49695
|
maxError: optional(nullable(number)),
|
|
49591
49696
|
simplify: optional(nullable(string))
|
|
49592
49697
|
})),
|
|
49593
|
-
labelText: string,
|
|
49698
|
+
labelText: optional(string),
|
|
49594
49699
|
size: string,
|
|
49595
49700
|
coefficient: boolean,
|
|
49596
49701
|
rightAlign: optional(boolean),
|
|
49597
|
-
static: boolean,
|
|
49702
|
+
static: defaulted(boolean, () => false),
|
|
49598
49703
|
answerForms: optional(array(object({
|
|
49599
49704
|
name: parseMathFormat,
|
|
49600
49705
|
simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
|
|
@@ -49607,12 +49712,18 @@ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
|
|
|
49607
49712
|
function parseRenderer(rawValue, ctx) {
|
|
49608
49713
|
return parsePerseusRenderer(rawValue, ctx);
|
|
49609
49714
|
}
|
|
49715
|
+
const largeToAuto = (height, ctx) => {
|
|
49716
|
+
if (height === "large") {
|
|
49717
|
+
return ctx.success("auto");
|
|
49718
|
+
}
|
|
49719
|
+
return ctx.success(height);
|
|
49720
|
+
};
|
|
49610
49721
|
const parseOrdererWidget = parseWidget(constant("orderer"), object({
|
|
49611
|
-
options: array(parseRenderer),
|
|
49722
|
+
options: defaulted(array(parseRenderer), () => []),
|
|
49612
49723
|
correctOptions: array(parseRenderer),
|
|
49613
49724
|
otherOptions: array(parseRenderer),
|
|
49614
|
-
height: enumeration("normal", "auto"),
|
|
49615
|
-
layout: enumeration("horizontal", "vertical")
|
|
49725
|
+
height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
|
|
49726
|
+
layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
|
|
49616
49727
|
}));
|
|
49617
49728
|
|
|
49618
49729
|
const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
|
|
@@ -49626,7 +49737,7 @@ const parsePassageWidget = parseWidget(constant("passage"), object({
|
|
|
49626
49737
|
passageText: string,
|
|
49627
49738
|
passageTitle: string,
|
|
49628
49739
|
showLineNumbers: boolean,
|
|
49629
|
-
static: boolean
|
|
49740
|
+
static: defaulted(boolean, () => false)
|
|
49630
49741
|
}));
|
|
49631
49742
|
|
|
49632
49743
|
const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
|
|
@@ -49647,7 +49758,10 @@ const parsePlotterWidget = parseWidget(constant("plotter"), object({
|
|
|
49647
49758
|
picUrl: optional(nullable(string)),
|
|
49648
49759
|
picSize: optional(nullable(number)),
|
|
49649
49760
|
picBoxHeight: optional(nullable(number)),
|
|
49650
|
-
|
|
49761
|
+
// NOTE(benchristel): I copied the default plotDimensions from
|
|
49762
|
+
// plotter.tsx. See the parse-perseus-json/README.md for an explanation
|
|
49763
|
+
// of why we want to duplicate the defaults here.
|
|
49764
|
+
plotDimensions: defaulted(array(number), () => [380, 300])
|
|
49651
49765
|
}));
|
|
49652
49766
|
|
|
49653
49767
|
const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
|
|
@@ -49800,6 +49914,10 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
|
|
|
49800
49914
|
return parseAndAssign(`table ${id}`, parseTableWidget);
|
|
49801
49915
|
case "video":
|
|
49802
49916
|
return parseAndAssign(`video ${id}`, parseVideoWidget);
|
|
49917
|
+
case "sequence":
|
|
49918
|
+
// sequence is a deprecated widget type, and the corresponding
|
|
49919
|
+
// widget component no longer exists.
|
|
49920
|
+
return parseAndAssign(`sequence ${id}`, parseDeprecatedWidget);
|
|
49803
49921
|
default:
|
|
49804
49922
|
if (getWidget(type)) {
|
|
49805
49923
|
// @ts-expect-error - 'type' is not a valid widget type
|
|
@@ -49808,6 +49926,11 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
|
|
|
49808
49926
|
return ctx.failure("a valid widget type", type);
|
|
49809
49927
|
}
|
|
49810
49928
|
};
|
|
49929
|
+
const parseDeprecatedWidget = parseWidget(
|
|
49930
|
+
// Ignore the incoming widget type and hardcode "deprecated-standin"
|
|
49931
|
+
(_, ctx) => ctx.success("deprecated-standin"),
|
|
49932
|
+
// Allow any widget options
|
|
49933
|
+
object({}));
|
|
49811
49934
|
const parseStringToPositiveInt = (rawValue, ctx) => {
|
|
49812
49935
|
if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
|
|
49813
49936
|
return ctx.failure("numeric string", rawValue);
|
|
@@ -50089,6 +50212,9 @@ const GrapherUtil = {
|
|
|
50089
50212
|
getEquationString,
|
|
50090
50213
|
typeToButton
|
|
50091
50214
|
};
|
|
50215
|
+
const ScoringUtil = {
|
|
50216
|
+
keScoreFromPerseusScore
|
|
50217
|
+
};
|
|
50092
50218
|
|
|
50093
50219
|
exports.ApiOptions = ApiOptions;
|
|
50094
50220
|
exports.ArticleRenderer = ArticleRenderer;
|
|
@@ -50122,6 +50248,7 @@ exports.PlotterWidget = Plotter$1;
|
|
|
50122
50248
|
exports.QUESTION_WIDGETS = QUESTION_WIDGETS;
|
|
50123
50249
|
exports.Radio = Radio;
|
|
50124
50250
|
exports.Renderer = Renderer;
|
|
50251
|
+
exports.ScoringUtil = ScoringUtil;
|
|
50125
50252
|
exports.ServerItemRenderer = ref;
|
|
50126
50253
|
exports.TableWidget = Table$1;
|
|
50127
50254
|
exports.Util = Util;
|