@khanacademy/perseus 44.0.0 → 45.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/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export { default as ServerItemRenderer } from "./server-item-renderer";
14
14
  export { default as HintsRenderer } from "./hints-renderer";
15
15
  export { default as HintRenderer } from "./hint-renderer";
16
16
  export { default as Renderer } from "./renderer";
17
+ export { scorePerseusItem } from "./renderer-util";
17
18
  /**
18
19
  * Widgets
19
20
  */
@@ -91,6 +92,7 @@ export type { Alignment, APIOptions, APIOptionsWithDefaults, ChangeHandler, Devi
91
92
  export type { ParsedValue } from "./util";
92
93
  export type { Hint, LegacyButtonSets, LockedFigure, LockedFigureColor, LockedFigureFillType, LockedFigureType, LockedPointType, LockedLineType, LockedVectorType, LockedEllipseType, LockedPolygonType, LockedFunctionType, LockedLabelType, LockedLineStyle, PerseusGraphType, PerseusAnswerArea, PerseusExpressionWidgetOptions, Range, Size, CollinearTuple, MathFormat, InputNumberWidget, // TODO(jeremy): remove?
93
94
  PerseusArticle, PerseusImageBackground, PerseusInputNumberWidgetOptions, PerseusInteractiveGraphWidgetOptions, PerseusItem, PerseusPhetSimulationWidgetOptions, PerseusPlotterWidgetOptions, PerseusPythonProgramWidgetOptions, PerseusRadioWidgetOptions, PerseusRenderer, PerseusWidget, PerseusWidgetsMap, MultiItem, } from "./perseus-types";
95
+ export type { UserInputMap } from "./validation.types";
94
96
  export type { Coord } from "./interactive2/types";
95
97
  export type { MarkerType } from "./widgets/label-image/types";
96
98
  /**
package/dist/index.js CHANGED
@@ -14757,9 +14757,9 @@ const pluck = function (table, subKey) {
14757
14757
  * });
14758
14758
  * {a: 2, b: 3}
14759
14759
  */
14760
- const mapObject$1 = function (obj, lambda) {
14760
+ const mapObject$2 = function (obj, lambda) {
14761
14761
  const result = {};
14762
- ___default["default"].each(___default["default"].keys(obj), function (key) {
14762
+ Object.keys(obj).forEach(key => {
14763
14763
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
14764
14764
  result[key] = lambda(obj[key], key);
14765
14765
  });
@@ -14776,14 +14776,14 @@ const mapObject$1 = function (obj, lambda) {
14776
14776
  */
14777
14777
  const mapObjectFromArray = function (arr, lambda) {
14778
14778
  const result = {};
14779
- ___default["default"].each(arr, function (elem) {
14779
+ arr.forEach(elem => {
14780
14780
  result[elem] = lambda(elem);
14781
14781
  });
14782
14782
  return result;
14783
14783
  };
14784
14784
  var objective_ = {
14785
14785
  pluck: pluck,
14786
- mapObject: mapObject$1,
14786
+ mapObject: mapObject$2,
14787
14787
  mapObjectFromArray: mapObjectFromArray
14788
14788
  };
14789
14789
 
@@ -18919,6 +18919,138 @@ class QuestionParagraph extends React__namespace.Component {
18919
18919
  }
18920
18920
  }
18921
18921
 
18922
+ // These are all the current widgets that require user-input and are supported for automatic scoring
18923
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
18924
+
18925
+ /**
18926
+ * Regex for widget placeholders in a string.
18927
+ *
18928
+ * First capture group is the widget ID (ex. 'radio 1')
18929
+ * Second capture group is the widget type (ex. "radio)
18930
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
18931
+ */
18932
+ function getWidgetRegex() {
18933
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
18934
+ }
18935
+
18936
+ /**
18937
+ * Add a widget placeholder using the widget ID.
18938
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
18939
+ *
18940
+ * @param {string} id
18941
+ * @returns {string}
18942
+ */
18943
+ function addWidget(id) {
18944
+ return `[[☃ ${id}]]`;
18945
+ }
18946
+
18947
+ /**
18948
+ * Get a widget type by a widget's ID
18949
+ * (please don't derive type from ID)
18950
+ *
18951
+ * @param {string} widgetId the ID of the widget
18952
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18953
+ * @returns {string} the widget type (ie "radio")
18954
+ */
18955
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
18956
+ const widget = widgetMap[WidgetId];
18957
+ return widget?.type ?? null;
18958
+ }
18959
+
18960
+ /**
18961
+ * Does the content have a specific type of widget?
18962
+ *
18963
+ * @param {string} type the type of the widget in question (ie "radio")
18964
+ * @param {string} content the string to search through
18965
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18966
+ * @returns {boolean} if the content includes the widget type
18967
+ */
18968
+ function contentHasWidgetType(type, content, widgetMap) {
18969
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
18970
+ }
18971
+
18972
+ /**
18973
+ * Extract all widget IDs, which includes the widget type and instance number.
18974
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
18975
+ *
18976
+ * Content should contain Perseus widget placeholders,
18977
+ * which look like: '[[☃ radio 1]]'.
18978
+ *
18979
+ * @param {string} content
18980
+ * @returns {ReadonlyArray<string>} widgetIds
18981
+ */
18982
+ function getWidgetIdsFromContent(content) {
18983
+ const widgets = [];
18984
+ const localWidgetRegex = getWidgetRegex();
18985
+ let match = localWidgetRegex.exec(content);
18986
+ while (match !== null) {
18987
+ widgets.push(match[1]);
18988
+ match = localWidgetRegex.exec(content);
18989
+ }
18990
+ return widgets;
18991
+ }
18992
+
18993
+ /**
18994
+ * Get a list of widget IDs from content,
18995
+ * but only for specific widget types
18996
+ *
18997
+ * @param {string} type the type of widget (ie "radio")
18998
+ * @param {string} content the string to parse
18999
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
19000
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
19001
+ */
19002
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
19003
+ const rv = [];
19004
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
19005
+ widgetIdsInContent.forEach(widgetId => {
19006
+ const widget = widgetMap[widgetId];
19007
+ if (widget?.type === type) {
19008
+ rv.push(widgetId);
19009
+ }
19010
+ });
19011
+ return rv;
19012
+ }
19013
+
19014
+ /**
19015
+ * Pull the widget map out of ItemData
19016
+ *
19017
+ * @param {PerseusItem} PerseusItem containing a widgetMap
19018
+ * @returns {WidgetMap} the widget map in the PerseusItem
19019
+ */
19020
+ function getWidgetsMapFromItemData(itemData) {
19021
+ return itemData.question.widgets;
19022
+ }
19023
+
19024
+ /**
19025
+ * Get the widget information from a WidgetMap
19026
+ *
19027
+ * @param {string} widgetId the ID of the widget
19028
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
19029
+ */
19030
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
19031
+ return widgetMap[widgetId] ?? null;
19032
+ }
19033
+
19034
+ /**
19035
+ * Get select widgets from a widget map.
19036
+ * Useful for multi-items that needs to split
19037
+ * one PerseusItem into several
19038
+ *
19039
+ * @param {ReadonlyArray<string>} widgetIds to extract
19040
+ * @param {PerseusWidgetsMap} widgetMap to extract from
19041
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
19042
+ */
19043
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
19044
+ const widgets = {};
19045
+ widgetIds.forEach(widgetId => {
19046
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
19047
+ if (widget) {
19048
+ widgets[widgetId] = widget;
19049
+ }
19050
+ });
19051
+ return widgets;
19052
+ }
19053
+
18922
19054
  const DEFAULT_ALIGNMENT$1 = "block";
18923
19055
  // NOTE(kevinb): "default" is not one in `validAlignments`.
18924
19056
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -19349,12 +19481,38 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19349
19481
  isLintable: isLintable
19350
19482
  });
19351
19483
 
19484
+ const {
19485
+ mapObject: mapObject$1
19486
+ } = objective_;
19487
+ function getUpgradedWidgetOptions(oldWidgetOptions) {
19488
+ // @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
19489
+ return mapObject$1(oldWidgetOptions, (widgetInfo, widgetId) => {
19490
+ if (!widgetInfo.type || !widgetInfo.alignment) {
19491
+ const newValues = {};
19492
+ if (!widgetInfo.type) {
19493
+ // TODO: why does widget have no type?
19494
+ // We don't want to derive type from widget ID
19495
+ // see: LEMS-1845
19496
+ newValues.type = widgetId.split(" ")[0];
19497
+ }
19498
+ if (!widgetInfo.alignment) {
19499
+ newValues.alignment = "default";
19500
+ }
19501
+ widgetInfo = {
19502
+ ...widgetInfo,
19503
+ ...newValues
19504
+ };
19505
+ }
19506
+ return upgradeWidgetInfoToLatestVersion(widgetInfo);
19507
+ });
19508
+ }
19352
19509
  function emptyWidgetsFunctional(widgets,
19353
19510
  // This is a port of old code, I'm not sure why
19354
19511
  // we need widgetIds vs the keys of the widgets object
19355
19512
  widgetIds, userInputMap, strings, locale) {
19513
+ const upgradedWidgets = getUpgradedWidgetOptions(widgets);
19356
19514
  return widgetIds.filter(id => {
19357
- const widget = widgets[id];
19515
+ const widget = upgradedWidgets[id];
19358
19516
  if (!widget || widget.static) {
19359
19517
  // Static widgets shouldn't count as empty
19360
19518
  return false;
@@ -19373,12 +19531,27 @@ widgetIds, userInputMap, strings, locale) {
19373
19531
  }
19374
19532
  });
19375
19533
  }
19534
+
19535
+ // TODO: combine scorePerseusItem with scoreWidgetsFunctional
19536
+ // once scorePerseusItem is the only one calling scoreWidgetsFunctional
19537
+ function scorePerseusItem(perseusRenderData, userInputMap,
19538
+ // TODO(LEMS-2461,LEMS-2391): these probably
19539
+ // need to be removed before we move this to the server
19540
+ strings, locale) {
19541
+ // There seems to be a chance that PerseusRenderer.widgets might include
19542
+ // widget data for widgets that are not in PerseusRenderer.content,
19543
+ // so this checks that the widgets are being used before scoring them
19544
+ const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
19545
+ const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
19546
+ return Util.flattenScores(scores);
19547
+ }
19376
19548
  function scoreWidgetsFunctional(widgets,
19377
19549
  // This is a port of old code, I'm not sure why
19378
19550
  // we need widgetIds vs the keys of the widgets object
19379
19551
  widgetIds, userInputMap, strings, locale) {
19552
+ const upgradedWidgets = getUpgradedWidgetOptions(widgets);
19380
19553
  const gradedWidgetIds = widgetIds.filter(id => {
19381
- const props = widgets[id];
19554
+ const props = upgradedWidgets[id];
19382
19555
  const widgetIsGraded = props?.graded == null || props.graded;
19383
19556
  const widgetIsStatic = !!props?.static;
19384
19557
  // Ungraded widgets or widgets set to static shouldn't be graded.
@@ -19386,14 +19559,14 @@ widgetIds, userInputMap, strings, locale) {
19386
19559
  });
19387
19560
  const widgetScores = {};
19388
19561
  gradedWidgetIds.forEach(id => {
19389
- const widget = widgets[id];
19562
+ const widget = upgradedWidgets[id];
19390
19563
  if (!widget) {
19391
19564
  return;
19392
19565
  }
19393
19566
  const userInput = userInputMap[id];
19394
19567
  const scorer = getWidgetScorer(widget.type);
19395
19568
  if (widget.type === "group") {
19396
- const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19569
+ const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
19397
19570
  widgetScores[id] = Util.flattenScores(scores);
19398
19571
  } else if (scorer) {
19399
19572
  widgetScores[id] = scorer(userInput, widget.options, strings, locale);
@@ -19918,32 +20091,12 @@ class Renderer extends React__namespace.Component {
19918
20091
  };
19919
20092
  };
19920
20093
  _getInitialWidgetState = props => {
19921
- const allWidgetInfo = this._getAllWidgetsInfo(props);
20094
+ const allWidgetInfo = getUpgradedWidgetOptions(props.widgets);
19922
20095
  return {
19923
20096
  widgetInfo: allWidgetInfo,
19924
20097
  widgetProps: this._getAllWidgetsStartProps(allWidgetInfo, props)
19925
20098
  };
19926
20099
  };
19927
-
19928
- // @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
19929
- _getAllWidgetsInfo = props => {
19930
- return mapObject(props.widgets, (widgetInfo, widgetId) => {
19931
- if (!widgetInfo.type || !widgetInfo.alignment) {
19932
- const newValues = {};
19933
- if (!widgetInfo.type) {
19934
- // TODO: why does widget have no type?
19935
- // We don't want to derive type from widget ID
19936
- // see: LEMS-1845
19937
- newValues.type = widgetId.split(" ")[0];
19938
- }
19939
- if (!widgetInfo.alignment) {
19940
- newValues.alignment = "default";
19941
- }
19942
- widgetInfo = ___default["default"].extend({}, widgetInfo, newValues);
19943
- }
19944
- return upgradeWidgetInfoToLatestVersion(widgetInfo);
19945
- });
19946
- };
19947
20100
  _getAllWidgetsStartProps = (allWidgetInfo, props) => {
19948
20101
  const {
19949
20102
  apiOptions,
@@ -20442,7 +20595,7 @@ class Renderer extends React__namespace.Component {
20442
20595
  // Instead, we center them with css magic in styles.less
20443
20596
  // /cry(aria)
20444
20597
  this._foundTextNodes = true;
20445
- if (___default["default"].contains(this.widgetIds, node.id)) {
20598
+ if (this.widgetIds.includes(node.id)) {
20446
20599
  // We don't want to render a duplicate widget key/ref,
20447
20600
  // as this causes problems with react (for obvious
20448
20601
  // reasons). Instead we just notify the
@@ -20767,7 +20920,7 @@ class Renderer extends React__namespace.Component {
20767
20920
  };
20768
20921
  getInputPaths = () => {
20769
20922
  const inputPaths = [];
20770
- ___default["default"].each(this.widgetIds, widgetId => {
20923
+ this.widgetIds.forEach(widgetId => {
20771
20924
  const widget = this.getWidgetInstance(widgetId);
20772
20925
  if (widget && widget.getInputPaths) {
20773
20926
  // Grab all input paths and add widgetID to the front
@@ -20775,7 +20928,7 @@ class Renderer extends React__namespace.Component {
20775
20928
  // Prefix paths with their widgetID and add to collective
20776
20929
  // list of paths.
20777
20930
  // @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
20778
- ___default["default"].each(widgetInputPaths, inputPath => {
20931
+ widgetInputPaths.forEach(inputPath => {
20779
20932
  const relativeInputPath = [widgetId].concat(inputPath);
20780
20933
  inputPaths.push(relativeInputPath);
20781
20934
  });
@@ -20968,33 +21121,30 @@ class Renderer extends React__namespace.Component {
20968
21121
  }
20969
21122
 
20970
21123
  /**
20971
- * Returns an object mapping from widget ID to perseus-style score.
20972
- * The keys of this object are the values of the array returned
20973
- * from `getWidgetIds`.
20974
- */
20975
- scoreWidgets() {
20976
- return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20977
- }
20978
-
20979
- /**
20980
- * Grades the content.
21124
+ * Scores the content.
21125
+ *
21126
+ * @deprecated use scorePerseusItem
20981
21127
  */
20982
21128
  score() {
20983
- const scores = this.scoreWidgets();
21129
+ const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20984
21130
  const combinedScore = Util.flattenScores(scores);
20985
21131
  return combinedScore;
20986
21132
  }
20987
- guessAndScore() {
21133
+
21134
+ /**
21135
+ * @deprecated use scorePerseusItem
21136
+ */
21137
+ guessAndScore = () => {
20988
21138
  const totalGuess = this.getUserInput();
20989
21139
  const totalScore = this.score();
20990
21140
  return [totalGuess, totalScore];
20991
- }
21141
+ };
20992
21142
  examples = () => {
20993
21143
  const widgetIds = this.widgetIds;
20994
- const examples = ___default["default"].compact(___default["default"].map(widgetIds, widgetId => {
21144
+ const examples = widgetIds.map(widgetId => {
20995
21145
  const widget = this.getWidgetInstance(widgetId);
20996
21146
  return widget != null && widget.examples ? widget.examples() : null;
20997
- }));
21147
+ }).filter(Boolean);
20998
21148
 
20999
21149
  // no widgets with examples
21000
21150
  if (!examples.length) {
@@ -29602,6 +29752,9 @@ class Group extends React__namespace.Component {
29602
29752
  this.change("widgets", this.rendererRef.props.widgets);
29603
29753
  }
29604
29754
  };
29755
+
29756
+ // TODO(LEMS-2391): replace this when there's a separate check
29757
+ // for valid/invalid state
29605
29758
  const score = this.rendererRef?.score();
29606
29759
  const isValid = score && score.type !== "invalid";
29607
29760
  const isInvalid = score && score.type === "invalid";
@@ -45227,7 +45380,7 @@ const init = function () {
45227
45380
 
45228
45381
  // This file is processed by a Rollup plugin (replace) to inject the production
45229
45382
  const libName = "@khanacademy/perseus";
45230
- const libVersion = "44.0.0";
45383
+ const libVersion = "45.0.0";
45231
45384
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45232
45385
 
45233
45386
  const apiVersion = {
@@ -45831,6 +45984,13 @@ class ServerItemRenderer extends React__namespace.Component {
45831
45984
  this.props.onRendered(true);
45832
45985
  }
45833
45986
  }
45987
+ if (this.props.score && this.props.score !== prevProps.score) {
45988
+ const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45989
+ this.setState({
45990
+ questionCompleted: this.props.score.correct,
45991
+ questionHighlightedWidgets: emptyQuestionAreaWidgets
45992
+ });
45993
+ }
45834
45994
  }
45835
45995
  componentWillUnmount() {
45836
45996
  if (this.blurTimeoutID != null) {
@@ -45966,18 +46126,37 @@ class ServerItemRenderer extends React__namespace.Component {
45966
46126
  return this.questionRenderer.getPromptJSON();
45967
46127
  }
45968
46128
 
46129
+ /**
46130
+ * Returns an array of the widget `.getUserInput()` results
46131
+ *
46132
+ * TODO: can we remove this? Seems to be just for backwards
46133
+ * compatibility with old Perseus Chrome logging
46134
+ * @deprecated use getUserInput
46135
+ */
46136
+ getUserInputLegacy() {
46137
+ return this.questionRenderer.getUserInput();
46138
+ }
46139
+
46140
+ /**
46141
+ * Returns an object of the widget `.getUserInput()` results
46142
+ */
46143
+ getUserInput() {
46144
+ return this.questionRenderer.getUserInputMap();
46145
+ }
46146
+
45969
46147
  /**
45970
46148
  * Grades the item.
46149
+ *
46150
+ * @deprecated use scorePerseusItem
45971
46151
  */
45972
46152
  scoreInput() {
45973
- const guessAndScore = this.questionRenderer.guessAndScore();
45974
- const guess = guessAndScore[0];
45975
- const score = guessAndScore[1];
46153
+ const guess = this.getUserInput();
46154
+ const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
45976
46155
 
45977
46156
  // Continue to include an empty guess for the now defunct answer area.
45978
46157
  // TODO(alex): Check whether we rely on the format here for
45979
46158
  // analyzing ProblemLogs. If not, remove this layer.
45980
- const maxCompatGuess = [guess, []];
46159
+ const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
45981
46160
  const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
45982
46161
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45983
46162
  this.setState({
@@ -47839,138 +48018,6 @@ const registerAllWidgetsForTesting = () => {
47839
48018
  replaceDeprecatedWidgets();
47840
48019
  };
47841
48020
 
47842
- // These are all the current widgets that require user-input and are supported for automatic scoring
47843
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
47844
-
47845
- /**
47846
- * Regex for widget placeholders in a string.
47847
- *
47848
- * First capture group is the widget ID (ex. 'radio 1')
47849
- * Second capture group is the widget type (ex. "radio)
47850
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
47851
- */
47852
- function getWidgetRegex() {
47853
- return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
47854
- }
47855
-
47856
- /**
47857
- * Add a widget placeholder using the widget ID.
47858
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
47859
- *
47860
- * @param {string} id
47861
- * @returns {string}
47862
- */
47863
- function addWidget(id) {
47864
- return `[[☃ ${id}]]`;
47865
- }
47866
-
47867
- /**
47868
- * Get a widget type by a widget's ID
47869
- * (please don't derive type from ID)
47870
- *
47871
- * @param {string} widgetId the ID of the widget
47872
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47873
- * @returns {string} the widget type (ie "radio")
47874
- */
47875
- function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
47876
- const widget = widgetMap[WidgetId];
47877
- return widget?.type ?? null;
47878
- }
47879
-
47880
- /**
47881
- * Does the content have a specific type of widget?
47882
- *
47883
- * @param {string} type the type of the widget in question (ie "radio")
47884
- * @param {string} content the string to search through
47885
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47886
- * @returns {boolean} if the content includes the widget type
47887
- */
47888
- function contentHasWidgetType(type, content, widgetMap) {
47889
- return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
47890
- }
47891
-
47892
- /**
47893
- * Extract all widget IDs, which includes the widget type and instance number.
47894
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
47895
- *
47896
- * Content should contain Perseus widget placeholders,
47897
- * which look like: '[[☃ radio 1]]'.
47898
- *
47899
- * @param {string} content
47900
- * @returns {ReadonlyArray<string>} widgetIds
47901
- */
47902
- function getWidgetIdsFromContent(content) {
47903
- const widgets = [];
47904
- const localWidgetRegex = getWidgetRegex();
47905
- let match = localWidgetRegex.exec(content);
47906
- while (match !== null) {
47907
- widgets.push(match[1]);
47908
- match = localWidgetRegex.exec(content);
47909
- }
47910
- return widgets;
47911
- }
47912
-
47913
- /**
47914
- * Get a list of widget IDs from content,
47915
- * but only for specific widget types
47916
- *
47917
- * @param {string} type the type of widget (ie "radio")
47918
- * @param {string} content the string to parse
47919
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47920
- * @returns {ReadonlyArray<string>} the widget type (ie "radio")
47921
- */
47922
- function getWidgetIdsFromContentByType(type, content, widgetMap) {
47923
- const rv = [];
47924
- const widgetIdsInContent = getWidgetIdsFromContent(content);
47925
- widgetIdsInContent.forEach(widgetId => {
47926
- const widget = widgetMap[widgetId];
47927
- if (widget?.type === type) {
47928
- rv.push(widgetId);
47929
- }
47930
- });
47931
- return rv;
47932
- }
47933
-
47934
- /**
47935
- * Pull the widget map out of ItemData
47936
- *
47937
- * @param {PerseusItem} PerseusItem containing a widgetMap
47938
- * @returns {WidgetMap} the widget map in the PerseusItem
47939
- */
47940
- function getWidgetsMapFromItemData(itemData) {
47941
- return itemData.question.widgets;
47942
- }
47943
-
47944
- /**
47945
- * Get the widget information from a WidgetMap
47946
- *
47947
- * @param {string} widgetId the ID of the widget
47948
- * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
47949
- */
47950
- function getWidgetFromWidgetMap(widgetId, widgetMap) {
47951
- return widgetMap[widgetId] ?? null;
47952
- }
47953
-
47954
- /**
47955
- * Get select widgets from a widget map.
47956
- * Useful for multi-items that needs to split
47957
- * one PerseusItem into several
47958
- *
47959
- * @param {ReadonlyArray<string>} widgetIds to extract
47960
- * @param {PerseusWidgetsMap} widgetMap to extract from
47961
- * @return {PerseusWidgetsMap} a new widget map with requested widgets
47962
- */
47963
- function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
47964
- const widgets = {};
47965
- widgetIds.forEach(widgetId => {
47966
- const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
47967
- if (widget) {
47968
- widgets[widgetId] = widget;
47969
- }
47970
- });
47971
- return widgets;
47972
- }
47973
-
47974
48021
  /**
47975
48022
  * This function extracts the answers from the widgets.
47976
48023
  *
@@ -50140,6 +50187,7 @@ exports.parsePerseusItem = parsePerseusItem;
50140
50187
  exports.plotterPlotTypes = plotterPlotTypes;
50141
50188
  exports.preprocessTex = preprocessTex;
50142
50189
  exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
50190
+ exports.scorePerseusItem = scorePerseusItem;
50143
50191
  exports.shapes = shapes;
50144
50192
  exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
50145
50193
  exports.traverse = traverse;