@khanacademy/perseus 44.0.0 → 45.1.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 +473 -286
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +169 -12
- package/dist/index.d.ts +3 -1
- package/dist/index.js +475 -281
- 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-util.d.ts +4 -2
- package/dist/renderer.d.ts +13 -11
- package/dist/server-item-renderer.d.ts +32 -1
- package/dist/types.d.ts +63 -16
- package/dist/util/parse-perseus-json/general-purpose-parsers/string-to-number.d.ts +2 -0
- package/dist/validation.types.d.ts +8 -3
- package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +1 -1
- package/dist/widgets/expression/expression.d.ts +1 -1
- package/dist/widgets/group/group.d.ts +3 -1
- package/dist/widgets/group/score-group.d.ts +5 -0
- 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 +2 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3918,6 +3918,7 @@ Zoom.prototype.zoomImage = function () {
|
|
|
3918
3918
|
this._zoomOriginal();
|
|
3919
3919
|
}.bind(this);
|
|
3920
3920
|
img.src = this._targetImage.src;
|
|
3921
|
+
img.alt = this._targetImage.alt;
|
|
3921
3922
|
this.$zoomedImage = $zoomedImage;
|
|
3922
3923
|
};
|
|
3923
3924
|
Zoom.prototype._zoomOriginal = function () {
|
|
@@ -8993,7 +8994,10 @@ var Tex$1 = {
|
|
|
8993
8994
|
// We use createElement instead of JSX here because we can't name this file tex.tsx;
|
|
8994
8995
|
// that name is already taken.
|
|
8995
8996
|
// eslint-disable-next-line import/no-deprecated
|
|
8996
|
-
render(
|
|
8997
|
+
render(
|
|
8998
|
+
/*#__PURE__*/
|
|
8999
|
+
// eslint-disable-next-line react/no-children-prop
|
|
9000
|
+
React__namespace.createElement(TeX, {
|
|
8997
9001
|
children: text,
|
|
8998
9002
|
onRender: callback
|
|
8999
9003
|
}), $texHolder[0]);
|
|
@@ -14757,36 +14761,15 @@ const pluck = function (table, subKey) {
|
|
|
14757
14761
|
* });
|
|
14758
14762
|
* {a: 2, b: 3}
|
|
14759
14763
|
*/
|
|
14760
|
-
const mapObject
|
|
14764
|
+
const mapObject = function (obj, lambda) {
|
|
14761
14765
|
const result = {};
|
|
14762
|
-
|
|
14766
|
+
Object.keys(obj).forEach(key => {
|
|
14763
14767
|
// @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
|
|
14764
14768
|
result[key] = lambda(obj[key], key);
|
|
14765
14769
|
});
|
|
14766
14770
|
return result;
|
|
14767
14771
|
};
|
|
14768
14772
|
|
|
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
|
-
___default["default"].each(arr, function (elem) {
|
|
14780
|
-
result[elem] = lambda(elem);
|
|
14781
|
-
});
|
|
14782
|
-
return result;
|
|
14783
|
-
};
|
|
14784
|
-
var objective_ = {
|
|
14785
|
-
pluck: pluck,
|
|
14786
|
-
mapObject: mapObject$1,
|
|
14787
|
-
mapObjectFromArray: mapObjectFromArray
|
|
14788
|
-
};
|
|
14789
|
-
|
|
14790
14773
|
/**
|
|
14791
14774
|
* MovableLine
|
|
14792
14775
|
*/
|
|
@@ -14851,7 +14834,7 @@ ___default["default"].extend(MovableLine$4.prototype, {
|
|
|
14851
14834
|
// - are objects, not primitives (and need a deeper copy)
|
|
14852
14835
|
// - they don't need getters created for them
|
|
14853
14836
|
// TODO(jack): Consider "default" once we es3ify perseus
|
|
14854
|
-
|
|
14837
|
+
pluck(MovableLineOptions, "standard")), DEFAULT_PROPS$2);
|
|
14855
14838
|
},
|
|
14856
14839
|
/**
|
|
14857
14840
|
* Resets the object to its state as if it were constructed with
|
|
@@ -15358,7 +15341,7 @@ class MovablePoint$5 {
|
|
|
15358
15341
|
// - are objects, not primitives (and need a deeper copy)
|
|
15359
15342
|
// - they don't need getters created for them
|
|
15360
15343
|
// TODO(jack): Consider "default" once we es3ify perseus
|
|
15361
|
-
|
|
15344
|
+
pluck(MovablePointOptions, "standard")
|
|
15362
15345
|
// We only update props here, because we want things on state to
|
|
15363
15346
|
// be persistent, and updated appropriately in modify()
|
|
15364
15347
|
),
|
|
@@ -16259,7 +16242,7 @@ ___default["default"].extend(MovablePolygon.prototype, {
|
|
|
16259
16242
|
// - are objects, not primitives (and need a deeper copy)
|
|
16260
16243
|
// - they don't need getters created for them
|
|
16261
16244
|
// TODO(jack): Consider "default" once we es3ify perseus
|
|
16262
|
-
|
|
16245
|
+
pluck(MovablePolygonOptions, "standard")
|
|
16263
16246
|
|
|
16264
16247
|
// We only update props here, because we want things on state to
|
|
16265
16248
|
// be persistent, and updated appropriately in modify()
|
|
@@ -18919,6 +18902,138 @@ class QuestionParagraph extends React__namespace.Component {
|
|
|
18919
18902
|
}
|
|
18920
18903
|
}
|
|
18921
18904
|
|
|
18905
|
+
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
18906
|
+
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
18907
|
+
|
|
18908
|
+
/**
|
|
18909
|
+
* Regex for widget placeholders in a string.
|
|
18910
|
+
*
|
|
18911
|
+
* First capture group is the widget ID (ex. 'radio 1')
|
|
18912
|
+
* Second capture group is the widget type (ex. "radio)
|
|
18913
|
+
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
18914
|
+
*/
|
|
18915
|
+
function getWidgetRegex() {
|
|
18916
|
+
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
18917
|
+
}
|
|
18918
|
+
|
|
18919
|
+
/**
|
|
18920
|
+
* Add a widget placeholder using the widget ID.
|
|
18921
|
+
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
18922
|
+
*
|
|
18923
|
+
* @param {string} id
|
|
18924
|
+
* @returns {string}
|
|
18925
|
+
*/
|
|
18926
|
+
function addWidget(id) {
|
|
18927
|
+
return `[[☃ ${id}]]`;
|
|
18928
|
+
}
|
|
18929
|
+
|
|
18930
|
+
/**
|
|
18931
|
+
* Get a widget type by a widget's ID
|
|
18932
|
+
* (please don't derive type from ID)
|
|
18933
|
+
*
|
|
18934
|
+
* @param {string} widgetId the ID of the widget
|
|
18935
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
18936
|
+
* @returns {string} the widget type (ie "radio")
|
|
18937
|
+
*/
|
|
18938
|
+
function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
|
|
18939
|
+
const widget = widgetMap[WidgetId];
|
|
18940
|
+
return widget?.type ?? null;
|
|
18941
|
+
}
|
|
18942
|
+
|
|
18943
|
+
/**
|
|
18944
|
+
* Does the content have a specific type of widget?
|
|
18945
|
+
*
|
|
18946
|
+
* @param {string} type the type of the widget in question (ie "radio")
|
|
18947
|
+
* @param {string} content the string to search through
|
|
18948
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
18949
|
+
* @returns {boolean} if the content includes the widget type
|
|
18950
|
+
*/
|
|
18951
|
+
function contentHasWidgetType(type, content, widgetMap) {
|
|
18952
|
+
return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
|
|
18953
|
+
}
|
|
18954
|
+
|
|
18955
|
+
/**
|
|
18956
|
+
* Extract all widget IDs, which includes the widget type and instance number.
|
|
18957
|
+
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
18958
|
+
*
|
|
18959
|
+
* Content should contain Perseus widget placeholders,
|
|
18960
|
+
* which look like: '[[☃ radio 1]]'.
|
|
18961
|
+
*
|
|
18962
|
+
* @param {string} content
|
|
18963
|
+
* @returns {ReadonlyArray<string>} widgetIds
|
|
18964
|
+
*/
|
|
18965
|
+
function getWidgetIdsFromContent(content) {
|
|
18966
|
+
const widgets = [];
|
|
18967
|
+
const localWidgetRegex = getWidgetRegex();
|
|
18968
|
+
let match = localWidgetRegex.exec(content);
|
|
18969
|
+
while (match !== null) {
|
|
18970
|
+
widgets.push(match[1]);
|
|
18971
|
+
match = localWidgetRegex.exec(content);
|
|
18972
|
+
}
|
|
18973
|
+
return widgets;
|
|
18974
|
+
}
|
|
18975
|
+
|
|
18976
|
+
/**
|
|
18977
|
+
* Get a list of widget IDs from content,
|
|
18978
|
+
* but only for specific widget types
|
|
18979
|
+
*
|
|
18980
|
+
* @param {string} type the type of widget (ie "radio")
|
|
18981
|
+
* @param {string} content the string to parse
|
|
18982
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
18983
|
+
* @returns {ReadonlyArray<string>} the widget type (ie "radio")
|
|
18984
|
+
*/
|
|
18985
|
+
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
18986
|
+
const rv = [];
|
|
18987
|
+
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
18988
|
+
widgetIdsInContent.forEach(widgetId => {
|
|
18989
|
+
const widget = widgetMap[widgetId];
|
|
18990
|
+
if (widget?.type === type) {
|
|
18991
|
+
rv.push(widgetId);
|
|
18992
|
+
}
|
|
18993
|
+
});
|
|
18994
|
+
return rv;
|
|
18995
|
+
}
|
|
18996
|
+
|
|
18997
|
+
/**
|
|
18998
|
+
* Pull the widget map out of ItemData
|
|
18999
|
+
*
|
|
19000
|
+
* @param {PerseusItem} PerseusItem containing a widgetMap
|
|
19001
|
+
* @returns {WidgetMap} the widget map in the PerseusItem
|
|
19002
|
+
*/
|
|
19003
|
+
function getWidgetsMapFromItemData(itemData) {
|
|
19004
|
+
return itemData.question.widgets;
|
|
19005
|
+
}
|
|
19006
|
+
|
|
19007
|
+
/**
|
|
19008
|
+
* Get the widget information from a WidgetMap
|
|
19009
|
+
*
|
|
19010
|
+
* @param {string} widgetId the ID of the widget
|
|
19011
|
+
* @returns {PerseusWidget | null} the widget data if it exists, otherwise null
|
|
19012
|
+
*/
|
|
19013
|
+
function getWidgetFromWidgetMap(widgetId, widgetMap) {
|
|
19014
|
+
return widgetMap[widgetId] ?? null;
|
|
19015
|
+
}
|
|
19016
|
+
|
|
19017
|
+
/**
|
|
19018
|
+
* Get select widgets from a widget map.
|
|
19019
|
+
* Useful for multi-items that needs to split
|
|
19020
|
+
* one PerseusItem into several
|
|
19021
|
+
*
|
|
19022
|
+
* @param {ReadonlyArray<string>} widgetIds to extract
|
|
19023
|
+
* @param {PerseusWidgetsMap} widgetMap to extract from
|
|
19024
|
+
* @return {PerseusWidgetsMap} a new widget map with requested widgets
|
|
19025
|
+
*/
|
|
19026
|
+
function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
|
|
19027
|
+
const widgets = {};
|
|
19028
|
+
widgetIds.forEach(widgetId => {
|
|
19029
|
+
const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
|
|
19030
|
+
if (widget) {
|
|
19031
|
+
widgets[widgetId] = widget;
|
|
19032
|
+
}
|
|
19033
|
+
});
|
|
19034
|
+
return widgets;
|
|
19035
|
+
}
|
|
19036
|
+
|
|
18922
19037
|
const DEFAULT_ALIGNMENT$1 = "block";
|
|
18923
19038
|
// NOTE(kevinb): "default" is not one in `validAlignments`.
|
|
18924
19039
|
const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
|
|
@@ -19349,36 +19464,67 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
|
|
|
19349
19464
|
isLintable: isLintable
|
|
19350
19465
|
});
|
|
19351
19466
|
|
|
19467
|
+
function getUpgradedWidgetOptions(oldWidgetOptions) {
|
|
19468
|
+
// @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; }'.
|
|
19469
|
+
return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
|
|
19470
|
+
if (!widgetInfo.type || !widgetInfo.alignment) {
|
|
19471
|
+
const newValues = {};
|
|
19472
|
+
if (!widgetInfo.type) {
|
|
19473
|
+
// TODO: why does widget have no type?
|
|
19474
|
+
// We don't want to derive type from widget ID
|
|
19475
|
+
// see: LEMS-1845
|
|
19476
|
+
newValues.type = widgetId.split(" ")[0];
|
|
19477
|
+
}
|
|
19478
|
+
if (!widgetInfo.alignment) {
|
|
19479
|
+
newValues.alignment = "default";
|
|
19480
|
+
}
|
|
19481
|
+
widgetInfo = {
|
|
19482
|
+
...widgetInfo,
|
|
19483
|
+
...newValues
|
|
19484
|
+
};
|
|
19485
|
+
}
|
|
19486
|
+
return upgradeWidgetInfoToLatestVersion(widgetInfo);
|
|
19487
|
+
});
|
|
19488
|
+
}
|
|
19352
19489
|
function emptyWidgetsFunctional(widgets,
|
|
19353
19490
|
// This is a port of old code, I'm not sure why
|
|
19354
19491
|
// we need widgetIds vs the keys of the widgets object
|
|
19355
19492
|
widgetIds, userInputMap, strings, locale) {
|
|
19493
|
+
const upgradedWidgets = getUpgradedWidgetOptions(widgets);
|
|
19356
19494
|
return widgetIds.filter(id => {
|
|
19357
|
-
const widget =
|
|
19495
|
+
const widget = upgradedWidgets[id];
|
|
19358
19496
|
if (!widget || widget.static) {
|
|
19359
19497
|
// Static widgets shouldn't count as empty
|
|
19360
19498
|
return false;
|
|
19361
19499
|
}
|
|
19362
|
-
let score = null;
|
|
19363
|
-
const userInput = userInputMap[id];
|
|
19364
19500
|
const scorer = getWidgetScorer(widget.type);
|
|
19365
|
-
|
|
19366
|
-
const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
|
|
19367
|
-
score = Util.flattenScores(scores);
|
|
19368
|
-
} else if (scorer) {
|
|
19369
|
-
score = scorer(userInput, widget.options, strings, locale);
|
|
19370
|
-
}
|
|
19501
|
+
const score = scorer?.(userInputMap[id], widget.options, strings, locale);
|
|
19371
19502
|
if (score) {
|
|
19372
19503
|
return Util.scoreIsEmpty(score);
|
|
19373
19504
|
}
|
|
19374
19505
|
});
|
|
19375
19506
|
}
|
|
19507
|
+
|
|
19508
|
+
// TODO: combine scorePerseusItem with scoreWidgetsFunctional
|
|
19509
|
+
// once scorePerseusItem is the only one calling scoreWidgetsFunctional
|
|
19510
|
+
function scorePerseusItem(perseusRenderData, userInputMap,
|
|
19511
|
+
// TODO(LEMS-2461,LEMS-2391): these probably
|
|
19512
|
+
// need to be removed before we move this to the server
|
|
19513
|
+
strings, locale) {
|
|
19514
|
+
// There seems to be a chance that PerseusRenderer.widgets might include
|
|
19515
|
+
// widget data for widgets that are not in PerseusRenderer.content,
|
|
19516
|
+
// so this checks that the widgets are being used before scoring them
|
|
19517
|
+
const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
|
|
19518
|
+
const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
|
|
19519
|
+
return Util.flattenScores(scores);
|
|
19520
|
+
}
|
|
19376
19521
|
function scoreWidgetsFunctional(widgets,
|
|
19377
19522
|
// This is a port of old code, I'm not sure why
|
|
19378
19523
|
// we need widgetIds vs the keys of the widgets object
|
|
19379
19524
|
widgetIds, userInputMap, strings, locale) {
|
|
19525
|
+
const upgradedWidgets = getUpgradedWidgetOptions(widgets);
|
|
19380
19526
|
const gradedWidgetIds = widgetIds.filter(id => {
|
|
19381
|
-
const props =
|
|
19527
|
+
const props = upgradedWidgets[id];
|
|
19382
19528
|
const widgetIsGraded = props?.graded == null || props.graded;
|
|
19383
19529
|
const widgetIsStatic = !!props?.static;
|
|
19384
19530
|
// Ungraded widgets or widgets set to static shouldn't be graded.
|
|
@@ -19386,17 +19532,15 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19386
19532
|
});
|
|
19387
19533
|
const widgetScores = {};
|
|
19388
19534
|
gradedWidgetIds.forEach(id => {
|
|
19389
|
-
const widget =
|
|
19535
|
+
const widget = upgradedWidgets[id];
|
|
19390
19536
|
if (!widget) {
|
|
19391
19537
|
return;
|
|
19392
19538
|
}
|
|
19393
19539
|
const userInput = userInputMap[id];
|
|
19394
19540
|
const scorer = getWidgetScorer(widget.type);
|
|
19395
|
-
|
|
19396
|
-
|
|
19397
|
-
widgetScores[id] =
|
|
19398
|
-
} else if (scorer) {
|
|
19399
|
-
widgetScores[id] = scorer(userInput, widget.options, strings, locale);
|
|
19541
|
+
const score = scorer?.(userInput, widget.options, strings, locale);
|
|
19542
|
+
if (score != null) {
|
|
19543
|
+
widgetScores[id] = score;
|
|
19400
19544
|
}
|
|
19401
19545
|
});
|
|
19402
19546
|
return widgetScores;
|
|
@@ -19703,9 +19847,6 @@ class WidgetContainer extends React__namespace.Component {
|
|
|
19703
19847
|
}
|
|
19704
19848
|
}
|
|
19705
19849
|
|
|
19706
|
-
const {
|
|
19707
|
-
mapObject
|
|
19708
|
-
} = objective_;
|
|
19709
19850
|
const rContainsNonWhitespace = /\S/;
|
|
19710
19851
|
const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
|
|
19711
19852
|
const noopOnRender = () => {};
|
|
@@ -19918,32 +20059,12 @@ class Renderer extends React__namespace.Component {
|
|
|
19918
20059
|
};
|
|
19919
20060
|
};
|
|
19920
20061
|
_getInitialWidgetState = props => {
|
|
19921
|
-
const allWidgetInfo =
|
|
20062
|
+
const allWidgetInfo = getUpgradedWidgetOptions(props.widgets);
|
|
19922
20063
|
return {
|
|
19923
20064
|
widgetInfo: allWidgetInfo,
|
|
19924
20065
|
widgetProps: this._getAllWidgetsStartProps(allWidgetInfo, props)
|
|
19925
20066
|
};
|
|
19926
20067
|
};
|
|
19927
|
-
|
|
19928
|
-
// @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; }'.
|
|
19929
|
-
_getAllWidgetsInfo = props => {
|
|
19930
|
-
return mapObject(props.widgets, (widgetInfo, widgetId) => {
|
|
19931
|
-
if (!widgetInfo.type || !widgetInfo.alignment) {
|
|
19932
|
-
const newValues = {};
|
|
19933
|
-
if (!widgetInfo.type) {
|
|
19934
|
-
// TODO: why does widget have no type?
|
|
19935
|
-
// We don't want to derive type from widget ID
|
|
19936
|
-
// see: LEMS-1845
|
|
19937
|
-
newValues.type = widgetId.split(" ")[0];
|
|
19938
|
-
}
|
|
19939
|
-
if (!widgetInfo.alignment) {
|
|
19940
|
-
newValues.alignment = "default";
|
|
19941
|
-
}
|
|
19942
|
-
widgetInfo = ___default["default"].extend({}, widgetInfo, newValues);
|
|
19943
|
-
}
|
|
19944
|
-
return upgradeWidgetInfoToLatestVersion(widgetInfo);
|
|
19945
|
-
});
|
|
19946
|
-
};
|
|
19947
20068
|
_getAllWidgetsStartProps = (allWidgetInfo, props) => {
|
|
19948
20069
|
const {
|
|
19949
20070
|
apiOptions,
|
|
@@ -20442,7 +20563,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20442
20563
|
// Instead, we center them with css magic in styles.less
|
|
20443
20564
|
// /cry(aria)
|
|
20444
20565
|
this._foundTextNodes = true;
|
|
20445
|
-
if (
|
|
20566
|
+
if (this.widgetIds.includes(node.id)) {
|
|
20446
20567
|
// We don't want to render a duplicate widget key/ref,
|
|
20447
20568
|
// as this causes problems with react (for obvious
|
|
20448
20569
|
// reasons). Instead we just notify the
|
|
@@ -20767,7 +20888,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20767
20888
|
};
|
|
20768
20889
|
getInputPaths = () => {
|
|
20769
20890
|
const inputPaths = [];
|
|
20770
|
-
|
|
20891
|
+
this.widgetIds.forEach(widgetId => {
|
|
20771
20892
|
const widget = this.getWidgetInstance(widgetId);
|
|
20772
20893
|
if (widget && widget.getInputPaths) {
|
|
20773
20894
|
// Grab all input paths and add widgetID to the front
|
|
@@ -20775,7 +20896,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20775
20896
|
// Prefix paths with their widgetID and add to collective
|
|
20776
20897
|
// list of paths.
|
|
20777
20898
|
// @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
|
|
20778
|
-
|
|
20899
|
+
widgetInputPaths.forEach(inputPath => {
|
|
20779
20900
|
const relativeInputPath = [widgetId].concat(inputPath);
|
|
20780
20901
|
inputPaths.push(relativeInputPath);
|
|
20781
20902
|
});
|
|
@@ -20842,6 +20963,13 @@ class Renderer extends React__namespace.Component {
|
|
|
20842
20963
|
}, this);
|
|
20843
20964
|
return state;
|
|
20844
20965
|
};
|
|
20966
|
+
|
|
20967
|
+
/**
|
|
20968
|
+
* Returns an array of widget ids that are empty (meaning widgets where the
|
|
20969
|
+
* learner has not interacted with the widget yet or has not filled in all
|
|
20970
|
+
* fields). For example, the `interactive-graph` widget is considered
|
|
20971
|
+
* empty if the graph is in the starting state.
|
|
20972
|
+
*/
|
|
20845
20973
|
emptyWidgets() {
|
|
20846
20974
|
return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
20847
20975
|
}
|
|
@@ -20895,7 +21023,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20895
21023
|
}, 0);
|
|
20896
21024
|
});
|
|
20897
21025
|
};
|
|
20898
|
-
setInputValue = (path, newValue,
|
|
21026
|
+
setInputValue = (path, newValue, cb) => {
|
|
20899
21027
|
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
20900
21028
|
const widgetId = ___default["default"].first(path);
|
|
20901
21029
|
// @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
|
|
@@ -20903,7 +21031,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20903
21031
|
const widget = this.getWidgetInstance(widgetId);
|
|
20904
21032
|
|
|
20905
21033
|
// Widget handles parsing of the interWidgetPath.
|
|
20906
|
-
widget?.setInputValue?.(interWidgetPath, newValue,
|
|
21034
|
+
widget?.setInputValue?.(interWidgetPath, newValue, cb);
|
|
20907
21035
|
};
|
|
20908
21036
|
|
|
20909
21037
|
/**
|
|
@@ -20968,33 +21096,30 @@ class Renderer extends React__namespace.Component {
|
|
|
20968
21096
|
}
|
|
20969
21097
|
|
|
20970
21098
|
/**
|
|
20971
|
-
*
|
|
20972
|
-
*
|
|
20973
|
-
*
|
|
20974
|
-
*/
|
|
20975
|
-
scoreWidgets() {
|
|
20976
|
-
return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
20977
|
-
}
|
|
20978
|
-
|
|
20979
|
-
/**
|
|
20980
|
-
* Grades the content.
|
|
21099
|
+
* Scores the content.
|
|
21100
|
+
*
|
|
21101
|
+
* @deprecated use scorePerseusItem
|
|
20981
21102
|
*/
|
|
20982
21103
|
score() {
|
|
20983
|
-
const scores = this.
|
|
21104
|
+
const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
20984
21105
|
const combinedScore = Util.flattenScores(scores);
|
|
20985
21106
|
return combinedScore;
|
|
20986
21107
|
}
|
|
20987
|
-
|
|
21108
|
+
|
|
21109
|
+
/**
|
|
21110
|
+
* @deprecated use scorePerseusItem
|
|
21111
|
+
*/
|
|
21112
|
+
guessAndScore = () => {
|
|
20988
21113
|
const totalGuess = this.getUserInput();
|
|
20989
21114
|
const totalScore = this.score();
|
|
20990
21115
|
return [totalGuess, totalScore];
|
|
20991
|
-
}
|
|
21116
|
+
};
|
|
20992
21117
|
examples = () => {
|
|
20993
21118
|
const widgetIds = this.widgetIds;
|
|
20994
|
-
const examples =
|
|
21119
|
+
const examples = widgetIds.map(widgetId => {
|
|
20995
21120
|
const widget = this.getWidgetInstance(widgetId);
|
|
20996
21121
|
return widget != null && widget.examples ? widget.examples() : null;
|
|
20997
|
-
}));
|
|
21122
|
+
}).filter(Boolean);
|
|
20998
21123
|
|
|
20999
21124
|
// no widgets with examples
|
|
21000
21125
|
if (!examples.length) {
|
|
@@ -21204,6 +21329,7 @@ class TextInput extends React__namespace.Component {
|
|
|
21204
21329
|
value: formattedValue,
|
|
21205
21330
|
type: "text",
|
|
21206
21331
|
"aria-label": labelText,
|
|
21332
|
+
"aria-describedby": this.props["aria-describedby"],
|
|
21207
21333
|
onChange: value => this.props.onChange(value),
|
|
21208
21334
|
placeholder: placeholder,
|
|
21209
21335
|
onFocus: onFocus,
|
|
@@ -21462,6 +21588,7 @@ class Tooltip extends React__namespace.Component {
|
|
|
21462
21588
|
}, /*#__PURE__*/React__namespace.createElement("div", {
|
|
21463
21589
|
ref: this.tooltipContainerRef,
|
|
21464
21590
|
className: "tooltipContainer",
|
|
21591
|
+
role: "tooltip",
|
|
21465
21592
|
style: {
|
|
21466
21593
|
position: "absolute",
|
|
21467
21594
|
// height must start out undefined, not null, so that
|
|
@@ -21538,9 +21665,15 @@ class InputWithExamples extends React__namespace.Component {
|
|
|
21538
21665
|
};
|
|
21539
21666
|
_renderInput = () => {
|
|
21540
21667
|
const id = this._getUniqueId();
|
|
21668
|
+
const ariaId = `aria-for-${id}`;
|
|
21669
|
+
// Generate text from a known set of format options that will read well in a screen reader
|
|
21670
|
+
const examplesAria = this.props.examples.length === 7 ? "" : `${this.props.examples[0]}
|
|
21671
|
+
${this.props.examples.slice(1).join(", or\n")}`
|
|
21672
|
+
// @ts-expect-error TS2550: Property replaceAll does not exist on type string.
|
|
21673
|
+
.replaceAll("*", "").replaceAll("$", "").replaceAll("\\ \\text{pi}", " pi").replaceAll("\\ ", " and ");
|
|
21541
21674
|
const inputProps = {
|
|
21542
21675
|
id: id,
|
|
21543
|
-
"aria-describedby":
|
|
21676
|
+
"aria-describedby": ariaId,
|
|
21544
21677
|
ref: "input",
|
|
21545
21678
|
className: this._getInputClassName(),
|
|
21546
21679
|
labelText: this.props.labelText,
|
|
@@ -21556,7 +21689,12 @@ class InputWithExamples extends React__namespace.Component {
|
|
|
21556
21689
|
autoCorrect: "off",
|
|
21557
21690
|
spellCheck: "false"
|
|
21558
21691
|
};
|
|
21559
|
-
return /*#__PURE__*/React__namespace.createElement(TextInput, inputProps)
|
|
21692
|
+
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(TextInput, inputProps), /*#__PURE__*/React__namespace.createElement("span", {
|
|
21693
|
+
id: ariaId,
|
|
21694
|
+
style: {
|
|
21695
|
+
display: "none"
|
|
21696
|
+
}
|
|
21697
|
+
}, examplesAria));
|
|
21560
21698
|
};
|
|
21561
21699
|
_handleFocus = () => {
|
|
21562
21700
|
this.props.onFocus();
|
|
@@ -21597,8 +21735,12 @@ class InputWithExamples extends React__namespace.Component {
|
|
|
21597
21735
|
};
|
|
21598
21736
|
render() {
|
|
21599
21737
|
const input = this._renderInput();
|
|
21600
|
-
const examplesContent =
|
|
21601
|
-
|
|
21738
|
+
const examplesContent = this.props.examples.length <= 2 ? this.props.examples.join(" ") // A single item (with or without leading text) is not a "list"
|
|
21739
|
+
: this.props.examples // 2 or more items should display as a list
|
|
21740
|
+
.map((example, index) => {
|
|
21741
|
+
// If the first example is bold, then it is most likely a heading/leading text.
|
|
21742
|
+
// So, it shouldn't be part of the list.
|
|
21743
|
+
return index === 0 && example.startsWith("**") ? `${example}\n` : `- ${example}`;
|
|
21602
21744
|
}).join("\n");
|
|
21603
21745
|
const showExamples = this.props.shouldShowExamples && this.state.showExamples;
|
|
21604
21746
|
return /*#__PURE__*/React__namespace.createElement(Tooltip, {
|
|
@@ -22252,7 +22394,7 @@ function scoreNumericInput(userInput, rubric, strings) {
|
|
|
22252
22394
|
// We may have received TeX; try to parse it before grading.
|
|
22253
22395
|
// If `currentValue` is not TeX, this should be a no-op.
|
|
22254
22396
|
const currentValue = ParseTex(userInput.currentValue);
|
|
22255
|
-
const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => Math.abs(answer.value) <= 1);
|
|
22397
|
+
const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value !== undefined && Math.abs(answer.value) <= 1);
|
|
22256
22398
|
|
|
22257
22399
|
// The coefficient is an attribute of the widget
|
|
22258
22400
|
let localValue = currentValue;
|
|
@@ -22328,7 +22470,8 @@ class NumericInput extends React__namespace.Component {
|
|
|
22328
22470
|
state = {
|
|
22329
22471
|
// keeps track of the other set of values when switching
|
|
22330
22472
|
// between 0 and finite solutions
|
|
22331
|
-
previousValues: [""]
|
|
22473
|
+
previousValues: [""],
|
|
22474
|
+
isFocused: false
|
|
22332
22475
|
};
|
|
22333
22476
|
|
|
22334
22477
|
// TODO(Nicole, Jeremy): This is maybe never used and should be removed
|
|
@@ -22393,9 +22536,21 @@ class NumericInput extends React__namespace.Component {
|
|
|
22393
22536
|
};
|
|
22394
22537
|
_handleFocus = () => {
|
|
22395
22538
|
this.props.onFocus([]);
|
|
22539
|
+
this.setState(currentState => {
|
|
22540
|
+
return {
|
|
22541
|
+
...currentState,
|
|
22542
|
+
isFocused: true
|
|
22543
|
+
};
|
|
22544
|
+
});
|
|
22396
22545
|
};
|
|
22397
22546
|
_handleBlur = () => {
|
|
22398
22547
|
this.props.onBlur([]);
|
|
22548
|
+
this.setState(currentState => {
|
|
22549
|
+
return {
|
|
22550
|
+
...currentState,
|
|
22551
|
+
isFocused: false
|
|
22552
|
+
};
|
|
22553
|
+
});
|
|
22399
22554
|
};
|
|
22400
22555
|
render() {
|
|
22401
22556
|
let labelText = this.props.labelText;
|
|
@@ -22426,13 +22581,20 @@ class NumericInput extends React__namespace.Component {
|
|
|
22426
22581
|
// component.
|
|
22427
22582
|
const styles = aphrodite.StyleSheet.create({
|
|
22428
22583
|
input: {
|
|
22584
|
+
borderRadius: "3px",
|
|
22585
|
+
borderWidth: this.state.isFocused ? "2px" : "1px",
|
|
22586
|
+
display: "inline-block",
|
|
22587
|
+
fontFamily: `Symbola, "Times New Roman", serif`,
|
|
22588
|
+
fontSize: "18px",
|
|
22589
|
+
height: "32px",
|
|
22590
|
+
lineHeight: "18px",
|
|
22591
|
+
padding: this.state.isFocused ? "4px" : "4px 5px",
|
|
22592
|
+
// account for added focus border thickness
|
|
22429
22593
|
textAlign: this.props.rightAlign ? "right" : "left",
|
|
22430
|
-
width: this.props.size === "small" ? 40 : 80
|
|
22431
|
-
padding: 0,
|
|
22432
|
-
height: "auto"
|
|
22594
|
+
width: this.props.size === "small" ? 40 : 80
|
|
22433
22595
|
}
|
|
22434
22596
|
});
|
|
22435
|
-
return /*#__PURE__*/React__namespace.createElement(
|
|
22597
|
+
return /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
|
|
22436
22598
|
ref: ref => this.inputRef = ref,
|
|
22437
22599
|
value: this.props.currentValue,
|
|
22438
22600
|
onChange: this.handleChange,
|
|
@@ -22444,7 +22606,7 @@ class NumericInput extends React__namespace.Component {
|
|
|
22444
22606
|
id: this.props.widgetId,
|
|
22445
22607
|
disabled: this.props.apiOptions.readOnly,
|
|
22446
22608
|
style: styles.input
|
|
22447
|
-
})
|
|
22609
|
+
});
|
|
22448
22610
|
}
|
|
22449
22611
|
}
|
|
22450
22612
|
|
|
@@ -27372,8 +27534,8 @@ var Definition$1 = {
|
|
|
27372
27534
|
scorer: () => scoreNoop()
|
|
27373
27535
|
};
|
|
27374
27536
|
|
|
27375
|
-
//
|
|
27376
|
-
//
|
|
27537
|
+
// There are no required props, but this component can receive props from any
|
|
27538
|
+
// deprecated widget
|
|
27377
27539
|
class DeprecatedStandin extends React__default["default"].Component {
|
|
27378
27540
|
static contextType = PerseusI18nContext;
|
|
27379
27541
|
// this just helps with TS weak typing when a Widget
|
|
@@ -29496,6 +29658,13 @@ const getPromptJSON$f = rendererJSON => {
|
|
|
29496
29658
|
};
|
|
29497
29659
|
};
|
|
29498
29660
|
|
|
29661
|
+
// The `group` widget is basically a widget hosting a full Perseus system in
|
|
29662
|
+
// it. As such, scoring a group means scoring all widgets it contains.
|
|
29663
|
+
function scoreGroup(userInput, options, strings, locale) {
|
|
29664
|
+
const scores = scoreWidgetsFunctional(options.widgets, Object.keys(options.widgets), userInput, strings, locale);
|
|
29665
|
+
return Util.flattenScores(scores);
|
|
29666
|
+
}
|
|
29667
|
+
|
|
29499
29668
|
// exports has no 'transform'
|
|
29500
29669
|
|
|
29501
29670
|
class Group extends React__namespace.Component {
|
|
@@ -29602,6 +29771,9 @@ class Group extends React__namespace.Component {
|
|
|
29602
29771
|
this.change("widgets", this.rendererRef.props.widgets);
|
|
29603
29772
|
}
|
|
29604
29773
|
};
|
|
29774
|
+
|
|
29775
|
+
// TODO(LEMS-2391): replace this when there's a separate check
|
|
29776
|
+
// for valid/invalid state
|
|
29605
29777
|
const score = this.rendererRef?.score();
|
|
29606
29778
|
const isValid = score && score.type !== "invalid";
|
|
29607
29779
|
const isInvalid = score && score.type === "invalid";
|
|
@@ -29644,6 +29816,9 @@ var Group$1 = {
|
|
|
29644
29816
|
displayName: "Group (SAT only)",
|
|
29645
29817
|
widget: Group,
|
|
29646
29818
|
traverseChildWidgets: traverseChildWidgets,
|
|
29819
|
+
// TODO(LEMS-2656): remove TS suppression
|
|
29820
|
+
// @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
|
|
29821
|
+
scorer: scoreGroup,
|
|
29647
29822
|
hidden: true,
|
|
29648
29823
|
isLintable: true
|
|
29649
29824
|
};
|
|
@@ -32387,6 +32562,61 @@ const LegacyGrid = _ref => {
|
|
|
32387
32562
|
|
|
32388
32563
|
// Range is replaced within this file with Interval, but it is used elsewhere
|
|
32389
32564
|
// and exported from the package, so we need to keep it around.
|
|
32565
|
+
/**
|
|
32566
|
+
* Our core set of Perseus widgets.
|
|
32567
|
+
*
|
|
32568
|
+
* This interface is the basis for "registering" all Perseus widget types.
|
|
32569
|
+
* There should be one key/value pair for each supported widget. If you create
|
|
32570
|
+
* a new widget, an entry should be added to this interface. Note that this
|
|
32571
|
+
* only registers the widget options type, you'll also need to register the
|
|
32572
|
+
* widget so that it's available at runtime (@see
|
|
32573
|
+
* {@link file://./widgets.ts#registerWidget}).
|
|
32574
|
+
*
|
|
32575
|
+
* Importantly, the key should be the name that is used in widget IDs. For most
|
|
32576
|
+
* widgets that is the same as the widget option's `type` field. In cases where
|
|
32577
|
+
* a widget has been deprecated and replaced with the deprecated-standin
|
|
32578
|
+
* widget, it should be the original widget type!
|
|
32579
|
+
*
|
|
32580
|
+
* If you define the widget outside of this package, you can still add the new
|
|
32581
|
+
* widget to this interface by writing the following in that package that
|
|
32582
|
+
* contains the widget. TypeScript will merge that definition of the
|
|
32583
|
+
* `PerseusWidgets` with the one defined below.
|
|
32584
|
+
*
|
|
32585
|
+
* ```typescript
|
|
32586
|
+
* declare module "@khanacademy/perseus" {
|
|
32587
|
+
* interface PerseusWidgetTypes {
|
|
32588
|
+
* // A new widget
|
|
32589
|
+
* "new-awesomeness": MyAwesomeNewWidget;
|
|
32590
|
+
*
|
|
32591
|
+
* // A deprecated widget
|
|
32592
|
+
* "super-old-widget": AutoCorrectWidget;
|
|
32593
|
+
* }
|
|
32594
|
+
* }
|
|
32595
|
+
*
|
|
32596
|
+
* // The new widget's options definition
|
|
32597
|
+
* type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>;
|
|
32598
|
+
*
|
|
32599
|
+
* // The deprecated widget's options definition
|
|
32600
|
+
* type SuperOldWidget = WidgetOptions<'super-old-widget', object>;
|
|
32601
|
+
* ```
|
|
32602
|
+
*
|
|
32603
|
+
* This interface can be extended through the magic of TypeScript "Declaration
|
|
32604
|
+
* merging". Specifically, we augment this module and extend this interface.
|
|
32605
|
+
*
|
|
32606
|
+
* @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
|
|
32607
|
+
*/
|
|
32608
|
+
/**
|
|
32609
|
+
* A map of widget IDs to widget options. This is most often used as the type
|
|
32610
|
+
* for a set of widgets defined in a `PerseusItem` but can also be useful to
|
|
32611
|
+
* represent a function parameter where only `widgets` from a `PerseusItem` are
|
|
32612
|
+
* needed. Today Widget IDs are made up of the widget type and an incrementing
|
|
32613
|
+
* integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid
|
|
32614
|
+
* reading/parsing the widget id to derive any information from it, except in
|
|
32615
|
+
* the case of this map.
|
|
32616
|
+
*
|
|
32617
|
+
* @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
|
|
32618
|
+
* by augmenting the PerseusWidgetTypes with new widget types!
|
|
32619
|
+
*/
|
|
32390
32620
|
/**
|
|
32391
32621
|
* A "PerseusItem" is a classic Perseus item. It is rendered by the
|
|
32392
32622
|
* `ServerItemRenderer` and the layout is pre-set.
|
|
@@ -32402,6 +32632,9 @@ const LegacyGrid = _ref => {
|
|
|
32402
32632
|
* A "MultiItem" is an advanced Perseus item. It is rendered by the
|
|
32403
32633
|
* `MultiRenderer` and you can control the layout of individual parts of the
|
|
32404
32634
|
* item.
|
|
32635
|
+
*
|
|
32636
|
+
* @deprecated MultiItem support is slated for removal in a future Perseus
|
|
32637
|
+
* release.
|
|
32405
32638
|
*/
|
|
32406
32639
|
const ItemExtras = [
|
|
32407
32640
|
// The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
|
|
@@ -32423,6 +32656,11 @@ const ItemExtras = [
|
|
|
32423
32656
|
// The user might benefit from using a statistics Z Table like https://www.ztable.net/
|
|
32424
32657
|
"zTable"];
|
|
32425
32658
|
|
|
32659
|
+
/**
|
|
32660
|
+
* The type representing the common structure of all widget's options. The
|
|
32661
|
+
* `Options` generic type represents the widget-specific option data.
|
|
32662
|
+
*/
|
|
32663
|
+
|
|
32426
32664
|
// prettier-ignore
|
|
32427
32665
|
|
|
32428
32666
|
// prettier-ignore
|
|
@@ -32491,7 +32729,9 @@ const ItemExtras = [
|
|
|
32491
32729
|
|
|
32492
32730
|
//prettier-ignore
|
|
32493
32731
|
|
|
32494
|
-
|
|
32732
|
+
/**
|
|
32733
|
+
* A background image applied to various widgets.
|
|
32734
|
+
*/
|
|
32495
32735
|
|
|
32496
32736
|
const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
|
|
32497
32737
|
|
|
@@ -37135,8 +37375,6 @@ const makeInvalidTypeError = (functionName, graphType) => {
|
|
|
37135
37375
|
});
|
|
37136
37376
|
};
|
|
37137
37377
|
|
|
37138
|
-
// There's no transform function in exports
|
|
37139
|
-
|
|
37140
37378
|
// TODO: there's another, very similar getSinusoidCoefficients function
|
|
37141
37379
|
// they should probably be merged
|
|
37142
37380
|
function getSinusoidCoefficients(coords) {
|
|
@@ -42254,23 +42492,37 @@ const getPromptJSON$6 = (renderProps, userInput) => {
|
|
|
42254
42492
|
};
|
|
42255
42493
|
};
|
|
42256
42494
|
|
|
42257
|
-
|
|
42258
|
-
|
|
42259
|
-
|
|
42260
|
-
|
|
42261
|
-
|
|
42262
|
-
|
|
42263
|
-
|
|
42264
|
-
const
|
|
42495
|
+
/**
|
|
42496
|
+
* Checks user input is within the allowed range and not the same as the initial
|
|
42497
|
+
* state.
|
|
42498
|
+
* @param userInput
|
|
42499
|
+
* @see 'scoreNumberLine' for the scoring logic.
|
|
42500
|
+
*/
|
|
42501
|
+
function validateNumberLine(userInput) {
|
|
42502
|
+
const divisionRange = userInput.divisionRange;
|
|
42503
|
+
const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
|
|
42265
42504
|
|
|
42266
42505
|
// TODO: I don't think isTickCrtl is a thing anymore
|
|
42267
|
-
if (
|
|
42506
|
+
if (userInput.isTickCrtl && outsideAllowedRange) {
|
|
42268
42507
|
return {
|
|
42269
42508
|
type: "invalid",
|
|
42270
42509
|
message: "Number of divisions is outside the allowed range."
|
|
42271
42510
|
};
|
|
42272
42511
|
}
|
|
42273
|
-
|
|
42512
|
+
return null;
|
|
42513
|
+
}
|
|
42514
|
+
|
|
42515
|
+
function scoreNumberLine(userInput, scoringData) {
|
|
42516
|
+
const validationError = validateNumberLine(userInput);
|
|
42517
|
+
if (validationError) {
|
|
42518
|
+
return validationError;
|
|
42519
|
+
}
|
|
42520
|
+
const range = scoringData.range;
|
|
42521
|
+
const start = scoringData.initialX != null ? scoringData.initialX : range[0];
|
|
42522
|
+
const startRel = scoringData.isInequality ? "ge" : "eq";
|
|
42523
|
+
const correctRel = scoringData.correctRel || "eq";
|
|
42524
|
+
const correctPos = kmath.number.equal(userInput.numLinePosition, scoringData.correctX || 0);
|
|
42525
|
+
if (correctPos && correctRel === userInput.rel) {
|
|
42274
42526
|
return {
|
|
42275
42527
|
type: "points",
|
|
42276
42528
|
earned: 1,
|
|
@@ -42278,7 +42530,7 @@ function scoreNumberLine(state, rubric) {
|
|
|
42278
42530
|
message: null
|
|
42279
42531
|
};
|
|
42280
42532
|
}
|
|
42281
|
-
if (
|
|
42533
|
+
if (userInput.numLinePosition === start && userInput.rel === startRel) {
|
|
42282
42534
|
// We're where we started.
|
|
42283
42535
|
return {
|
|
42284
42536
|
type: "invalid",
|
|
@@ -45227,7 +45479,7 @@ const init = function () {
|
|
|
45227
45479
|
|
|
45228
45480
|
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
45229
45481
|
const libName = "@khanacademy/perseus";
|
|
45230
|
-
const libVersion = "
|
|
45482
|
+
const libVersion = "45.1.0";
|
|
45231
45483
|
perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
45232
45484
|
|
|
45233
45485
|
const apiVersion = {
|
|
@@ -45815,14 +46067,10 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
45815
46067
|
});
|
|
45816
46068
|
}
|
|
45817
46069
|
componentDidUpdate(prevProps, prevState) {
|
|
45818
|
-
|
|
46070
|
+
const answerableCallback = this.props.apiOptions.answerableCallback;
|
|
46071
|
+
if (answerableCallback != null) {
|
|
45819
46072
|
const isAnswerable = this.questionRenderer.emptyWidgets().length === 0;
|
|
45820
|
-
|
|
45821
|
-
answerableCallback
|
|
45822
|
-
} = this.props.apiOptions;
|
|
45823
|
-
if (answerableCallback) {
|
|
45824
|
-
answerableCallback(isAnswerable);
|
|
45825
|
-
}
|
|
46073
|
+
answerableCallback(isAnswerable);
|
|
45826
46074
|
}
|
|
45827
46075
|
if (!this._fullyRendered) {
|
|
45828
46076
|
const assetsLoaded = Object.values(this.state.assetStatuses).every(Boolean);
|
|
@@ -45831,6 +46079,13 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
45831
46079
|
this.props.onRendered(true);
|
|
45832
46080
|
}
|
|
45833
46081
|
}
|
|
46082
|
+
if (this.props.score && this.props.score !== prevProps.score) {
|
|
46083
|
+
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
46084
|
+
this.setState({
|
|
46085
|
+
questionCompleted: this.props.score.correct,
|
|
46086
|
+
questionHighlightedWidgets: emptyQuestionAreaWidgets
|
|
46087
|
+
});
|
|
46088
|
+
}
|
|
45834
46089
|
}
|
|
45835
46090
|
componentWillUnmount() {
|
|
45836
46091
|
if (this.blurTimeoutID != null) {
|
|
@@ -45966,18 +46221,37 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
45966
46221
|
return this.questionRenderer.getPromptJSON();
|
|
45967
46222
|
}
|
|
45968
46223
|
|
|
46224
|
+
/**
|
|
46225
|
+
* Returns an array of the widget `.getUserInput()` results
|
|
46226
|
+
*
|
|
46227
|
+
* TODO: can we remove this? Seems to be just for backwards
|
|
46228
|
+
* compatibility with old Perseus Chrome logging
|
|
46229
|
+
* @deprecated use getUserInput
|
|
46230
|
+
*/
|
|
46231
|
+
getUserInputLegacy() {
|
|
46232
|
+
return this.questionRenderer.getUserInput();
|
|
46233
|
+
}
|
|
46234
|
+
|
|
46235
|
+
/**
|
|
46236
|
+
* Returns an object of the widget `.getUserInput()` results
|
|
46237
|
+
*/
|
|
46238
|
+
getUserInput() {
|
|
46239
|
+
return this.questionRenderer.getUserInputMap();
|
|
46240
|
+
}
|
|
46241
|
+
|
|
45969
46242
|
/**
|
|
45970
46243
|
* Grades the item.
|
|
46244
|
+
*
|
|
46245
|
+
* @deprecated use scorePerseusItem
|
|
45971
46246
|
*/
|
|
45972
46247
|
scoreInput() {
|
|
45973
|
-
const
|
|
45974
|
-
const
|
|
45975
|
-
const score = guessAndScore[1];
|
|
46248
|
+
const guess = this.getUserInput();
|
|
46249
|
+
const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
|
|
45976
46250
|
|
|
45977
46251
|
// Continue to include an empty guess for the now defunct answer area.
|
|
45978
46252
|
// TODO(alex): Check whether we rely on the format here for
|
|
45979
46253
|
// analyzing ProblemLogs. If not, remove this layer.
|
|
45980
|
-
const maxCompatGuess = [
|
|
46254
|
+
const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
|
|
45981
46255
|
const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
|
|
45982
46256
|
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
45983
46257
|
this.setState({
|
|
@@ -46167,7 +46441,7 @@ const traverseRenderer$1 = function (rendererOptions, contentCallback, deepWidge
|
|
|
46167
46441
|
newContent = modifiedContent;
|
|
46168
46442
|
}
|
|
46169
46443
|
}
|
|
46170
|
-
const newWidgets =
|
|
46444
|
+
const newWidgets = mapObject(rendererOptions.widgets || {}, function (widgetInfo, widgetId) {
|
|
46171
46445
|
// Widgets without info or a type are empty widgets, and
|
|
46172
46446
|
// should always be renderable. It's also annoying to write
|
|
46173
46447
|
// checks for this everywhere, so we just filter them out once and
|
|
@@ -47839,138 +48113,6 @@ const registerAllWidgetsForTesting = () => {
|
|
|
47839
48113
|
replaceDeprecatedWidgets();
|
|
47840
48114
|
};
|
|
47841
48115
|
|
|
47842
|
-
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
47843
|
-
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
47844
|
-
|
|
47845
|
-
/**
|
|
47846
|
-
* Regex for widget placeholders in a string.
|
|
47847
|
-
*
|
|
47848
|
-
* First capture group is the widget ID (ex. 'radio 1')
|
|
47849
|
-
* Second capture group is the widget type (ex. "radio)
|
|
47850
|
-
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
47851
|
-
*/
|
|
47852
|
-
function getWidgetRegex() {
|
|
47853
|
-
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
47854
|
-
}
|
|
47855
|
-
|
|
47856
|
-
/**
|
|
47857
|
-
* Add a widget placeholder using the widget ID.
|
|
47858
|
-
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
47859
|
-
*
|
|
47860
|
-
* @param {string} id
|
|
47861
|
-
* @returns {string}
|
|
47862
|
-
*/
|
|
47863
|
-
function addWidget(id) {
|
|
47864
|
-
return `[[☃ ${id}]]`;
|
|
47865
|
-
}
|
|
47866
|
-
|
|
47867
|
-
/**
|
|
47868
|
-
* Get a widget type by a widget's ID
|
|
47869
|
-
* (please don't derive type from ID)
|
|
47870
|
-
*
|
|
47871
|
-
* @param {string} widgetId the ID of the widget
|
|
47872
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47873
|
-
* @returns {string} the widget type (ie "radio")
|
|
47874
|
-
*/
|
|
47875
|
-
function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
|
|
47876
|
-
const widget = widgetMap[WidgetId];
|
|
47877
|
-
return widget?.type ?? null;
|
|
47878
|
-
}
|
|
47879
|
-
|
|
47880
|
-
/**
|
|
47881
|
-
* Does the content have a specific type of widget?
|
|
47882
|
-
*
|
|
47883
|
-
* @param {string} type the type of the widget in question (ie "radio")
|
|
47884
|
-
* @param {string} content the string to search through
|
|
47885
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47886
|
-
* @returns {boolean} if the content includes the widget type
|
|
47887
|
-
*/
|
|
47888
|
-
function contentHasWidgetType(type, content, widgetMap) {
|
|
47889
|
-
return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
|
|
47890
|
-
}
|
|
47891
|
-
|
|
47892
|
-
/**
|
|
47893
|
-
* Extract all widget IDs, which includes the widget type and instance number.
|
|
47894
|
-
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
47895
|
-
*
|
|
47896
|
-
* Content should contain Perseus widget placeholders,
|
|
47897
|
-
* which look like: '[[☃ radio 1]]'.
|
|
47898
|
-
*
|
|
47899
|
-
* @param {string} content
|
|
47900
|
-
* @returns {ReadonlyArray<string>} widgetIds
|
|
47901
|
-
*/
|
|
47902
|
-
function getWidgetIdsFromContent(content) {
|
|
47903
|
-
const widgets = [];
|
|
47904
|
-
const localWidgetRegex = getWidgetRegex();
|
|
47905
|
-
let match = localWidgetRegex.exec(content);
|
|
47906
|
-
while (match !== null) {
|
|
47907
|
-
widgets.push(match[1]);
|
|
47908
|
-
match = localWidgetRegex.exec(content);
|
|
47909
|
-
}
|
|
47910
|
-
return widgets;
|
|
47911
|
-
}
|
|
47912
|
-
|
|
47913
|
-
/**
|
|
47914
|
-
* Get a list of widget IDs from content,
|
|
47915
|
-
* but only for specific widget types
|
|
47916
|
-
*
|
|
47917
|
-
* @param {string} type the type of widget (ie "radio")
|
|
47918
|
-
* @param {string} content the string to parse
|
|
47919
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47920
|
-
* @returns {ReadonlyArray<string>} the widget type (ie "radio")
|
|
47921
|
-
*/
|
|
47922
|
-
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
47923
|
-
const rv = [];
|
|
47924
|
-
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
47925
|
-
widgetIdsInContent.forEach(widgetId => {
|
|
47926
|
-
const widget = widgetMap[widgetId];
|
|
47927
|
-
if (widget?.type === type) {
|
|
47928
|
-
rv.push(widgetId);
|
|
47929
|
-
}
|
|
47930
|
-
});
|
|
47931
|
-
return rv;
|
|
47932
|
-
}
|
|
47933
|
-
|
|
47934
|
-
/**
|
|
47935
|
-
* Pull the widget map out of ItemData
|
|
47936
|
-
*
|
|
47937
|
-
* @param {PerseusItem} PerseusItem containing a widgetMap
|
|
47938
|
-
* @returns {WidgetMap} the widget map in the PerseusItem
|
|
47939
|
-
*/
|
|
47940
|
-
function getWidgetsMapFromItemData(itemData) {
|
|
47941
|
-
return itemData.question.widgets;
|
|
47942
|
-
}
|
|
47943
|
-
|
|
47944
|
-
/**
|
|
47945
|
-
* Get the widget information from a WidgetMap
|
|
47946
|
-
*
|
|
47947
|
-
* @param {string} widgetId the ID of the widget
|
|
47948
|
-
* @returns {PerseusWidget | null} the widget data if it exists, otherwise null
|
|
47949
|
-
*/
|
|
47950
|
-
function getWidgetFromWidgetMap(widgetId, widgetMap) {
|
|
47951
|
-
return widgetMap[widgetId] ?? null;
|
|
47952
|
-
}
|
|
47953
|
-
|
|
47954
|
-
/**
|
|
47955
|
-
* Get select widgets from a widget map.
|
|
47956
|
-
* Useful for multi-items that needs to split
|
|
47957
|
-
* one PerseusItem into several
|
|
47958
|
-
*
|
|
47959
|
-
* @param {ReadonlyArray<string>} widgetIds to extract
|
|
47960
|
-
* @param {PerseusWidgetsMap} widgetMap to extract from
|
|
47961
|
-
* @return {PerseusWidgetsMap} a new widget map with requested widgets
|
|
47962
|
-
*/
|
|
47963
|
-
function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
|
|
47964
|
-
const widgets = {};
|
|
47965
|
-
widgetIds.forEach(widgetId => {
|
|
47966
|
-
const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
|
|
47967
|
-
if (widget) {
|
|
47968
|
-
widgets[widgetId] = widget;
|
|
47969
|
-
}
|
|
47970
|
-
});
|
|
47971
|
-
return widgets;
|
|
47972
|
-
}
|
|
47973
|
-
|
|
47974
48116
|
/**
|
|
47975
48117
|
* This function extracts the answers from the widgets.
|
|
47976
48118
|
*
|
|
@@ -48909,7 +49051,7 @@ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
|
|
|
48909
49051
|
const parseDefinitionWidget = parseWidget(constant("definition"), object({
|
|
48910
49052
|
togglePrompt: string,
|
|
48911
49053
|
definition: string,
|
|
48912
|
-
static: boolean
|
|
49054
|
+
static: defaulted(boolean, () => false)
|
|
48913
49055
|
}));
|
|
48914
49056
|
|
|
48915
49057
|
const parseDropdownWidget = parseWidget(constant("dropdown"), object({
|
|
@@ -49112,15 +49254,26 @@ const parseIframeWidget = parseWidget(constant("iframe"), object({
|
|
|
49112
49254
|
static: defaulted(boolean, () => false)
|
|
49113
49255
|
}));
|
|
49114
49256
|
|
|
49257
|
+
const stringToNumber = (rawValue, ctx) => {
|
|
49258
|
+
if (typeof rawValue === "number") {
|
|
49259
|
+
return ctx.success(rawValue);
|
|
49260
|
+
}
|
|
49261
|
+
const parsedNumber = +rawValue;
|
|
49262
|
+
if (rawValue === "" || isNaN(parsedNumber)) {
|
|
49263
|
+
return ctx.failure("a number or numeric string", rawValue);
|
|
49264
|
+
}
|
|
49265
|
+
return ctx.success(parsedNumber);
|
|
49266
|
+
};
|
|
49267
|
+
|
|
49268
|
+
const numericToNumber = pipeParsers(union(number).or(string).parser).then(stringToNumber).parser;
|
|
49115
49269
|
const parsePerseusImageBackground = object({
|
|
49116
49270
|
url: optional(nullable(string)),
|
|
49117
|
-
width: optional(
|
|
49118
|
-
height: optional(
|
|
49119
|
-
top: optional(
|
|
49120
|
-
left: optional(
|
|
49121
|
-
bottom: optional(
|
|
49122
|
-
|
|
49123
|
-
scale: optional(union(number).or(string).parser)
|
|
49271
|
+
width: optional(numericToNumber),
|
|
49272
|
+
height: optional(numericToNumber),
|
|
49273
|
+
top: optional(numericToNumber),
|
|
49274
|
+
left: optional(numericToNumber),
|
|
49275
|
+
bottom: optional(numericToNumber),
|
|
49276
|
+
scale: optional(numericToNumber)
|
|
49124
49277
|
});
|
|
49125
49278
|
|
|
49126
49279
|
const pairOfNumbers$2 = pair(number, number);
|
|
@@ -49139,6 +49292,12 @@ const parseImageWidget = parseWidget(constant("image"), object({
|
|
|
49139
49292
|
box: optional(pairOfNumbers$2)
|
|
49140
49293
|
}));
|
|
49141
49294
|
|
|
49295
|
+
const booleanToString = (rawValue, ctx) => {
|
|
49296
|
+
if (typeof rawValue === "boolean") {
|
|
49297
|
+
return ctx.success(String(rawValue));
|
|
49298
|
+
}
|
|
49299
|
+
return ctx.failure("boolean", rawValue);
|
|
49300
|
+
};
|
|
49142
49301
|
const parseInputNumberWidget = parseWidget(constant("input-number"), object({
|
|
49143
49302
|
answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
|
|
49144
49303
|
inexact: optional(boolean),
|
|
@@ -49146,7 +49305,11 @@ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
|
|
|
49146
49305
|
rightAlign: optional(boolean),
|
|
49147
49306
|
simplify: enumeration("required", "optional", "enforced"),
|
|
49148
49307
|
size: enumeration("normal", "small"),
|
|
49149
|
-
|
|
49308
|
+
// TODO(benchristel): there are some content items where value is a
|
|
49309
|
+
// boolean, even though that makes no sense. We should figure out if
|
|
49310
|
+
// those content items are actually published anywhere, and consider
|
|
49311
|
+
// updating them.
|
|
49312
|
+
value: union(number).or(string).or(booleanToString).parser,
|
|
49150
49313
|
customKeypad: optional(boolean)
|
|
49151
49314
|
}));
|
|
49152
49315
|
|
|
@@ -49444,18 +49607,27 @@ const parseLockedFunctionType = object({
|
|
|
49444
49607
|
const parseLockedFigure = union(parseLockedPointType).or(parseLockedLineType).or(parseLockedVectorType).or(parseLockedEllipseType).or(parseLockedPolygonType).or(parseLockedFunctionType).or(parseLockedLabelType).parser;
|
|
49445
49608
|
const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
|
|
49446
49609
|
step: pairOfNumbers,
|
|
49447
|
-
|
|
49448
|
-
|
|
49610
|
+
// TODO(benchristel): rather than making gridStep and snapStep
|
|
49611
|
+
// optional, we should duplicate the defaulting logic from the
|
|
49612
|
+
// InteractiveGraph component. See parse-perseus-json/README.md for
|
|
49613
|
+
// why.
|
|
49614
|
+
gridStep: optional(pairOfNumbers),
|
|
49615
|
+
snapStep: optional(pairOfNumbers),
|
|
49449
49616
|
backgroundImage: optional(parsePerseusImageBackground),
|
|
49450
49617
|
markings: enumeration("graph", "grid", "none"),
|
|
49451
|
-
labels: array(string),
|
|
49618
|
+
labels: optional(array(string)),
|
|
49452
49619
|
showProtractor: boolean,
|
|
49453
49620
|
showRuler: optional(boolean),
|
|
49454
49621
|
showTooltips: optional(boolean),
|
|
49455
49622
|
rulerLabel: optional(string),
|
|
49456
49623
|
rulerTicks: optional(number),
|
|
49457
49624
|
range: pair(pairOfNumbers, pairOfNumbers),
|
|
49458
|
-
|
|
49625
|
+
// NOTE(benchristel): I copied the default graph from
|
|
49626
|
+
// interactive-graph.tsx. See the parse-perseus-json/README.md for
|
|
49627
|
+
// an explanation of why we want to duplicate the default here.
|
|
49628
|
+
graph: defaulted(parsePerseusGraphType, () => ({
|
|
49629
|
+
type: "linear"
|
|
49630
|
+
})),
|
|
49459
49631
|
correct: parsePerseusGraphType,
|
|
49460
49632
|
// TODO(benchristel): default lockedFigures to empty array
|
|
49461
49633
|
lockedFigures: optional(array(parseLockedFigure)),
|
|
@@ -49523,7 +49695,10 @@ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
|
|
|
49523
49695
|
isTickCtrl: optional(nullable(boolean)),
|
|
49524
49696
|
divisionRange: array(number),
|
|
49525
49697
|
numDivisions: optional(nullable(number)),
|
|
49526
|
-
|
|
49698
|
+
// NOTE(benchristel): I copied the default snapDivisions from
|
|
49699
|
+
// number-line.tsx. See the parse-perseus-json/README.md for
|
|
49700
|
+
// an explanation of why we want to duplicate the default here.
|
|
49701
|
+
snapDivisions: defaulted(number, () => 2),
|
|
49527
49702
|
tickStep: optional(nullable(number)),
|
|
49528
49703
|
correctRel: optional(nullable(string)),
|
|
49529
49704
|
correctX: number,
|
|
@@ -49536,18 +49711,18 @@ const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "d
|
|
|
49536
49711
|
const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
|
|
49537
49712
|
answers: array(object({
|
|
49538
49713
|
message: string,
|
|
49539
|
-
value: number,
|
|
49714
|
+
value: optional(number),
|
|
49540
49715
|
status: string,
|
|
49541
49716
|
answerForms: optional(array(parseMathFormat)),
|
|
49542
49717
|
strict: boolean,
|
|
49543
49718
|
maxError: optional(nullable(number)),
|
|
49544
49719
|
simplify: optional(nullable(string))
|
|
49545
49720
|
})),
|
|
49546
|
-
labelText: string,
|
|
49721
|
+
labelText: optional(string),
|
|
49547
49722
|
size: string,
|
|
49548
49723
|
coefficient: boolean,
|
|
49549
49724
|
rightAlign: optional(boolean),
|
|
49550
|
-
static: boolean,
|
|
49725
|
+
static: defaulted(boolean, () => false),
|
|
49551
49726
|
answerForms: optional(array(object({
|
|
49552
49727
|
name: parseMathFormat,
|
|
49553
49728
|
simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
|
|
@@ -49560,12 +49735,18 @@ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
|
|
|
49560
49735
|
function parseRenderer(rawValue, ctx) {
|
|
49561
49736
|
return parsePerseusRenderer(rawValue, ctx);
|
|
49562
49737
|
}
|
|
49738
|
+
const largeToAuto = (height, ctx) => {
|
|
49739
|
+
if (height === "large") {
|
|
49740
|
+
return ctx.success("auto");
|
|
49741
|
+
}
|
|
49742
|
+
return ctx.success(height);
|
|
49743
|
+
};
|
|
49563
49744
|
const parseOrdererWidget = parseWidget(constant("orderer"), object({
|
|
49564
|
-
options: array(parseRenderer),
|
|
49745
|
+
options: defaulted(array(parseRenderer), () => []),
|
|
49565
49746
|
correctOptions: array(parseRenderer),
|
|
49566
49747
|
otherOptions: array(parseRenderer),
|
|
49567
|
-
height: enumeration("normal", "auto"),
|
|
49568
|
-
layout: enumeration("horizontal", "vertical")
|
|
49748
|
+
height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
|
|
49749
|
+
layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
|
|
49569
49750
|
}));
|
|
49570
49751
|
|
|
49571
49752
|
const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
|
|
@@ -49579,7 +49760,7 @@ const parsePassageWidget = parseWidget(constant("passage"), object({
|
|
|
49579
49760
|
passageText: string,
|
|
49580
49761
|
passageTitle: string,
|
|
49581
49762
|
showLineNumbers: boolean,
|
|
49582
|
-
static: boolean
|
|
49763
|
+
static: defaulted(boolean, () => false)
|
|
49583
49764
|
}));
|
|
49584
49765
|
|
|
49585
49766
|
const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
|
|
@@ -49600,7 +49781,10 @@ const parsePlotterWidget = parseWidget(constant("plotter"), object({
|
|
|
49600
49781
|
picUrl: optional(nullable(string)),
|
|
49601
49782
|
picSize: optional(nullable(number)),
|
|
49602
49783
|
picBoxHeight: optional(nullable(number)),
|
|
49603
|
-
|
|
49784
|
+
// NOTE(benchristel): I copied the default plotDimensions from
|
|
49785
|
+
// plotter.tsx. See the parse-perseus-json/README.md for an explanation
|
|
49786
|
+
// of why we want to duplicate the defaults here.
|
|
49787
|
+
plotDimensions: defaulted(array(number), () => [380, 300])
|
|
49604
49788
|
}));
|
|
49605
49789
|
|
|
49606
49790
|
const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
|
|
@@ -49753,6 +49937,10 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
|
|
|
49753
49937
|
return parseAndAssign(`table ${id}`, parseTableWidget);
|
|
49754
49938
|
case "video":
|
|
49755
49939
|
return parseAndAssign(`video ${id}`, parseVideoWidget);
|
|
49940
|
+
case "sequence":
|
|
49941
|
+
// sequence is a deprecated widget type, and the corresponding
|
|
49942
|
+
// widget component no longer exists.
|
|
49943
|
+
return parseAndAssign(`sequence ${id}`, parseDeprecatedWidget);
|
|
49756
49944
|
default:
|
|
49757
49945
|
if (getWidget(type)) {
|
|
49758
49946
|
// @ts-expect-error - 'type' is not a valid widget type
|
|
@@ -49761,6 +49949,11 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
|
|
|
49761
49949
|
return ctx.failure("a valid widget type", type);
|
|
49762
49950
|
}
|
|
49763
49951
|
};
|
|
49952
|
+
const parseDeprecatedWidget = parseWidget(
|
|
49953
|
+
// Ignore the incoming widget type and hardcode "deprecated-standin"
|
|
49954
|
+
(_, ctx) => ctx.success("deprecated-standin"),
|
|
49955
|
+
// Allow any widget options
|
|
49956
|
+
object({}));
|
|
49764
49957
|
const parseStringToPositiveInt = (rawValue, ctx) => {
|
|
49765
49958
|
if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
|
|
49766
49959
|
return ctx.failure("numeric string", rawValue);
|
|
@@ -50140,6 +50333,7 @@ exports.parsePerseusItem = parsePerseusItem;
|
|
|
50140
50333
|
exports.plotterPlotTypes = plotterPlotTypes;
|
|
50141
50334
|
exports.preprocessTex = preprocessTex;
|
|
50142
50335
|
exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
|
|
50336
|
+
exports.scorePerseusItem = scorePerseusItem;
|
|
50143
50337
|
exports.shapes = shapes;
|
|
50144
50338
|
exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
|
|
50145
50339
|
exports.traverse = traverse;
|