@khanacademy/perseus 41.5.0 → 42.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/index.js +325 -268
- package/dist/es/index.js.map +1 -1
- package/dist/es/strings.js +3 -2
- package/dist/es/strings.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +322 -263
- package/dist/index.js.map +1 -1
- package/dist/renderer-util.d.ts +3 -2
- package/dist/renderer.d.ts +7 -10
- package/dist/server-item-renderer.d.ts +33 -2
- package/dist/strings.d.ts +2 -1
- package/dist/strings.js +3 -2
- package/dist/strings.js.map +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/util/parse-perseus-json/exhaustive-test-tool/index.d.ts +2 -0
- package/dist/widget-ai-utils/graded-group/{prompt-utils.d.ts → graded-group-ai-utils.d.ts} +1 -1
- package/dist/widget-ai-utils/graded-group-set/{prompt-utils.d.ts → graded-group-set-ai-utils.d.ts} +1 -1
- package/dist/widget-ai-utils/group/{prompt-utils.d.ts → group-ai-utils.d.ts} +1 -1
- package/dist/widget-ai-utils/prompt-types.d.ts +33 -0
- package/dist/widget-ai-utils/unsupported-widget.d.ts +1 -1
- package/dist/widgets/categorizer/categorizer.d.ts +1 -1
- package/dist/widgets/definition/definition.d.ts +1 -1
- package/dist/widgets/dropdown/dropdown.d.ts +1 -1
- package/dist/widgets/explanation/explanation.d.ts +1 -1
- package/dist/widgets/expression/expression.d.ts +1 -1
- package/dist/widgets/graded-group/graded-group.d.ts +1 -1
- package/dist/widgets/graded-group-set/graded-group-set.d.ts +1 -1
- package/dist/widgets/grapher/grapher.d.ts +1 -1
- package/dist/widgets/group/group.d.ts +1 -1
- package/dist/widgets/image/image.d.ts +1 -1
- package/dist/widgets/input-number/input-number.d.ts +1 -1
- package/dist/widgets/interactive-graph.d.ts +1 -1
- package/dist/widgets/interactive-graphs/graphs/components/movable-point.d.ts +9 -0
- package/dist/widgets/interactive-graphs/graphs/components/use-control-point.d.ts +9 -0
- package/dist/widgets/label-image/label-image.d.ts +1 -1
- package/dist/widgets/matcher/matcher.d.ts +1 -1
- package/dist/widgets/matrix/matrix.d.ts +1 -1
- package/dist/widgets/number-line/number-line.d.ts +1 -1
- package/dist/widgets/orderer/orderer.d.ts +1 -1
- package/dist/widgets/passage/passage.d.ts +1 -1
- package/dist/widgets/passage-ref/passage-ref.d.ts +1 -1
- package/dist/widgets/radio/radio-component.d.ts +1 -1
- package/dist/widgets/sorter/sorter.d.ts +1 -1
- package/package.json +1 -1
- package/dist/prompt-types.d.ts +0 -33
- /package/dist/widget-ai-utils/categorizer/{prompt-utils.d.ts → categorizer-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/cs-program/{prompt-utils.d.ts → cs-program-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/definition/{prompt-utils.d.ts → definition-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/dropdown/{prompt-utils.d.ts → dropdown-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/explanation/{prompt-utils.d.ts → explanation-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/expression/{prompt-utils.d.ts → expression-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/grapher/{prompt-utils.d.ts → grapher-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/iframe/{prompt-utils.d.ts → iframe-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/image/{prompt-utils.d.ts → image-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/input-number/{prompt-utils.d.ts → input-number-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/interaction/{prompt-utils.d.ts → interaction-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/interactive-graph/{prompt-utils.d.ts → interactive-graph-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/label-image/{prompt-utils.d.ts → label-image-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/matcher/{prompt-utils.d.ts → matcher-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/matrix/{prompt-utils.d.ts → matrix-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/measurer/{prompt-utils.d.ts → measurer-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/number-line/{prompt-utils.d.ts → number-line-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/orderer/{prompt-utils.d.ts → orderer-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/passage/{prompt-utils.d.ts → passage-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/passage-ref/{prompt-utils.d.ts → passage-ref-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/phet-simulation/{prompt-utils.d.ts → phet-simulation-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/plotter/{prompt-utils.d.ts → plotter-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/python-program/{prompt-utils.d.ts → python-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/radio/{prompt-utils.d.ts → radio-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/sorter/{prompt-utils.d.ts → sorter-ai-utils.d.ts} +0 -0
- /package/dist/widget-ai-utils/video/{prompt-utils.d.ts → video-ai-utils.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -371,10 +371,11 @@ const mockStrings = {
|
|
|
371
371
|
graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
|
|
372
372
|
srPointAtCoordinates: _ref27 => {
|
|
373
373
|
let {
|
|
374
|
+
num,
|
|
374
375
|
x,
|
|
375
376
|
y
|
|
376
377
|
} = _ref27;
|
|
377
|
-
return `Point at ${x} comma ${y}`;
|
|
378
|
+
return `Point ${num} at ${x} comma ${y}`;
|
|
378
379
|
},
|
|
379
380
|
srInteractiveElements: _ref28 => {
|
|
380
381
|
let {
|
|
@@ -18874,6 +18875,138 @@ class QuestionParagraph extends React__namespace.Component {
|
|
|
18874
18875
|
}
|
|
18875
18876
|
}
|
|
18876
18877
|
|
|
18878
|
+
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
18879
|
+
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
18880
|
+
|
|
18881
|
+
/**
|
|
18882
|
+
* Regex for widget placeholders in a string.
|
|
18883
|
+
*
|
|
18884
|
+
* First capture group is the widget ID (ex. 'radio 1')
|
|
18885
|
+
* Second capture group is the widget type (ex. "radio)
|
|
18886
|
+
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
18887
|
+
*/
|
|
18888
|
+
function getWidgetRegex() {
|
|
18889
|
+
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
18890
|
+
}
|
|
18891
|
+
|
|
18892
|
+
/**
|
|
18893
|
+
* Add a widget placeholder using the widget ID.
|
|
18894
|
+
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
18895
|
+
*
|
|
18896
|
+
* @param {string} id
|
|
18897
|
+
* @returns {string}
|
|
18898
|
+
*/
|
|
18899
|
+
function addWidget(id) {
|
|
18900
|
+
return `[[☃ ${id}]]`;
|
|
18901
|
+
}
|
|
18902
|
+
|
|
18903
|
+
/**
|
|
18904
|
+
* Get a widget type by a widget's ID
|
|
18905
|
+
* (please don't derive type from ID)
|
|
18906
|
+
*
|
|
18907
|
+
* @param {string} widgetId the ID of the widget
|
|
18908
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
18909
|
+
* @returns {string} the widget type (ie "radio")
|
|
18910
|
+
*/
|
|
18911
|
+
function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
|
|
18912
|
+
const widget = widgetMap[WidgetId];
|
|
18913
|
+
return widget?.type ?? null;
|
|
18914
|
+
}
|
|
18915
|
+
|
|
18916
|
+
/**
|
|
18917
|
+
* Does the content have a specific type of widget?
|
|
18918
|
+
*
|
|
18919
|
+
* @param {string} type the type of the widget in question (ie "radio")
|
|
18920
|
+
* @param {string} content the string to search through
|
|
18921
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
18922
|
+
* @returns {boolean} if the content includes the widget type
|
|
18923
|
+
*/
|
|
18924
|
+
function contentHasWidgetType(type, content, widgetMap) {
|
|
18925
|
+
return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
|
|
18926
|
+
}
|
|
18927
|
+
|
|
18928
|
+
/**
|
|
18929
|
+
* Extract all widget IDs, which includes the widget type and instance number.
|
|
18930
|
+
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
18931
|
+
*
|
|
18932
|
+
* Content should contain Perseus widget placeholders,
|
|
18933
|
+
* which look like: '[[☃ radio 1]]'.
|
|
18934
|
+
*
|
|
18935
|
+
* @param {string} content
|
|
18936
|
+
* @returns {ReadonlyArray<string>} widgetIds
|
|
18937
|
+
*/
|
|
18938
|
+
function getWidgetIdsFromContent(content) {
|
|
18939
|
+
const widgets = [];
|
|
18940
|
+
const localWidgetRegex = getWidgetRegex();
|
|
18941
|
+
let match = localWidgetRegex.exec(content);
|
|
18942
|
+
while (match !== null) {
|
|
18943
|
+
widgets.push(match[1]);
|
|
18944
|
+
match = localWidgetRegex.exec(content);
|
|
18945
|
+
}
|
|
18946
|
+
return widgets;
|
|
18947
|
+
}
|
|
18948
|
+
|
|
18949
|
+
/**
|
|
18950
|
+
* Get a list of widget IDs from content,
|
|
18951
|
+
* but only for specific widget types
|
|
18952
|
+
*
|
|
18953
|
+
* @param {string} type the type of widget (ie "radio")
|
|
18954
|
+
* @param {string} content the string to parse
|
|
18955
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
18956
|
+
* @returns {ReadonlyArray<string>} the widget type (ie "radio")
|
|
18957
|
+
*/
|
|
18958
|
+
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
18959
|
+
const rv = [];
|
|
18960
|
+
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
18961
|
+
widgetIdsInContent.forEach(widgetId => {
|
|
18962
|
+
const widget = widgetMap[widgetId];
|
|
18963
|
+
if (widget?.type === type) {
|
|
18964
|
+
rv.push(widgetId);
|
|
18965
|
+
}
|
|
18966
|
+
});
|
|
18967
|
+
return rv;
|
|
18968
|
+
}
|
|
18969
|
+
|
|
18970
|
+
/**
|
|
18971
|
+
* Pull the widget map out of ItemData
|
|
18972
|
+
*
|
|
18973
|
+
* @param {PerseusItem} PerseusItem containing a widgetMap
|
|
18974
|
+
* @returns {WidgetMap} the widget map in the PerseusItem
|
|
18975
|
+
*/
|
|
18976
|
+
function getWidgetsMapFromItemData(itemData) {
|
|
18977
|
+
return itemData.question.widgets;
|
|
18978
|
+
}
|
|
18979
|
+
|
|
18980
|
+
/**
|
|
18981
|
+
* Get the widget information from a WidgetMap
|
|
18982
|
+
*
|
|
18983
|
+
* @param {string} widgetId the ID of the widget
|
|
18984
|
+
* @returns {PerseusWidget | null} the widget data if it exists, otherwise null
|
|
18985
|
+
*/
|
|
18986
|
+
function getWidgetFromWidgetMap(widgetId, widgetMap) {
|
|
18987
|
+
return widgetMap[widgetId] ?? null;
|
|
18988
|
+
}
|
|
18989
|
+
|
|
18990
|
+
/**
|
|
18991
|
+
* Get select widgets from a widget map.
|
|
18992
|
+
* Useful for multi-items that needs to split
|
|
18993
|
+
* one PerseusItem into several
|
|
18994
|
+
*
|
|
18995
|
+
* @param {ReadonlyArray<string>} widgetIds to extract
|
|
18996
|
+
* @param {PerseusWidgetsMap} widgetMap to extract from
|
|
18997
|
+
* @return {PerseusWidgetsMap} a new widget map with requested widgets
|
|
18998
|
+
*/
|
|
18999
|
+
function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
|
|
19000
|
+
const widgets = {};
|
|
19001
|
+
widgetIds.forEach(widgetId => {
|
|
19002
|
+
const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
|
|
19003
|
+
if (widget) {
|
|
19004
|
+
widgets[widgetId] = widget;
|
|
19005
|
+
}
|
|
19006
|
+
});
|
|
19007
|
+
return widgets;
|
|
19008
|
+
}
|
|
19009
|
+
|
|
18877
19010
|
const DEFAULT_ALIGNMENT$1 = "block";
|
|
18878
19011
|
// NOTE(kevinb): "default" is not one in `validAlignments`.
|
|
18879
19012
|
const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
|
|
@@ -19334,6 +19467,20 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19334
19467
|
}
|
|
19335
19468
|
});
|
|
19336
19469
|
}
|
|
19470
|
+
|
|
19471
|
+
// TODO: combine scorePerseusItem with scoreWidgetsFunctional
|
|
19472
|
+
// once scorePerseusItem is the only one calling scoreWidgetsFunctional
|
|
19473
|
+
function scorePerseusItem(perseusRenderData, userInputMap,
|
|
19474
|
+
// TODO(LEMS-2461,LEMS-2391): these probably
|
|
19475
|
+
// need to be removed before we move this to the server
|
|
19476
|
+
strings, locale) {
|
|
19477
|
+
// There seems to be a chance that PerseusRenderer.widgets might include
|
|
19478
|
+
// widget data for widgets that are not in PerseusRenderer.content,
|
|
19479
|
+
// so this checks that the widgets are being used before scoring them
|
|
19480
|
+
const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
|
|
19481
|
+
const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
|
|
19482
|
+
return Util.flattenScores(scores);
|
|
19483
|
+
}
|
|
19337
19484
|
function scoreWidgetsFunctional(widgets,
|
|
19338
19485
|
// This is a port of old code, I'm not sure why
|
|
19339
19486
|
// we need widgetIds vs the keys of the widgets object
|
|
@@ -19354,7 +19501,7 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19354
19501
|
const userInput = userInputMap[id];
|
|
19355
19502
|
const scorer = getWidgetScorer(widget.type);
|
|
19356
19503
|
if (widget.type === "group") {
|
|
19357
|
-
const scores = scoreWidgetsFunctional(widget.options.widgets,
|
|
19504
|
+
const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
|
|
19358
19505
|
widgetScores[id] = Util.flattenScores(scores);
|
|
19359
19506
|
} else if (scorer) {
|
|
19360
19507
|
widgetScores[id] = scorer(userInput, widget.options, strings, locale);
|
|
@@ -20403,7 +20550,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20403
20550
|
// Instead, we center them with css magic in styles.less
|
|
20404
20551
|
// /cry(aria)
|
|
20405
20552
|
this._foundTextNodes = true;
|
|
20406
|
-
if (
|
|
20553
|
+
if (this.widgetIds.includes(node.id)) {
|
|
20407
20554
|
// We don't want to render a duplicate widget key/ref,
|
|
20408
20555
|
// as this causes problems with react (for obvious
|
|
20409
20556
|
// reasons). Instead we just notify the
|
|
@@ -20728,7 +20875,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20728
20875
|
};
|
|
20729
20876
|
getInputPaths = () => {
|
|
20730
20877
|
const inputPaths = [];
|
|
20731
|
-
|
|
20878
|
+
this.widgetIds.forEach(widgetId => {
|
|
20732
20879
|
const widget = this.getWidgetInstance(widgetId);
|
|
20733
20880
|
if (widget && widget.getInputPaths) {
|
|
20734
20881
|
// Grab all input paths and add widgetID to the front
|
|
@@ -20736,7 +20883,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20736
20883
|
// Prefix paths with their widgetID and add to collective
|
|
20737
20884
|
// list of paths.
|
|
20738
20885
|
// @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
|
|
20739
|
-
|
|
20886
|
+
widgetInputPaths.forEach(inputPath => {
|
|
20740
20887
|
const relativeInputPath = [widgetId].concat(inputPath);
|
|
20741
20888
|
inputPaths.push(relativeInputPath);
|
|
20742
20889
|
});
|
|
@@ -20929,33 +21076,30 @@ class Renderer extends React__namespace.Component {
|
|
|
20929
21076
|
}
|
|
20930
21077
|
|
|
20931
21078
|
/**
|
|
20932
|
-
*
|
|
20933
|
-
*
|
|
20934
|
-
*
|
|
20935
|
-
*/
|
|
20936
|
-
scoreWidgets() {
|
|
20937
|
-
return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
20938
|
-
}
|
|
20939
|
-
|
|
20940
|
-
/**
|
|
20941
|
-
* Grades the content.
|
|
21079
|
+
* Scores the content.
|
|
21080
|
+
*
|
|
21081
|
+
* @deprecated use scorePerseusItem
|
|
20942
21082
|
*/
|
|
20943
21083
|
score() {
|
|
20944
|
-
const scores = this.
|
|
21084
|
+
const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
20945
21085
|
const combinedScore = Util.flattenScores(scores);
|
|
20946
21086
|
return combinedScore;
|
|
20947
21087
|
}
|
|
20948
|
-
|
|
21088
|
+
|
|
21089
|
+
/**
|
|
21090
|
+
* @deprecated use scorePerseusItem
|
|
21091
|
+
*/
|
|
21092
|
+
guessAndScore = () => {
|
|
20949
21093
|
const totalGuess = this.getUserInput();
|
|
20950
21094
|
const totalScore = this.score();
|
|
20951
21095
|
return [totalGuess, totalScore];
|
|
20952
|
-
}
|
|
21096
|
+
};
|
|
20953
21097
|
examples = () => {
|
|
20954
21098
|
const widgetIds = this.widgetIds;
|
|
20955
|
-
const examples =
|
|
21099
|
+
const examples = widgetIds.map(widgetId => {
|
|
20956
21100
|
const widget = this.getWidgetInstance(widgetId);
|
|
20957
21101
|
return widget != null && widget.examples ? widget.examples() : null;
|
|
20958
|
-
}));
|
|
21102
|
+
}).filter(Boolean);
|
|
20959
21103
|
|
|
20960
21104
|
// no widgets with examples
|
|
20961
21105
|
if (!examples.length) {
|
|
@@ -22463,7 +22607,9 @@ const propUpgrades$2 = {
|
|
|
22463
22607
|
// input-number to numeric-input. In this case, we need to upgrade
|
|
22464
22608
|
// the widget options accordingly.
|
|
22465
22609
|
if (initialProps.value) {
|
|
22466
|
-
|
|
22610
|
+
// If the answerType is not number or percent, we need to provide
|
|
22611
|
+
// the answer form for the numeric-input widget
|
|
22612
|
+
const provideAnswerForm = initialProps.answerType !== "number" && initialProps.answerType !== "percent";
|
|
22467
22613
|
|
|
22468
22614
|
// We need to determine the mathFormat for the numeric-input widget
|
|
22469
22615
|
const mathFormat = initialProps.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
|
|
@@ -29528,6 +29674,9 @@ class Group extends React__namespace.Component {
|
|
|
29528
29674
|
this.change("widgets", this.rendererRef.props.widgets);
|
|
29529
29675
|
}
|
|
29530
29676
|
};
|
|
29677
|
+
|
|
29678
|
+
// TODO(LEMS-2391): replace this when there's a separate check
|
|
29679
|
+
// for valid/invalid state
|
|
29531
29680
|
const score = this.rendererRef?.score();
|
|
29532
29681
|
const isValid = score && score.type !== "invalid";
|
|
29533
29682
|
const isInvalid = score && score.type === "invalid";
|
|
@@ -31371,6 +31520,108 @@ function AxisArrows() {
|
|
|
31371
31520
|
})));
|
|
31372
31521
|
}
|
|
31373
31522
|
|
|
31523
|
+
/**
|
|
31524
|
+
* 44 is touch best practice and AAA compliant for WCAG
|
|
31525
|
+
* https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
|
|
31526
|
+
*/
|
|
31527
|
+
const TARGET_SIZE = 44;
|
|
31528
|
+
const REMOVE_BUTTON_ID = "perseus_mafs_remove_button";
|
|
31529
|
+
|
|
31530
|
+
// same as pointsFromNormalized in interactive-graph.tsx
|
|
31531
|
+
const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
|
|
31532
|
+
// maps over [x, y]
|
|
31533
|
+
coords.map((coord, i) => {
|
|
31534
|
+
const axisRange = range[i];
|
|
31535
|
+
if (noSnap) {
|
|
31536
|
+
return axisRange[MIN] + size(axisRange) * coord;
|
|
31537
|
+
}
|
|
31538
|
+
const axisStep = step[i];
|
|
31539
|
+
const nSteps = Math.floor(size(axisRange) / axisStep);
|
|
31540
|
+
const tick = Math.round(coord * nSteps);
|
|
31541
|
+
return axisRange[MIN] + axisStep * tick;
|
|
31542
|
+
}));
|
|
31543
|
+
|
|
31544
|
+
// same as normalizeCoords in interactive-graph.tsx
|
|
31545
|
+
const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
|
|
31546
|
+
return (coord + ranges[i][1]) / size(ranges[i]);
|
|
31547
|
+
}));
|
|
31548
|
+
|
|
31549
|
+
// Returns the closest point to the given `point` that is within the graph
|
|
31550
|
+
// bounds given in `state`.
|
|
31551
|
+
function bound$1(_ref) {
|
|
31552
|
+
let {
|
|
31553
|
+
snapStep,
|
|
31554
|
+
range,
|
|
31555
|
+
point
|
|
31556
|
+
} = _ref;
|
|
31557
|
+
const boundingBox = inset(snapStep, range);
|
|
31558
|
+
return clampToBox(boundingBox, point);
|
|
31559
|
+
}
|
|
31560
|
+
function isUnlimitedGraphState(state) {
|
|
31561
|
+
return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
|
|
31562
|
+
}
|
|
31563
|
+
|
|
31564
|
+
/**
|
|
31565
|
+
* Replace all text outside of the $ TeX blocks with `\\text{...}`
|
|
31566
|
+
* This way, the entire resulting string can be rendered within <TeX>
|
|
31567
|
+
* and the text outside of the $ blocks will be non-TeX text.
|
|
31568
|
+
*/
|
|
31569
|
+
function replaceOutsideTeX(mathString) {
|
|
31570
|
+
// All the information we need is in the first section,
|
|
31571
|
+
// whether it's typed as "blockmath" or "paragraph"
|
|
31572
|
+
const firstSection = SimpleMarkdown__namespace.parse(mathString)[0];
|
|
31573
|
+
|
|
31574
|
+
// If it's blockMath, the outer level has the full math content.
|
|
31575
|
+
if (firstSection.type === "blockMath") {
|
|
31576
|
+
return firstSection.content;
|
|
31577
|
+
}
|
|
31578
|
+
|
|
31579
|
+
// If it's a paragraph, we need to iterate through the sections
|
|
31580
|
+
// to look for individual math blocks.
|
|
31581
|
+
const condensedNodes = condenseTextNodes(firstSection.content);
|
|
31582
|
+
let result = "";
|
|
31583
|
+
for (const piece of condensedNodes) {
|
|
31584
|
+
piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
|
|
31585
|
+
}
|
|
31586
|
+
return result;
|
|
31587
|
+
}
|
|
31588
|
+
// Helper function for replaceOutsideTeX()
|
|
31589
|
+
// Condense adjacent text nodes into a single text node
|
|
31590
|
+
function condenseTextNodes(nodes) {
|
|
31591
|
+
const result = [];
|
|
31592
|
+
if (!nodes) {
|
|
31593
|
+
return result;
|
|
31594
|
+
}
|
|
31595
|
+
let currentText = "";
|
|
31596
|
+
for (const node of nodes) {
|
|
31597
|
+
if (node.type === "math") {
|
|
31598
|
+
if (currentText) {
|
|
31599
|
+
result.push({
|
|
31600
|
+
type: "text",
|
|
31601
|
+
content: currentText
|
|
31602
|
+
});
|
|
31603
|
+
currentText = "";
|
|
31604
|
+
}
|
|
31605
|
+
result.push(node);
|
|
31606
|
+
} else {
|
|
31607
|
+
currentText += node.content;
|
|
31608
|
+
}
|
|
31609
|
+
}
|
|
31610
|
+
if (currentText) {
|
|
31611
|
+
result.push({
|
|
31612
|
+
type: "text",
|
|
31613
|
+
content: currentText
|
|
31614
|
+
});
|
|
31615
|
+
}
|
|
31616
|
+
return result;
|
|
31617
|
+
}
|
|
31618
|
+
|
|
31619
|
+
// Helper function for replaceOutsideTeX()
|
|
31620
|
+
function escapeSpecialChars(str) {
|
|
31621
|
+
// Escape $, \, {, and } characters
|
|
31622
|
+
return str.replace(/([$\\{}])/g, "\\$1");
|
|
31623
|
+
}
|
|
31624
|
+
|
|
31374
31625
|
function AxisLabels() {
|
|
31375
31626
|
const {
|
|
31376
31627
|
range,
|
|
@@ -31402,7 +31653,7 @@ function AxisLabels() {
|
|
|
31402
31653
|
fontSize: "14px",
|
|
31403
31654
|
transform: "translate(7px, -50%)"
|
|
31404
31655
|
}
|
|
31405
|
-
}, /*#__PURE__*/React__default["default"].createElement(TeX, null, xAxisLabelText)), /*#__PURE__*/React__default["default"].createElement("span", {
|
|
31656
|
+
}, /*#__PURE__*/React__default["default"].createElement(TeX, null, replaceOutsideTeX(xAxisLabelText))), /*#__PURE__*/React__default["default"].createElement("span", {
|
|
31406
31657
|
style: {
|
|
31407
31658
|
position: "absolute",
|
|
31408
31659
|
left: x2,
|
|
@@ -31410,7 +31661,7 @@ function AxisLabels() {
|
|
|
31410
31661
|
fontSize: "14px",
|
|
31411
31662
|
transform: "translate(-50%, 0px)"
|
|
31412
31663
|
}
|
|
31413
|
-
}, /*#__PURE__*/React__default["default"].createElement(TeX, null, yAxisLabelText)));
|
|
31664
|
+
}, /*#__PURE__*/React__default["default"].createElement(TeX, null, replaceOutsideTeX(yAxisLabelText))));
|
|
31414
31665
|
}
|
|
31415
31666
|
|
|
31416
31667
|
// The size of the ticks and labels in pixels
|
|
@@ -32348,108 +32599,6 @@ const lockedFigureFillStyles = {
|
|
|
32348
32599
|
|
|
32349
32600
|
const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
|
|
32350
32601
|
|
|
32351
|
-
/**
|
|
32352
|
-
* 44 is touch best practice and AAA compliant for WCAG
|
|
32353
|
-
* https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
|
|
32354
|
-
*/
|
|
32355
|
-
const TARGET_SIZE = 44;
|
|
32356
|
-
const REMOVE_BUTTON_ID = "perseus_mafs_remove_button";
|
|
32357
|
-
|
|
32358
|
-
// same as pointsFromNormalized in interactive-graph.tsx
|
|
32359
|
-
const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
|
|
32360
|
-
// maps over [x, y]
|
|
32361
|
-
coords.map((coord, i) => {
|
|
32362
|
-
const axisRange = range[i];
|
|
32363
|
-
if (noSnap) {
|
|
32364
|
-
return axisRange[MIN] + size(axisRange) * coord;
|
|
32365
|
-
}
|
|
32366
|
-
const axisStep = step[i];
|
|
32367
|
-
const nSteps = Math.floor(size(axisRange) / axisStep);
|
|
32368
|
-
const tick = Math.round(coord * nSteps);
|
|
32369
|
-
return axisRange[MIN] + axisStep * tick;
|
|
32370
|
-
}));
|
|
32371
|
-
|
|
32372
|
-
// same as normalizeCoords in interactive-graph.tsx
|
|
32373
|
-
const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
|
|
32374
|
-
return (coord + ranges[i][1]) / size(ranges[i]);
|
|
32375
|
-
}));
|
|
32376
|
-
|
|
32377
|
-
// Returns the closest point to the given `point` that is within the graph
|
|
32378
|
-
// bounds given in `state`.
|
|
32379
|
-
function bound$1(_ref) {
|
|
32380
|
-
let {
|
|
32381
|
-
snapStep,
|
|
32382
|
-
range,
|
|
32383
|
-
point
|
|
32384
|
-
} = _ref;
|
|
32385
|
-
const boundingBox = inset(snapStep, range);
|
|
32386
|
-
return clampToBox(boundingBox, point);
|
|
32387
|
-
}
|
|
32388
|
-
function isUnlimitedGraphState(state) {
|
|
32389
|
-
return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
|
|
32390
|
-
}
|
|
32391
|
-
|
|
32392
|
-
/**
|
|
32393
|
-
* Replace all text outside of the $ TeX blocks with `\\text{...}`
|
|
32394
|
-
* This way, the entire resulting string can be rendered within <TeX>
|
|
32395
|
-
* and the text outside of the $ blocks will be non-TeX text.
|
|
32396
|
-
*/
|
|
32397
|
-
function replaceOutsideTeX(mathString) {
|
|
32398
|
-
// All the information we need is in the first section,
|
|
32399
|
-
// whether it's typed as "blockmath" or "paragraph"
|
|
32400
|
-
const firstSection = SimpleMarkdown__namespace.parse(mathString)[0];
|
|
32401
|
-
|
|
32402
|
-
// If it's blockMath, the outer level has the full math content.
|
|
32403
|
-
if (firstSection.type === "blockMath") {
|
|
32404
|
-
return firstSection.content;
|
|
32405
|
-
}
|
|
32406
|
-
|
|
32407
|
-
// If it's a paragraph, we need to iterate through the sections
|
|
32408
|
-
// to look for individual math blocks.
|
|
32409
|
-
const condensedNodes = condenseTextNodes(firstSection.content);
|
|
32410
|
-
let result = "";
|
|
32411
|
-
for (const piece of condensedNodes) {
|
|
32412
|
-
piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
|
|
32413
|
-
}
|
|
32414
|
-
return result;
|
|
32415
|
-
}
|
|
32416
|
-
// Helper function for replaceOutsideTeX()
|
|
32417
|
-
// Condense adjacent text nodes into a single text node
|
|
32418
|
-
function condenseTextNodes(nodes) {
|
|
32419
|
-
const result = [];
|
|
32420
|
-
if (!nodes) {
|
|
32421
|
-
return result;
|
|
32422
|
-
}
|
|
32423
|
-
let currentText = "";
|
|
32424
|
-
for (const node of nodes) {
|
|
32425
|
-
if (node.type === "math") {
|
|
32426
|
-
if (currentText) {
|
|
32427
|
-
result.push({
|
|
32428
|
-
type: "text",
|
|
32429
|
-
content: currentText
|
|
32430
|
-
});
|
|
32431
|
-
currentText = "";
|
|
32432
|
-
}
|
|
32433
|
-
result.push(node);
|
|
32434
|
-
} else {
|
|
32435
|
-
currentText += node.content;
|
|
32436
|
-
}
|
|
32437
|
-
}
|
|
32438
|
-
if (currentText) {
|
|
32439
|
-
result.push({
|
|
32440
|
-
type: "text",
|
|
32441
|
-
content: currentText
|
|
32442
|
-
});
|
|
32443
|
-
}
|
|
32444
|
-
return result;
|
|
32445
|
-
}
|
|
32446
|
-
|
|
32447
|
-
// Helper function for replaceOutsideTeX()
|
|
32448
|
-
function escapeSpecialChars(str) {
|
|
32449
|
-
// Escape $, \, {, and } characters
|
|
32450
|
-
return str.replace(/([$\\{}])/g, "\\$1");
|
|
32451
|
-
}
|
|
32452
|
-
|
|
32453
32602
|
function LockedLabel(props) {
|
|
32454
32603
|
const {
|
|
32455
32604
|
coord,
|
|
@@ -33678,6 +33827,7 @@ function useControlPoint(params) {
|
|
|
33678
33827
|
} = useGraphConfig();
|
|
33679
33828
|
const {
|
|
33680
33829
|
point,
|
|
33830
|
+
sequenceNumber,
|
|
33681
33831
|
color,
|
|
33682
33832
|
cursor,
|
|
33683
33833
|
constrain = p => snap(snapStep, p),
|
|
@@ -33718,6 +33868,7 @@ function useControlPoint(params) {
|
|
|
33718
33868
|
ref: focusableHandleRef,
|
|
33719
33869
|
role: "button",
|
|
33720
33870
|
"aria-label": strings.srPointAtCoordinates({
|
|
33871
|
+
num: sequenceNumber,
|
|
33721
33872
|
x: srFormatNumber(point[X], locale),
|
|
33722
33873
|
y: srFormatNumber(point[Y], locale)
|
|
33723
33874
|
})
|
|
@@ -33795,6 +33946,7 @@ const MovableLine = props => {
|
|
|
33795
33946
|
focusableHandle: focusableHandle1
|
|
33796
33947
|
} = useControlPoint({
|
|
33797
33948
|
point: start,
|
|
33949
|
+
sequenceNumber: 1,
|
|
33798
33950
|
color,
|
|
33799
33951
|
onMove: p => onMovePoint(0, p)
|
|
33800
33952
|
});
|
|
@@ -33803,6 +33955,7 @@ const MovableLine = props => {
|
|
|
33803
33955
|
focusableHandle: focusableHandle2
|
|
33804
33956
|
} = useControlPoint({
|
|
33805
33957
|
point: end,
|
|
33958
|
+
sequenceNumber: 2,
|
|
33806
33959
|
color,
|
|
33807
33960
|
onMove: p => onMovePoint(1, p)
|
|
33808
33961
|
});
|
|
@@ -33984,14 +34137,17 @@ function AngleGraph(props) {
|
|
|
33984
34137
|
// Render the lines, angle, and then movable points
|
|
33985
34138
|
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, svgLines, /*#__PURE__*/React__namespace.createElement(Angle, angleParams), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
33986
34139
|
point: coords[1],
|
|
34140
|
+
sequenceNumber: 1,
|
|
33987
34141
|
constrain: p => p,
|
|
33988
34142
|
onMove: destination => dispatch(actions.angle.movePoint(1, destination))
|
|
33989
34143
|
}), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
33990
34144
|
point: coords[0],
|
|
34145
|
+
sequenceNumber: 2,
|
|
33991
34146
|
constrain: getAngleSideConstraint(coords[0], coords[1], snapDegrees || 1),
|
|
33992
34147
|
onMove: destination => dispatch(actions.angle.movePoint(0, destination))
|
|
33993
34148
|
}), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
33994
34149
|
point: coords[2],
|
|
34150
|
+
sequenceNumber: 3,
|
|
33995
34151
|
constrain: getAngleSideConstraint(coords[2], coords[1], snapDegrees || 1),
|
|
33996
34152
|
onMove: destination => dispatch(actions.angle.movePoint(2, destination))
|
|
33997
34153
|
}));
|
|
@@ -34156,6 +34312,7 @@ function CircleGraph(props) {
|
|
|
34156
34312
|
onMove: c => dispatch(actions.circle.moveCenter(c))
|
|
34157
34313
|
}), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
34158
34314
|
point: radiusPoint,
|
|
34315
|
+
sequenceNumber: 1,
|
|
34159
34316
|
cursor: "ew-resize",
|
|
34160
34317
|
onMove: newRadiusPoint => {
|
|
34161
34318
|
dispatch(actions.circle.moveRadiusPoint(newRadiusPoint));
|
|
@@ -34331,6 +34488,7 @@ function LimitedPointGraph(props) {
|
|
|
34331
34488
|
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
34332
34489
|
key: i,
|
|
34333
34490
|
point: point,
|
|
34491
|
+
sequenceNumber: i + 1,
|
|
34334
34492
|
onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination))
|
|
34335
34493
|
})));
|
|
34336
34494
|
}
|
|
@@ -34372,6 +34530,7 @@ function UnlimitedPointGraph(props) {
|
|
|
34372
34530
|
}), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
34373
34531
|
key: i,
|
|
34374
34532
|
point: point,
|
|
34533
|
+
sequenceNumber: i + 1,
|
|
34375
34534
|
onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
|
|
34376
34535
|
ref: ref => {
|
|
34377
34536
|
itemsRef.current[i] = ref;
|
|
@@ -34403,9 +34562,10 @@ function describePointGraph(state, i18n) {
|
|
|
34403
34562
|
if (state.coords.length === 0) {
|
|
34404
34563
|
return strings.srNoInteractiveElements;
|
|
34405
34564
|
}
|
|
34406
|
-
const pointDescriptions = state.coords.map(_ref2 => {
|
|
34565
|
+
const pointDescriptions = state.coords.map((_ref2, index) => {
|
|
34407
34566
|
let [x, y] = _ref2;
|
|
34408
34567
|
return strings.srPointAtCoordinates({
|
|
34568
|
+
num: index + 1,
|
|
34409
34569
|
x: srFormatNumber(x, locale),
|
|
34410
34570
|
y: srFormatNumber(y, locale)
|
|
34411
34571
|
});
|
|
@@ -34525,6 +34685,7 @@ const LimitedPolygonGraph = props => {
|
|
|
34525
34685
|
key: "point-" + i,
|
|
34526
34686
|
constrain: constrain,
|
|
34527
34687
|
point: point,
|
|
34688
|
+
sequenceNumber: i + 1,
|
|
34528
34689
|
onMove: destination => {
|
|
34529
34690
|
const now = Date.now();
|
|
34530
34691
|
const targetFPS = 40;
|
|
@@ -34671,6 +34832,7 @@ const UnlimitedPolygonGraph = props => {
|
|
|
34671
34832
|
}), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
34672
34833
|
key: i,
|
|
34673
34834
|
point: point,
|
|
34835
|
+
sequenceNumber: i + 1,
|
|
34674
34836
|
onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
|
|
34675
34837
|
ref: ref => {
|
|
34676
34838
|
pointRef.current[i] = ref;
|
|
@@ -34743,6 +34905,7 @@ function QuadraticGraph(props) {
|
|
|
34743
34905
|
}), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
34744
34906
|
key: "point-" + i,
|
|
34745
34907
|
point: coord,
|
|
34908
|
+
sequenceNumber: i + 1,
|
|
34746
34909
|
onMove: destination => dispatch(actions.quadratic.movePoint(i, destination))
|
|
34747
34910
|
})));
|
|
34748
34911
|
}
|
|
@@ -34871,6 +35034,7 @@ function SinusoidGraph(props) {
|
|
|
34871
35034
|
}), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
34872
35035
|
key: "point-" + i,
|
|
34873
35036
|
point: coord,
|
|
35037
|
+
sequenceNumber: i + 1,
|
|
34874
35038
|
onMove: destination => dispatch(actions.sinusoid.movePoint(i, destination))
|
|
34875
35039
|
})));
|
|
34876
35040
|
}
|
|
@@ -45039,7 +45203,7 @@ const init = function () {
|
|
|
45039
45203
|
|
|
45040
45204
|
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
45041
45205
|
const libName = "@khanacademy/perseus";
|
|
45042
|
-
const libVersion = "
|
|
45206
|
+
const libVersion = "42.0.0";
|
|
45043
45207
|
perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
45044
45208
|
|
|
45045
45209
|
const apiVersion = {
|
|
@@ -45644,6 +45808,13 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
45644
45808
|
this.props.onRendered(true);
|
|
45645
45809
|
}
|
|
45646
45810
|
}
|
|
45811
|
+
if (this.props.score && this.props.score !== prevProps.score) {
|
|
45812
|
+
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
45813
|
+
this.setState({
|
|
45814
|
+
questionCompleted: this.props.score.correct,
|
|
45815
|
+
questionHighlightedWidgets: emptyQuestionAreaWidgets
|
|
45816
|
+
});
|
|
45817
|
+
}
|
|
45647
45818
|
}
|
|
45648
45819
|
componentWillUnmount() {
|
|
45649
45820
|
if (this.blurTimeoutID != null) {
|
|
@@ -45779,18 +45950,37 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
45779
45950
|
return this.questionRenderer.getPromptJSON();
|
|
45780
45951
|
}
|
|
45781
45952
|
|
|
45953
|
+
/**
|
|
45954
|
+
* Returns an array of the widget `.getUserInput()` results
|
|
45955
|
+
*
|
|
45956
|
+
* TODO: can we remove this? Seems to be just for backwards
|
|
45957
|
+
* compatibility with old Perseus Chrome logging
|
|
45958
|
+
* @deprecated use getUserInput
|
|
45959
|
+
*/
|
|
45960
|
+
getUserInputLegacy() {
|
|
45961
|
+
return this.questionRenderer.getUserInput();
|
|
45962
|
+
}
|
|
45963
|
+
|
|
45964
|
+
/**
|
|
45965
|
+
* Returns an object of the widget `.getUserInput()` results
|
|
45966
|
+
*/
|
|
45967
|
+
getUserInput() {
|
|
45968
|
+
return this.questionRenderer.getUserInputMap();
|
|
45969
|
+
}
|
|
45970
|
+
|
|
45782
45971
|
/**
|
|
45783
45972
|
* Grades the item.
|
|
45973
|
+
*
|
|
45974
|
+
* @deprecated use scorePerseusItem
|
|
45784
45975
|
*/
|
|
45785
45976
|
scoreInput() {
|
|
45786
|
-
const
|
|
45787
|
-
const
|
|
45788
|
-
const score = guessAndScore[1];
|
|
45977
|
+
const guess = this.getUserInput();
|
|
45978
|
+
const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
|
|
45789
45979
|
|
|
45790
45980
|
// Continue to include an empty guess for the now defunct answer area.
|
|
45791
45981
|
// TODO(alex): Check whether we rely on the format here for
|
|
45792
45982
|
// analyzing ProblemLogs. If not, remove this layer.
|
|
45793
|
-
const maxCompatGuess = [
|
|
45983
|
+
const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
|
|
45794
45984
|
const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
|
|
45795
45985
|
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
45796
45986
|
this.setState({
|
|
@@ -47652,138 +47842,6 @@ const registerAllWidgetsForTesting = () => {
|
|
|
47652
47842
|
replaceDeprecatedWidgets();
|
|
47653
47843
|
};
|
|
47654
47844
|
|
|
47655
|
-
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
47656
|
-
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
47657
|
-
|
|
47658
|
-
/**
|
|
47659
|
-
* Regex for widget placeholders in a string.
|
|
47660
|
-
*
|
|
47661
|
-
* First capture group is the widget ID (ex. 'radio 1')
|
|
47662
|
-
* Second capture group is the widget type (ex. "radio)
|
|
47663
|
-
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
47664
|
-
*/
|
|
47665
|
-
function getWidgetRegex() {
|
|
47666
|
-
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
47667
|
-
}
|
|
47668
|
-
|
|
47669
|
-
/**
|
|
47670
|
-
* Add a widget placeholder using the widget ID.
|
|
47671
|
-
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
47672
|
-
*
|
|
47673
|
-
* @param {string} id
|
|
47674
|
-
* @returns {string}
|
|
47675
|
-
*/
|
|
47676
|
-
function addWidget(id) {
|
|
47677
|
-
return `[[☃ ${id}]]`;
|
|
47678
|
-
}
|
|
47679
|
-
|
|
47680
|
-
/**
|
|
47681
|
-
* Get a widget type by a widget's ID
|
|
47682
|
-
* (please don't derive type from ID)
|
|
47683
|
-
*
|
|
47684
|
-
* @param {string} widgetId the ID of the widget
|
|
47685
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47686
|
-
* @returns {string} the widget type (ie "radio")
|
|
47687
|
-
*/
|
|
47688
|
-
function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
|
|
47689
|
-
const widget = widgetMap[WidgetId];
|
|
47690
|
-
return widget?.type ?? null;
|
|
47691
|
-
}
|
|
47692
|
-
|
|
47693
|
-
/**
|
|
47694
|
-
* Does the content have a specific type of widget?
|
|
47695
|
-
*
|
|
47696
|
-
* @param {string} type the type of the widget in question (ie "radio")
|
|
47697
|
-
* @param {string} content the string to search through
|
|
47698
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47699
|
-
* @returns {boolean} if the content includes the widget type
|
|
47700
|
-
*/
|
|
47701
|
-
function contentHasWidgetType(type, content, widgetMap) {
|
|
47702
|
-
return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
|
|
47703
|
-
}
|
|
47704
|
-
|
|
47705
|
-
/**
|
|
47706
|
-
* Extract all widget IDs, which includes the widget type and instance number.
|
|
47707
|
-
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
47708
|
-
*
|
|
47709
|
-
* Content should contain Perseus widget placeholders,
|
|
47710
|
-
* which look like: '[[☃ radio 1]]'.
|
|
47711
|
-
*
|
|
47712
|
-
* @param {string} content
|
|
47713
|
-
* @returns {ReadonlyArray<string>} widgetIds
|
|
47714
|
-
*/
|
|
47715
|
-
function getWidgetIdsFromContent(content) {
|
|
47716
|
-
const widgets = [];
|
|
47717
|
-
const localWidgetRegex = getWidgetRegex();
|
|
47718
|
-
let match = localWidgetRegex.exec(content);
|
|
47719
|
-
while (match !== null) {
|
|
47720
|
-
widgets.push(match[1]);
|
|
47721
|
-
match = localWidgetRegex.exec(content);
|
|
47722
|
-
}
|
|
47723
|
-
return widgets;
|
|
47724
|
-
}
|
|
47725
|
-
|
|
47726
|
-
/**
|
|
47727
|
-
* Get a list of widget IDs from content,
|
|
47728
|
-
* but only for specific widget types
|
|
47729
|
-
*
|
|
47730
|
-
* @param {string} type the type of widget (ie "radio")
|
|
47731
|
-
* @param {string} content the string to parse
|
|
47732
|
-
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
47733
|
-
* @returns {ReadonlyArray<string>} the widget type (ie "radio")
|
|
47734
|
-
*/
|
|
47735
|
-
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
47736
|
-
const rv = [];
|
|
47737
|
-
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
47738
|
-
widgetIdsInContent.forEach(widgetId => {
|
|
47739
|
-
const widget = widgetMap[widgetId];
|
|
47740
|
-
if (widget?.type === type) {
|
|
47741
|
-
rv.push(widgetId);
|
|
47742
|
-
}
|
|
47743
|
-
});
|
|
47744
|
-
return rv;
|
|
47745
|
-
}
|
|
47746
|
-
|
|
47747
|
-
/**
|
|
47748
|
-
* Pull the widget map out of ItemData
|
|
47749
|
-
*
|
|
47750
|
-
* @param {PerseusItem} PerseusItem containing a widgetMap
|
|
47751
|
-
* @returns {WidgetMap} the widget map in the PerseusItem
|
|
47752
|
-
*/
|
|
47753
|
-
function getWidgetsMapFromItemData(itemData) {
|
|
47754
|
-
return itemData.question.widgets;
|
|
47755
|
-
}
|
|
47756
|
-
|
|
47757
|
-
/**
|
|
47758
|
-
* Get the widget information from a WidgetMap
|
|
47759
|
-
*
|
|
47760
|
-
* @param {string} widgetId the ID of the widget
|
|
47761
|
-
* @returns {PerseusWidget | null} the widget data if it exists, otherwise null
|
|
47762
|
-
*/
|
|
47763
|
-
function getWidgetFromWidgetMap(widgetId, widgetMap) {
|
|
47764
|
-
return widgetMap[widgetId] ?? null;
|
|
47765
|
-
}
|
|
47766
|
-
|
|
47767
|
-
/**
|
|
47768
|
-
* Get select widgets from a widget map.
|
|
47769
|
-
* Useful for multi-items that needs to split
|
|
47770
|
-
* one PerseusItem into several
|
|
47771
|
-
*
|
|
47772
|
-
* @param {ReadonlyArray<string>} widgetIds to extract
|
|
47773
|
-
* @param {PerseusWidgetsMap} widgetMap to extract from
|
|
47774
|
-
* @return {PerseusWidgetsMap} a new widget map with requested widgets
|
|
47775
|
-
*/
|
|
47776
|
-
function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
|
|
47777
|
-
const widgets = {};
|
|
47778
|
-
widgetIds.forEach(widgetId => {
|
|
47779
|
-
const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
|
|
47780
|
-
if (widget) {
|
|
47781
|
-
widgets[widgetId] = widget;
|
|
47782
|
-
}
|
|
47783
|
-
});
|
|
47784
|
-
return widgets;
|
|
47785
|
-
}
|
|
47786
|
-
|
|
47787
47845
|
/**
|
|
47788
47846
|
* This function extracts the answers from the widgets.
|
|
47789
47847
|
*
|
|
@@ -49836,6 +49894,7 @@ exports.parsePerseusItem = parsePerseusItem;
|
|
|
49836
49894
|
exports.plotterPlotTypes = plotterPlotTypes;
|
|
49837
49895
|
exports.preprocessTex = preprocessTex;
|
|
49838
49896
|
exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
|
|
49897
|
+
exports.scorePerseusItem = scorePerseusItem;
|
|
49839
49898
|
exports.shapes = shapes;
|
|
49840
49899
|
exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
|
|
49841
49900
|
exports.traverse = traverse;
|