@khanacademy/perseus 41.5.0 → 42.0.1
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 +513 -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 +4 -1
- package/dist/index.js +519 -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/deprecated-widgets/input-number.d.ts +9 -0
- package/dist/util/deprecated-widgets/modernize-widgets-utils.d.ts +4 -0
- 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 +3 -3
- 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,326 @@ class QuestionParagraph extends React__namespace.Component {
|
|
|
18874
18875
|
}
|
|
18875
18876
|
}
|
|
18876
18877
|
|
|
18878
|
+
// Methods to convert input-number widgets to numeric-input widgets
|
|
18879
|
+
|
|
18880
|
+
const inputNumberToNumericInput = json => {
|
|
18881
|
+
// First we need to create a map of the old input-number keys to the new numeric-input keys
|
|
18882
|
+
// so that we can ensure we update the content and widgets accordingly
|
|
18883
|
+
const renameMap = getInputNumberRenameMap(json);
|
|
18884
|
+
|
|
18885
|
+
// Then we can use this to update the JSON
|
|
18886
|
+
return convertInputNumberJson(json, renameMap);
|
|
18887
|
+
};
|
|
18888
|
+
|
|
18889
|
+
// We need to be able to run this code recursively in order to convert nested input-number widgets
|
|
18890
|
+
const convertInputNumberJson = (json, renameMap) => {
|
|
18891
|
+
const updatedContent = convertDeprecatedWidgetsInContent(json, renameMap);
|
|
18892
|
+
const updatedWidgets = convertInputNumberWidgetOptions(json, renameMap);
|
|
18893
|
+
const modernizedJson = {
|
|
18894
|
+
...json,
|
|
18895
|
+
content: updatedContent,
|
|
18896
|
+
widgets: updatedWidgets
|
|
18897
|
+
};
|
|
18898
|
+
return modernizedJson;
|
|
18899
|
+
};
|
|
18900
|
+
|
|
18901
|
+
// Convert the input-number json in the widgets of a PerseusRenderer object
|
|
18902
|
+
const convertInputNumberWidgetOptions = (json, renameMap) => {
|
|
18903
|
+
const widgets = {
|
|
18904
|
+
...json.widgets
|
|
18905
|
+
};
|
|
18906
|
+
// The question.widgets is a dictionary map of widgets, so we need to loop through in order to convert input-number to numeric-input
|
|
18907
|
+
// which can exist as both the key or as a value on widget.type.
|
|
18908
|
+
for (const key of Object.keys(widgets)) {
|
|
18909
|
+
// Loop through the keys of the widgets dictionary
|
|
18910
|
+
if (widgets[key].options.widgets) {
|
|
18911
|
+
widgets[key].options = {
|
|
18912
|
+
...inputNumberToNumericInput(widgets[key].options)
|
|
18913
|
+
};
|
|
18914
|
+
}
|
|
18915
|
+
// Check if the widget is an input-number
|
|
18916
|
+
if (widgets[key].type === "input-number") {
|
|
18917
|
+
// If the answerType is not number or percent, we need to provide
|
|
18918
|
+
// the answer form for the numeric-input widget
|
|
18919
|
+
const provideAnswerForm = widgets[key].options.answerType !== "number" && widgets[key].options.answerType !== "percent";
|
|
18920
|
+
// We need to determine the mathFormat for the numeric-input widget
|
|
18921
|
+
const mathFormat = widgets[key].options.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
|
|
18922
|
+
: widgets[key].options.answerType; // Otherwise, we can use the answerType directly
|
|
18923
|
+
|
|
18924
|
+
// We need to update the answers prop to match the numeric-input widget format
|
|
18925
|
+
const answers = [{
|
|
18926
|
+
value: widgets[key].options.value,
|
|
18927
|
+
simplify: widgets[key].options.simplify,
|
|
18928
|
+
// Input Number doesn't have a strict prop, so we default to false
|
|
18929
|
+
strict: false,
|
|
18930
|
+
// We only want to set maxError if the inexact prop is true
|
|
18931
|
+
maxError: widgets[key].options.inexact ? widgets[key].options.maxError : 0,
|
|
18932
|
+
status: "correct",
|
|
18933
|
+
// Input-number only allows correct answers
|
|
18934
|
+
message: ""
|
|
18935
|
+
}];
|
|
18936
|
+
|
|
18937
|
+
// Add the required answerForms if provided/applicable
|
|
18938
|
+
if (provideAnswerForm) {
|
|
18939
|
+
answers[0].answerForms = [mathFormat];
|
|
18940
|
+
}
|
|
18941
|
+
|
|
18942
|
+
// Update the options prop to match the numeric-input widget format
|
|
18943
|
+
const upgradedWidgetOptions = {
|
|
18944
|
+
answers,
|
|
18945
|
+
size: widgets[key].options.size,
|
|
18946
|
+
coefficient: false,
|
|
18947
|
+
// input-number doesn't have a coefficient prop
|
|
18948
|
+
labelText: "",
|
|
18949
|
+
// input-number doesn't have a labelText prop
|
|
18950
|
+
static: false,
|
|
18951
|
+
// static is always false for numeric-input
|
|
18952
|
+
rightAlign: widgets[key].options.rightAlign || false
|
|
18953
|
+
};
|
|
18954
|
+
const upgradedWidget = {
|
|
18955
|
+
options: upgradedWidgetOptions,
|
|
18956
|
+
type: "numeric-input"
|
|
18957
|
+
};
|
|
18958
|
+
const newWidgetName = renameMap[key];
|
|
18959
|
+
// Create the new key entry
|
|
18960
|
+
widgets[newWidgetName] = upgradedWidget;
|
|
18961
|
+
|
|
18962
|
+
// We need to delete the old widget key
|
|
18963
|
+
delete widgets[key];
|
|
18964
|
+
}
|
|
18965
|
+
}
|
|
18966
|
+
return widgets;
|
|
18967
|
+
};
|
|
18968
|
+
|
|
18969
|
+
// Convert the deprecated widget refs in the content string
|
|
18970
|
+
// of a PerseusRenderer object to their renamed equivalents
|
|
18971
|
+
const convertDeprecatedWidgetsInContent = (json, renameMap) => {
|
|
18972
|
+
return Object.keys(renameMap).reduce((newContent, oldKey) => {
|
|
18973
|
+
const newKey = renameMap[oldKey];
|
|
18974
|
+
return newKey ? newContent.replace(oldKey, newKey) : newContent;
|
|
18975
|
+
}, json.content);
|
|
18976
|
+
};
|
|
18977
|
+
|
|
18978
|
+
// Create a map of the old input-number keys to the new numeric-input keys
|
|
18979
|
+
const getInputNumberRenameMap = json => {
|
|
18980
|
+
const numericRegex = /(?<=\[\[\u2603 )(numeric-input \d+)(?=\]\])/g;
|
|
18981
|
+
const inputNumberRegex = /(?<=\[\[\u2603 )(input-number \d+)(?=\]\])/g;
|
|
18982
|
+
|
|
18983
|
+
// Get all the content strings within the json, which might be nested within widgets
|
|
18984
|
+
const allContentStrings = json.content;
|
|
18985
|
+
|
|
18986
|
+
// Loop through the content strings to get all the input-number widgets
|
|
18987
|
+
const renameMap = {};
|
|
18988
|
+
const inputNumberMatches = [...(allContentStrings.match(inputNumberRegex) || [])];
|
|
18989
|
+
|
|
18990
|
+
// We want to count any pre-existing numeric-input widgets
|
|
18991
|
+
// so that we can start the new ids at the next number
|
|
18992
|
+
const numericMatches = [...(allContentStrings.match(numericRegex) || [])];
|
|
18993
|
+
let currentNumericCount = numericMatches.reduce((count, match) => {
|
|
18994
|
+
const id = parseInt(match.split(" ")[1], 10);
|
|
18995
|
+
return id >= count ? id + 1 : count; // We want to start at the next highest number
|
|
18996
|
+
}, 1);
|
|
18997
|
+
|
|
18998
|
+
// Now that we have all the necessary information, we can create the renameMap
|
|
18999
|
+
for (const match of inputNumberMatches) {
|
|
19000
|
+
const oldKey = match;
|
|
19001
|
+
const newKey = `numeric-input ${currentNumericCount}`;
|
|
19002
|
+
renameMap[oldKey] = newKey;
|
|
19003
|
+
currentNumericCount++;
|
|
19004
|
+
}
|
|
19005
|
+
return renameMap;
|
|
19006
|
+
};
|
|
19007
|
+
|
|
19008
|
+
// Convert the user input data keys from input-number to numeric-input
|
|
19009
|
+
const convertUserInputNumberData = userInputMap => {
|
|
19010
|
+
const updatedUserInputMap = {
|
|
19011
|
+
...userInputMap
|
|
19012
|
+
};
|
|
19013
|
+
for (const key of Object.keys(userInputMap)) {
|
|
19014
|
+
if (key.includes("input-number")) {
|
|
19015
|
+
const updatedKey = key.replace("input-number", "numeric-input");
|
|
19016
|
+
updatedUserInputMap[updatedKey] = userInputMap[key];
|
|
19017
|
+
delete updatedUserInputMap[key];
|
|
19018
|
+
}
|
|
19019
|
+
}
|
|
19020
|
+
return updatedUserInputMap;
|
|
19021
|
+
};
|
|
19022
|
+
|
|
19023
|
+
const widgetRegExes = [/input-number \d+/]; // We can add more regexes here in the future
|
|
19024
|
+
|
|
19025
|
+
// This utils in this file are used to modernize our Perseus JSON structure,
|
|
19026
|
+
// so that we can convert deprecated widgets to their modern equivalents when
|
|
19027
|
+
// content creators use the Editor Page to update content containing these widgets.
|
|
19028
|
+
// Currently, we're only converting input-number to numeric-input,
|
|
19029
|
+
// but we can add more conversions here in the future.
|
|
19030
|
+
// Modernize the json content of a PerseusRenderer object
|
|
19031
|
+
// by converting deprecated widgets to their modern equivalents
|
|
19032
|
+
const convertDeprecatedWidgets = json => {
|
|
19033
|
+
// Currently we're only converting input-number to numeric-input,
|
|
19034
|
+
// But we can add more conversions here in the future
|
|
19035
|
+
return inputNumberToNumericInput(json);
|
|
19036
|
+
};
|
|
19037
|
+
const conversionRequired = json => {
|
|
19038
|
+
// If there's no content, then there's no conversion required
|
|
19039
|
+
if (!json.content) {
|
|
19040
|
+
return false;
|
|
19041
|
+
}
|
|
19042
|
+
// Check the content string for any top-level input-number widgets
|
|
19043
|
+
if (widgetRegExes.some(regex => regex.test(json.content))) {
|
|
19044
|
+
return true;
|
|
19045
|
+
}
|
|
19046
|
+
|
|
19047
|
+
// If there's no deprecated widget in the top-level, then check for any nested widgets
|
|
19048
|
+
for (const key of Object.keys(json.widgets)) {
|
|
19049
|
+
if (json.widgets[key].options.widgets) {
|
|
19050
|
+
const nestedJson = json.widgets[key].options;
|
|
19051
|
+
if (conversionRequired(nestedJson)) {
|
|
19052
|
+
return true;
|
|
19053
|
+
}
|
|
19054
|
+
}
|
|
19055
|
+
}
|
|
19056
|
+
return false;
|
|
19057
|
+
};
|
|
19058
|
+
|
|
19059
|
+
// Convert the user input data keys for deprecated widgets to their modern equivalents
|
|
19060
|
+
const convertUserInputData = userInputMap => {
|
|
19061
|
+
// Currently we're only converting input-number to numeric-input,
|
|
19062
|
+
// But we can add more conversions here in the future
|
|
19063
|
+
return convertUserInputNumberData(userInputMap);
|
|
19064
|
+
};
|
|
19065
|
+
|
|
19066
|
+
// These are all the current widgets that require user-input and are supported for automatic scoring
|
|
19067
|
+
const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
|
|
19068
|
+
|
|
19069
|
+
/**
|
|
19070
|
+
* Regex for widget placeholders in a string.
|
|
19071
|
+
*
|
|
19072
|
+
* First capture group is the widget ID (ex. 'radio 1')
|
|
19073
|
+
* Second capture group is the widget type (ex. "radio)
|
|
19074
|
+
* exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
|
|
19075
|
+
*/
|
|
19076
|
+
function getWidgetRegex() {
|
|
19077
|
+
return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
|
|
19078
|
+
}
|
|
19079
|
+
|
|
19080
|
+
/**
|
|
19081
|
+
* Add a widget placeholder using the widget ID.
|
|
19082
|
+
* ex. addWidget("radio 1") => "[[☃ radio 1]]"
|
|
19083
|
+
*
|
|
19084
|
+
* @param {string} id
|
|
19085
|
+
* @returns {string}
|
|
19086
|
+
*/
|
|
19087
|
+
function addWidget(id) {
|
|
19088
|
+
return `[[☃ ${id}]]`;
|
|
19089
|
+
}
|
|
19090
|
+
|
|
19091
|
+
/**
|
|
19092
|
+
* Get a widget type by a widget's ID
|
|
19093
|
+
* (please don't derive type from ID)
|
|
19094
|
+
*
|
|
19095
|
+
* @param {string} widgetId the ID of the widget
|
|
19096
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
19097
|
+
* @returns {string} the widget type (ie "radio")
|
|
19098
|
+
*/
|
|
19099
|
+
function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
|
|
19100
|
+
const widget = widgetMap[WidgetId];
|
|
19101
|
+
return widget?.type ?? null;
|
|
19102
|
+
}
|
|
19103
|
+
|
|
19104
|
+
/**
|
|
19105
|
+
* Does the content have a specific type of widget?
|
|
19106
|
+
*
|
|
19107
|
+
* @param {string} type the type of the widget in question (ie "radio")
|
|
19108
|
+
* @param {string} content the string to search through
|
|
19109
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
19110
|
+
* @returns {boolean} if the content includes the widget type
|
|
19111
|
+
*/
|
|
19112
|
+
function contentHasWidgetType(type, content, widgetMap) {
|
|
19113
|
+
return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
|
|
19114
|
+
}
|
|
19115
|
+
|
|
19116
|
+
/**
|
|
19117
|
+
* Extract all widget IDs, which includes the widget type and instance number.
|
|
19118
|
+
* example output: ['radio 1', 'categorizer 1', 'categorizor 2']
|
|
19119
|
+
*
|
|
19120
|
+
* Content should contain Perseus widget placeholders,
|
|
19121
|
+
* which look like: '[[☃ radio 1]]'.
|
|
19122
|
+
*
|
|
19123
|
+
* @param {string} content
|
|
19124
|
+
* @returns {ReadonlyArray<string>} widgetIds
|
|
19125
|
+
*/
|
|
19126
|
+
function getWidgetIdsFromContent(content) {
|
|
19127
|
+
const widgets = [];
|
|
19128
|
+
const localWidgetRegex = getWidgetRegex();
|
|
19129
|
+
let match = localWidgetRegex.exec(content);
|
|
19130
|
+
while (match !== null) {
|
|
19131
|
+
widgets.push(match[1]);
|
|
19132
|
+
match = localWidgetRegex.exec(content);
|
|
19133
|
+
}
|
|
19134
|
+
return widgets;
|
|
19135
|
+
}
|
|
19136
|
+
|
|
19137
|
+
/**
|
|
19138
|
+
* Get a list of widget IDs from content,
|
|
19139
|
+
* but only for specific widget types
|
|
19140
|
+
*
|
|
19141
|
+
* @param {string} type the type of widget (ie "radio")
|
|
19142
|
+
* @param {string} content the string to parse
|
|
19143
|
+
* @param {PerseusWidgetsMap} widgetMap widget ID to widget map
|
|
19144
|
+
* @returns {ReadonlyArray<string>} the widget type (ie "radio")
|
|
19145
|
+
*/
|
|
19146
|
+
function getWidgetIdsFromContentByType(type, content, widgetMap) {
|
|
19147
|
+
const rv = [];
|
|
19148
|
+
const widgetIdsInContent = getWidgetIdsFromContent(content);
|
|
19149
|
+
widgetIdsInContent.forEach(widgetId => {
|
|
19150
|
+
const widget = widgetMap[widgetId];
|
|
19151
|
+
if (widget?.type === type) {
|
|
19152
|
+
rv.push(widgetId);
|
|
19153
|
+
}
|
|
19154
|
+
});
|
|
19155
|
+
return rv;
|
|
19156
|
+
}
|
|
19157
|
+
|
|
19158
|
+
/**
|
|
19159
|
+
* Pull the widget map out of ItemData
|
|
19160
|
+
*
|
|
19161
|
+
* @param {PerseusItem} PerseusItem containing a widgetMap
|
|
19162
|
+
* @returns {WidgetMap} the widget map in the PerseusItem
|
|
19163
|
+
*/
|
|
19164
|
+
function getWidgetsMapFromItemData(itemData) {
|
|
19165
|
+
return itemData.question.widgets;
|
|
19166
|
+
}
|
|
19167
|
+
|
|
19168
|
+
/**
|
|
19169
|
+
* Get the widget information from a WidgetMap
|
|
19170
|
+
*
|
|
19171
|
+
* @param {string} widgetId the ID of the widget
|
|
19172
|
+
* @returns {PerseusWidget | null} the widget data if it exists, otherwise null
|
|
19173
|
+
*/
|
|
19174
|
+
function getWidgetFromWidgetMap(widgetId, widgetMap) {
|
|
19175
|
+
return widgetMap[widgetId] ?? null;
|
|
19176
|
+
}
|
|
19177
|
+
|
|
19178
|
+
/**
|
|
19179
|
+
* Get select widgets from a widget map.
|
|
19180
|
+
* Useful for multi-items that needs to split
|
|
19181
|
+
* one PerseusItem into several
|
|
19182
|
+
*
|
|
19183
|
+
* @param {ReadonlyArray<string>} widgetIds to extract
|
|
19184
|
+
* @param {PerseusWidgetsMap} widgetMap to extract from
|
|
19185
|
+
* @return {PerseusWidgetsMap} a new widget map with requested widgets
|
|
19186
|
+
*/
|
|
19187
|
+
function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
|
|
19188
|
+
const widgets = {};
|
|
19189
|
+
widgetIds.forEach(widgetId => {
|
|
19190
|
+
const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
|
|
19191
|
+
if (widget) {
|
|
19192
|
+
widgets[widgetId] = widget;
|
|
19193
|
+
}
|
|
19194
|
+
});
|
|
19195
|
+
return widgets;
|
|
19196
|
+
}
|
|
19197
|
+
|
|
18877
19198
|
const DEFAULT_ALIGNMENT$1 = "block";
|
|
18878
19199
|
// NOTE(kevinb): "default" is not one in `validAlignments`.
|
|
18879
19200
|
const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
|
|
@@ -19334,6 +19655,27 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19334
19655
|
}
|
|
19335
19656
|
});
|
|
19336
19657
|
}
|
|
19658
|
+
|
|
19659
|
+
// TODO: combine scorePerseusItem with scoreWidgetsFunctional
|
|
19660
|
+
// once scorePerseusItem is the only one calling scoreWidgetsFunctional
|
|
19661
|
+
function scorePerseusItem(perseusRenderData, userInputMap,
|
|
19662
|
+
// TODO(LEMS-2461,LEMS-2391): these probably
|
|
19663
|
+
// need to be removed before we move this to the server
|
|
19664
|
+
strings, locale) {
|
|
19665
|
+
// Check if the PerseusRenderer object contains any deprecated widgets that need to be converted
|
|
19666
|
+
const mustConvertData = conversionRequired(perseusRenderData);
|
|
19667
|
+
const convertedRenderData = mustConvertData ? convertDeprecatedWidgets(perseusRenderData) // Convert deprecated widgets to their modern equivalents
|
|
19668
|
+
: perseusRenderData;
|
|
19669
|
+
const convertedUserInputMap = mustConvertData ? convertUserInputData(userInputMap) // Convert deprecated user input data keys to their modern equivalents
|
|
19670
|
+
: userInputMap;
|
|
19671
|
+
|
|
19672
|
+
// There seems to be a chance that PerseusRenderer.widgets might include
|
|
19673
|
+
// widget data for widgets that are not in PerseusRenderer.content,
|
|
19674
|
+
// so this checks that the widgets are being used before scoring them
|
|
19675
|
+
const usedWidgetIds = getWidgetIdsFromContent(convertedRenderData.content);
|
|
19676
|
+
const scores = scoreWidgetsFunctional(convertedRenderData.widgets, usedWidgetIds, convertedUserInputMap, strings, locale);
|
|
19677
|
+
return Util.flattenScores(scores);
|
|
19678
|
+
}
|
|
19337
19679
|
function scoreWidgetsFunctional(widgets,
|
|
19338
19680
|
// This is a port of old code, I'm not sure why
|
|
19339
19681
|
// we need widgetIds vs the keys of the widgets object
|
|
@@ -19354,7 +19696,7 @@ widgetIds, userInputMap, strings, locale) {
|
|
|
19354
19696
|
const userInput = userInputMap[id];
|
|
19355
19697
|
const scorer = getWidgetScorer(widget.type);
|
|
19356
19698
|
if (widget.type === "group") {
|
|
19357
|
-
const scores = scoreWidgetsFunctional(widget.options.widgets,
|
|
19699
|
+
const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
|
|
19358
19700
|
widgetScores[id] = Util.flattenScores(scores);
|
|
19359
19701
|
} else if (scorer) {
|
|
19360
19702
|
widgetScores[id] = scorer(userInput, widget.options, strings, locale);
|
|
@@ -20403,7 +20745,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20403
20745
|
// Instead, we center them with css magic in styles.less
|
|
20404
20746
|
// /cry(aria)
|
|
20405
20747
|
this._foundTextNodes = true;
|
|
20406
|
-
if (
|
|
20748
|
+
if (this.widgetIds.includes(node.id)) {
|
|
20407
20749
|
// We don't want to render a duplicate widget key/ref,
|
|
20408
20750
|
// as this causes problems with react (for obvious
|
|
20409
20751
|
// reasons). Instead we just notify the
|
|
@@ -20728,7 +21070,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20728
21070
|
};
|
|
20729
21071
|
getInputPaths = () => {
|
|
20730
21072
|
const inputPaths = [];
|
|
20731
|
-
|
|
21073
|
+
this.widgetIds.forEach(widgetId => {
|
|
20732
21074
|
const widget = this.getWidgetInstance(widgetId);
|
|
20733
21075
|
if (widget && widget.getInputPaths) {
|
|
20734
21076
|
// Grab all input paths and add widgetID to the front
|
|
@@ -20736,7 +21078,7 @@ class Renderer extends React__namespace.Component {
|
|
|
20736
21078
|
// Prefix paths with their widgetID and add to collective
|
|
20737
21079
|
// list of paths.
|
|
20738
21080
|
// @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
|
|
20739
|
-
|
|
21081
|
+
widgetInputPaths.forEach(inputPath => {
|
|
20740
21082
|
const relativeInputPath = [widgetId].concat(inputPath);
|
|
20741
21083
|
inputPaths.push(relativeInputPath);
|
|
20742
21084
|
});
|
|
@@ -20929,33 +21271,30 @@ class Renderer extends React__namespace.Component {
|
|
|
20929
21271
|
}
|
|
20930
21272
|
|
|
20931
21273
|
/**
|
|
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.
|
|
21274
|
+
* Scores the content.
|
|
21275
|
+
*
|
|
21276
|
+
* @deprecated use scorePerseusItem
|
|
20942
21277
|
*/
|
|
20943
21278
|
score() {
|
|
20944
|
-
const scores = this.
|
|
21279
|
+
const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
|
|
20945
21280
|
const combinedScore = Util.flattenScores(scores);
|
|
20946
21281
|
return combinedScore;
|
|
20947
21282
|
}
|
|
20948
|
-
|
|
21283
|
+
|
|
21284
|
+
/**
|
|
21285
|
+
* @deprecated use scorePerseusItem
|
|
21286
|
+
*/
|
|
21287
|
+
guessAndScore = () => {
|
|
20949
21288
|
const totalGuess = this.getUserInput();
|
|
20950
21289
|
const totalScore = this.score();
|
|
20951
21290
|
return [totalGuess, totalScore];
|
|
20952
|
-
}
|
|
21291
|
+
};
|
|
20953
21292
|
examples = () => {
|
|
20954
21293
|
const widgetIds = this.widgetIds;
|
|
20955
|
-
const examples =
|
|
21294
|
+
const examples = widgetIds.map(widgetId => {
|
|
20956
21295
|
const widget = this.getWidgetInstance(widgetId);
|
|
20957
21296
|
return widget != null && widget.examples ? widget.examples() : null;
|
|
20958
|
-
}));
|
|
21297
|
+
}).filter(Boolean);
|
|
20959
21298
|
|
|
20960
21299
|
// no widgets with examples
|
|
20961
21300
|
if (!examples.length) {
|
|
@@ -22463,7 +22802,9 @@ const propUpgrades$2 = {
|
|
|
22463
22802
|
// input-number to numeric-input. In this case, we need to upgrade
|
|
22464
22803
|
// the widget options accordingly.
|
|
22465
22804
|
if (initialProps.value) {
|
|
22466
|
-
|
|
22805
|
+
// If the answerType is not number or percent, we need to provide
|
|
22806
|
+
// the answer form for the numeric-input widget
|
|
22807
|
+
const provideAnswerForm = initialProps.answerType !== "number" && initialProps.answerType !== "percent";
|
|
22467
22808
|
|
|
22468
22809
|
// We need to determine the mathFormat for the numeric-input widget
|
|
22469
22810
|
const mathFormat = initialProps.answerType === "rational" ? "proper" // input-number uses "rational" for proper fractions
|
|
@@ -29528,6 +29869,9 @@ class Group extends React__namespace.Component {
|
|
|
29528
29869
|
this.change("widgets", this.rendererRef.props.widgets);
|
|
29529
29870
|
}
|
|
29530
29871
|
};
|
|
29872
|
+
|
|
29873
|
+
// TODO(LEMS-2391): replace this when there's a separate check
|
|
29874
|
+
// for valid/invalid state
|
|
29531
29875
|
const score = this.rendererRef?.score();
|
|
29532
29876
|
const isValid = score && score.type !== "invalid";
|
|
29533
29877
|
const isInvalid = score && score.type === "invalid";
|
|
@@ -31371,6 +31715,108 @@ function AxisArrows() {
|
|
|
31371
31715
|
})));
|
|
31372
31716
|
}
|
|
31373
31717
|
|
|
31718
|
+
/**
|
|
31719
|
+
* 44 is touch best practice and AAA compliant for WCAG
|
|
31720
|
+
* https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
|
|
31721
|
+
*/
|
|
31722
|
+
const TARGET_SIZE = 44;
|
|
31723
|
+
const REMOVE_BUTTON_ID = "perseus_mafs_remove_button";
|
|
31724
|
+
|
|
31725
|
+
// same as pointsFromNormalized in interactive-graph.tsx
|
|
31726
|
+
const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coords =>
|
|
31727
|
+
// maps over [x, y]
|
|
31728
|
+
coords.map((coord, i) => {
|
|
31729
|
+
const axisRange = range[i];
|
|
31730
|
+
if (noSnap) {
|
|
31731
|
+
return axisRange[MIN] + size(axisRange) * coord;
|
|
31732
|
+
}
|
|
31733
|
+
const axisStep = step[i];
|
|
31734
|
+
const nSteps = Math.floor(size(axisRange) / axisStep);
|
|
31735
|
+
const tick = Math.round(coord * nSteps);
|
|
31736
|
+
return axisRange[MIN] + axisStep * tick;
|
|
31737
|
+
}));
|
|
31738
|
+
|
|
31739
|
+
// same as normalizeCoords in interactive-graph.tsx
|
|
31740
|
+
const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
|
|
31741
|
+
return (coord + ranges[i][1]) / size(ranges[i]);
|
|
31742
|
+
}));
|
|
31743
|
+
|
|
31744
|
+
// Returns the closest point to the given `point` that is within the graph
|
|
31745
|
+
// bounds given in `state`.
|
|
31746
|
+
function bound$1(_ref) {
|
|
31747
|
+
let {
|
|
31748
|
+
snapStep,
|
|
31749
|
+
range,
|
|
31750
|
+
point
|
|
31751
|
+
} = _ref;
|
|
31752
|
+
const boundingBox = inset(snapStep, range);
|
|
31753
|
+
return clampToBox(boundingBox, point);
|
|
31754
|
+
}
|
|
31755
|
+
function isUnlimitedGraphState(state) {
|
|
31756
|
+
return state.type === "point" && state.numPoints === "unlimited" || state.type === "polygon" && state.numSides === "unlimited";
|
|
31757
|
+
}
|
|
31758
|
+
|
|
31759
|
+
/**
|
|
31760
|
+
* Replace all text outside of the $ TeX blocks with `\\text{...}`
|
|
31761
|
+
* This way, the entire resulting string can be rendered within <TeX>
|
|
31762
|
+
* and the text outside of the $ blocks will be non-TeX text.
|
|
31763
|
+
*/
|
|
31764
|
+
function replaceOutsideTeX(mathString) {
|
|
31765
|
+
// All the information we need is in the first section,
|
|
31766
|
+
// whether it's typed as "blockmath" or "paragraph"
|
|
31767
|
+
const firstSection = SimpleMarkdown__namespace.parse(mathString)[0];
|
|
31768
|
+
|
|
31769
|
+
// If it's blockMath, the outer level has the full math content.
|
|
31770
|
+
if (firstSection.type === "blockMath") {
|
|
31771
|
+
return firstSection.content;
|
|
31772
|
+
}
|
|
31773
|
+
|
|
31774
|
+
// If it's a paragraph, we need to iterate through the sections
|
|
31775
|
+
// to look for individual math blocks.
|
|
31776
|
+
const condensedNodes = condenseTextNodes(firstSection.content);
|
|
31777
|
+
let result = "";
|
|
31778
|
+
for (const piece of condensedNodes) {
|
|
31779
|
+
piece.type === "math" ? result += piece.content : result += `\\text{${escapeSpecialChars(piece.content)}}`;
|
|
31780
|
+
}
|
|
31781
|
+
return result;
|
|
31782
|
+
}
|
|
31783
|
+
// Helper function for replaceOutsideTeX()
|
|
31784
|
+
// Condense adjacent text nodes into a single text node
|
|
31785
|
+
function condenseTextNodes(nodes) {
|
|
31786
|
+
const result = [];
|
|
31787
|
+
if (!nodes) {
|
|
31788
|
+
return result;
|
|
31789
|
+
}
|
|
31790
|
+
let currentText = "";
|
|
31791
|
+
for (const node of nodes) {
|
|
31792
|
+
if (node.type === "math") {
|
|
31793
|
+
if (currentText) {
|
|
31794
|
+
result.push({
|
|
31795
|
+
type: "text",
|
|
31796
|
+
content: currentText
|
|
31797
|
+
});
|
|
31798
|
+
currentText = "";
|
|
31799
|
+
}
|
|
31800
|
+
result.push(node);
|
|
31801
|
+
} else {
|
|
31802
|
+
currentText += node.content;
|
|
31803
|
+
}
|
|
31804
|
+
}
|
|
31805
|
+
if (currentText) {
|
|
31806
|
+
result.push({
|
|
31807
|
+
type: "text",
|
|
31808
|
+
content: currentText
|
|
31809
|
+
});
|
|
31810
|
+
}
|
|
31811
|
+
return result;
|
|
31812
|
+
}
|
|
31813
|
+
|
|
31814
|
+
// Helper function for replaceOutsideTeX()
|
|
31815
|
+
function escapeSpecialChars(str) {
|
|
31816
|
+
// Escape $, \, {, and } characters
|
|
31817
|
+
return str.replace(/([$\\{}])/g, "\\$1");
|
|
31818
|
+
}
|
|
31819
|
+
|
|
31374
31820
|
function AxisLabels() {
|
|
31375
31821
|
const {
|
|
31376
31822
|
range,
|
|
@@ -31402,7 +31848,7 @@ function AxisLabels() {
|
|
|
31402
31848
|
fontSize: "14px",
|
|
31403
31849
|
transform: "translate(7px, -50%)"
|
|
31404
31850
|
}
|
|
31405
|
-
}, /*#__PURE__*/React__default["default"].createElement(TeX, null, xAxisLabelText)), /*#__PURE__*/React__default["default"].createElement("span", {
|
|
31851
|
+
}, /*#__PURE__*/React__default["default"].createElement(TeX, null, replaceOutsideTeX(xAxisLabelText))), /*#__PURE__*/React__default["default"].createElement("span", {
|
|
31406
31852
|
style: {
|
|
31407
31853
|
position: "absolute",
|
|
31408
31854
|
left: x2,
|
|
@@ -31410,7 +31856,7 @@ function AxisLabels() {
|
|
|
31410
31856
|
fontSize: "14px",
|
|
31411
31857
|
transform: "translate(-50%, 0px)"
|
|
31412
31858
|
}
|
|
31413
|
-
}, /*#__PURE__*/React__default["default"].createElement(TeX, null, yAxisLabelText)));
|
|
31859
|
+
}, /*#__PURE__*/React__default["default"].createElement(TeX, null, replaceOutsideTeX(yAxisLabelText))));
|
|
31414
31860
|
}
|
|
31415
31861
|
|
|
31416
31862
|
// The size of the ticks and labels in pixels
|
|
@@ -32348,108 +32794,6 @@ const lockedFigureFillStyles = {
|
|
|
32348
32794
|
|
|
32349
32795
|
const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
|
|
32350
32796
|
|
|
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
32797
|
function LockedLabel(props) {
|
|
32454
32798
|
const {
|
|
32455
32799
|
coord,
|
|
@@ -33678,6 +34022,7 @@ function useControlPoint(params) {
|
|
|
33678
34022
|
} = useGraphConfig();
|
|
33679
34023
|
const {
|
|
33680
34024
|
point,
|
|
34025
|
+
sequenceNumber,
|
|
33681
34026
|
color,
|
|
33682
34027
|
cursor,
|
|
33683
34028
|
constrain = p => snap(snapStep, p),
|
|
@@ -33718,6 +34063,7 @@ function useControlPoint(params) {
|
|
|
33718
34063
|
ref: focusableHandleRef,
|
|
33719
34064
|
role: "button",
|
|
33720
34065
|
"aria-label": strings.srPointAtCoordinates({
|
|
34066
|
+
num: sequenceNumber,
|
|
33721
34067
|
x: srFormatNumber(point[X], locale),
|
|
33722
34068
|
y: srFormatNumber(point[Y], locale)
|
|
33723
34069
|
})
|
|
@@ -33795,6 +34141,7 @@ const MovableLine = props => {
|
|
|
33795
34141
|
focusableHandle: focusableHandle1
|
|
33796
34142
|
} = useControlPoint({
|
|
33797
34143
|
point: start,
|
|
34144
|
+
sequenceNumber: 1,
|
|
33798
34145
|
color,
|
|
33799
34146
|
onMove: p => onMovePoint(0, p)
|
|
33800
34147
|
});
|
|
@@ -33803,6 +34150,7 @@ const MovableLine = props => {
|
|
|
33803
34150
|
focusableHandle: focusableHandle2
|
|
33804
34151
|
} = useControlPoint({
|
|
33805
34152
|
point: end,
|
|
34153
|
+
sequenceNumber: 2,
|
|
33806
34154
|
color,
|
|
33807
34155
|
onMove: p => onMovePoint(1, p)
|
|
33808
34156
|
});
|
|
@@ -33984,14 +34332,17 @@ function AngleGraph(props) {
|
|
|
33984
34332
|
// Render the lines, angle, and then movable points
|
|
33985
34333
|
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, svgLines, /*#__PURE__*/React__namespace.createElement(Angle, angleParams), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
33986
34334
|
point: coords[1],
|
|
34335
|
+
sequenceNumber: 1,
|
|
33987
34336
|
constrain: p => p,
|
|
33988
34337
|
onMove: destination => dispatch(actions.angle.movePoint(1, destination))
|
|
33989
34338
|
}), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
33990
34339
|
point: coords[0],
|
|
34340
|
+
sequenceNumber: 2,
|
|
33991
34341
|
constrain: getAngleSideConstraint(coords[0], coords[1], snapDegrees || 1),
|
|
33992
34342
|
onMove: destination => dispatch(actions.angle.movePoint(0, destination))
|
|
33993
34343
|
}), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
33994
34344
|
point: coords[2],
|
|
34345
|
+
sequenceNumber: 3,
|
|
33995
34346
|
constrain: getAngleSideConstraint(coords[2], coords[1], snapDegrees || 1),
|
|
33996
34347
|
onMove: destination => dispatch(actions.angle.movePoint(2, destination))
|
|
33997
34348
|
}));
|
|
@@ -34156,6 +34507,7 @@ function CircleGraph(props) {
|
|
|
34156
34507
|
onMove: c => dispatch(actions.circle.moveCenter(c))
|
|
34157
34508
|
}), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
34158
34509
|
point: radiusPoint,
|
|
34510
|
+
sequenceNumber: 1,
|
|
34159
34511
|
cursor: "ew-resize",
|
|
34160
34512
|
onMove: newRadiusPoint => {
|
|
34161
34513
|
dispatch(actions.circle.moveRadiusPoint(newRadiusPoint));
|
|
@@ -34331,6 +34683,7 @@ function LimitedPointGraph(props) {
|
|
|
34331
34683
|
return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
34332
34684
|
key: i,
|
|
34333
34685
|
point: point,
|
|
34686
|
+
sequenceNumber: i + 1,
|
|
34334
34687
|
onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination))
|
|
34335
34688
|
})));
|
|
34336
34689
|
}
|
|
@@ -34372,6 +34725,7 @@ function UnlimitedPointGraph(props) {
|
|
|
34372
34725
|
}), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
34373
34726
|
key: i,
|
|
34374
34727
|
point: point,
|
|
34728
|
+
sequenceNumber: i + 1,
|
|
34375
34729
|
onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
|
|
34376
34730
|
ref: ref => {
|
|
34377
34731
|
itemsRef.current[i] = ref;
|
|
@@ -34403,9 +34757,10 @@ function describePointGraph(state, i18n) {
|
|
|
34403
34757
|
if (state.coords.length === 0) {
|
|
34404
34758
|
return strings.srNoInteractiveElements;
|
|
34405
34759
|
}
|
|
34406
|
-
const pointDescriptions = state.coords.map(_ref2 => {
|
|
34760
|
+
const pointDescriptions = state.coords.map((_ref2, index) => {
|
|
34407
34761
|
let [x, y] = _ref2;
|
|
34408
34762
|
return strings.srPointAtCoordinates({
|
|
34763
|
+
num: index + 1,
|
|
34409
34764
|
x: srFormatNumber(x, locale),
|
|
34410
34765
|
y: srFormatNumber(y, locale)
|
|
34411
34766
|
});
|
|
@@ -34525,6 +34880,7 @@ const LimitedPolygonGraph = props => {
|
|
|
34525
34880
|
key: "point-" + i,
|
|
34526
34881
|
constrain: constrain,
|
|
34527
34882
|
point: point,
|
|
34883
|
+
sequenceNumber: i + 1,
|
|
34528
34884
|
onMove: destination => {
|
|
34529
34885
|
const now = Date.now();
|
|
34530
34886
|
const targetFPS = 40;
|
|
@@ -34671,6 +35027,7 @@ const UnlimitedPolygonGraph = props => {
|
|
|
34671
35027
|
}), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
34672
35028
|
key: i,
|
|
34673
35029
|
point: point,
|
|
35030
|
+
sequenceNumber: i + 1,
|
|
34674
35031
|
onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
|
|
34675
35032
|
ref: ref => {
|
|
34676
35033
|
pointRef.current[i] = ref;
|
|
@@ -34743,6 +35100,7 @@ function QuadraticGraph(props) {
|
|
|
34743
35100
|
}), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
34744
35101
|
key: "point-" + i,
|
|
34745
35102
|
point: coord,
|
|
35103
|
+
sequenceNumber: i + 1,
|
|
34746
35104
|
onMove: destination => dispatch(actions.quadratic.movePoint(i, destination))
|
|
34747
35105
|
})));
|
|
34748
35106
|
}
|
|
@@ -34871,6 +35229,7 @@ function SinusoidGraph(props) {
|
|
|
34871
35229
|
}), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
|
|
34872
35230
|
key: "point-" + i,
|
|
34873
35231
|
point: coord,
|
|
35232
|
+
sequenceNumber: i + 1,
|
|
34874
35233
|
onMove: destination => dispatch(actions.sinusoid.movePoint(i, destination))
|
|
34875
35234
|
})));
|
|
34876
35235
|
}
|
|
@@ -45039,7 +45398,7 @@ const init = function () {
|
|
|
45039
45398
|
|
|
45040
45399
|
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
45041
45400
|
const libName = "@khanacademy/perseus";
|
|
45042
|
-
const libVersion = "
|
|
45401
|
+
const libVersion = "42.0.1";
|
|
45043
45402
|
perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
45044
45403
|
|
|
45045
45404
|
const apiVersion = {
|
|
@@ -45644,6 +46003,13 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
45644
46003
|
this.props.onRendered(true);
|
|
45645
46004
|
}
|
|
45646
46005
|
}
|
|
46006
|
+
if (this.props.score && this.props.score !== prevProps.score) {
|
|
46007
|
+
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
46008
|
+
this.setState({
|
|
46009
|
+
questionCompleted: this.props.score.correct,
|
|
46010
|
+
questionHighlightedWidgets: emptyQuestionAreaWidgets
|
|
46011
|
+
});
|
|
46012
|
+
}
|
|
45647
46013
|
}
|
|
45648
46014
|
componentWillUnmount() {
|
|
45649
46015
|
if (this.blurTimeoutID != null) {
|
|
@@ -45779,18 +46145,37 @@ class ServerItemRenderer extends React__namespace.Component {
|
|
|
45779
46145
|
return this.questionRenderer.getPromptJSON();
|
|
45780
46146
|
}
|
|
45781
46147
|
|
|
46148
|
+
/**
|
|
46149
|
+
* Returns an array of the widget `.getUserInput()` results
|
|
46150
|
+
*
|
|
46151
|
+
* TODO: can we remove this? Seems to be just for backwards
|
|
46152
|
+
* compatibility with old Perseus Chrome logging
|
|
46153
|
+
* @deprecated use getUserInput
|
|
46154
|
+
*/
|
|
46155
|
+
getUserInputLegacy() {
|
|
46156
|
+
return this.questionRenderer.getUserInput();
|
|
46157
|
+
}
|
|
46158
|
+
|
|
46159
|
+
/**
|
|
46160
|
+
* Returns an object of the widget `.getUserInput()` results
|
|
46161
|
+
*/
|
|
46162
|
+
getUserInput() {
|
|
46163
|
+
return this.questionRenderer.getUserInputMap();
|
|
46164
|
+
}
|
|
46165
|
+
|
|
45782
46166
|
/**
|
|
45783
46167
|
* Grades the item.
|
|
46168
|
+
*
|
|
46169
|
+
* @deprecated use scorePerseusItem
|
|
45784
46170
|
*/
|
|
45785
46171
|
scoreInput() {
|
|
45786
|
-
const
|
|
45787
|
-
const
|
|
45788
|
-
const score = guessAndScore[1];
|
|
46172
|
+
const guess = this.getUserInput();
|
|
46173
|
+
const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
|
|
45789
46174
|
|
|
45790
46175
|
// Continue to include an empty guess for the now defunct answer area.
|
|
45791
46176
|
// TODO(alex): Check whether we rely on the format here for
|
|
45792
46177
|
// analyzing ProblemLogs. If not, remove this layer.
|
|
45793
|
-
const maxCompatGuess = [
|
|
46178
|
+
const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
|
|
45794
46179
|
const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
|
|
45795
46180
|
const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
|
|
45796
46181
|
this.setState({
|
|
@@ -47652,138 +48037,6 @@ const registerAllWidgetsForTesting = () => {
|
|
|
47652
48037
|
replaceDeprecatedWidgets();
|
|
47653
48038
|
};
|
|
47654
48039
|
|
|
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
48040
|
/**
|
|
47788
48041
|
* This function extracts the answers from the widgets.
|
|
47789
48042
|
*
|
|
@@ -49785,6 +50038,8 @@ exports.buildMapper = buildMapper;
|
|
|
49785
50038
|
exports.components = components;
|
|
49786
50039
|
exports.containerSizeClass = containerSizeClass;
|
|
49787
50040
|
exports.contentHasWidgetType = contentHasWidgetType;
|
|
50041
|
+
exports.conversionRequired = conversionRequired;
|
|
50042
|
+
exports.convertDeprecatedWidgets = convertDeprecatedWidgets;
|
|
49788
50043
|
exports.convertWidgetNameToEnum = convertWidgetNameToEnum;
|
|
49789
50044
|
exports.displaySigFigs = displaySigFigs;
|
|
49790
50045
|
exports.generateTestCategorizerWidget = generateTestCategorizerWidget;
|
|
@@ -49836,6 +50091,7 @@ exports.parsePerseusItem = parsePerseusItem;
|
|
|
49836
50091
|
exports.plotterPlotTypes = plotterPlotTypes;
|
|
49837
50092
|
exports.preprocessTex = preprocessTex;
|
|
49838
50093
|
exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
|
|
50094
|
+
exports.scorePerseusItem = scorePerseusItem;
|
|
49839
50095
|
exports.shapes = shapes;
|
|
49840
50096
|
exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
|
|
49841
50097
|
exports.traverse = traverse;
|