@khanacademy/perseus 52.0.1 → 53.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/components/number-input.d.ts +2 -0
  2. package/dist/components/range-input.d.ts +1 -0
  3. package/dist/components/svg-image.d.ts +2 -2
  4. package/dist/es/index.css +0 -30
  5. package/dist/es/index.js +893 -1042
  6. package/dist/es/index.js.map +1 -1
  7. package/dist/es/strings.js +42 -6
  8. package/dist/es/strings.js.map +1 -1
  9. package/dist/index.css +0 -30
  10. package/dist/index.d.ts +1 -2
  11. package/dist/index.js +913 -2263
  12. package/dist/index.js.map +1 -1
  13. package/dist/perseus-api.d.ts +0 -1
  14. package/dist/renderer.d.ts +1 -1
  15. package/dist/server-item-renderer.d.ts +0 -3
  16. package/dist/strings.d.ts +36 -0
  17. package/dist/strings.js +60 -6
  18. package/dist/strings.js.map +1 -1
  19. package/dist/styles/constants.d.ts +0 -2
  20. package/dist/types.d.ts +3 -32
  21. package/dist/util/math-utils.d.ts +1 -0
  22. package/dist/util/scoring.d.ts +0 -15
  23. package/dist/util/test-utils.d.ts +1 -1
  24. package/dist/widget-ai-utils/mock-widget/prompt-utils.d.ts +1 -1
  25. package/dist/widget-container.d.ts +1 -1
  26. package/dist/widgets/categorizer/categorizer.d.ts +0 -3
  27. package/dist/widgets/cs-program/cs-program.d.ts +2 -3
  28. package/dist/widgets/definition/definition.d.ts +0 -2
  29. package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +0 -1
  30. package/dist/widgets/dropdown/dropdown.d.ts +2 -4
  31. package/dist/widgets/explanation/explanation.d.ts +0 -2
  32. package/dist/widgets/expression/expression.d.ts +1 -11
  33. package/dist/widgets/grapher/grapher.d.ts +2 -2
  34. package/dist/widgets/group/group.d.ts +0 -4
  35. package/dist/widgets/iframe/iframe.d.ts +2 -3
  36. package/dist/widgets/image/image.d.ts +0 -3
  37. package/dist/widgets/input-number/input-number.d.ts +0 -3
  38. package/dist/widgets/interaction/interaction.d.ts +0 -1
  39. package/dist/widgets/interactive-graph.d.ts +2 -3
  40. package/dist/widgets/interactive-graphs/backgrounds/axis-ticks.d.ts +1 -0
  41. package/dist/widgets/interactive-graphs/graphs/polygon.d.ts +3 -2
  42. package/dist/widgets/interactive-graphs/graphs/screenreader-text.d.ts +1 -1
  43. package/dist/widgets/interactive-graphs/graphs/utils.d.ts +16 -1
  44. package/dist/widgets/interactive-graphs/mafs-graph.d.ts +0 -2
  45. package/dist/widgets/interactive-graphs/stateful-mafs-graph.d.ts +0 -2
  46. package/dist/widgets/label-image/label-image.d.ts +2 -2
  47. package/dist/widgets/matcher/matcher.d.ts +0 -2
  48. package/dist/widgets/matrix/matrix.d.ts +2 -4
  49. package/dist/widgets/measurer/measurer.d.ts +0 -1
  50. package/dist/widgets/mock-widgets/mock-widget-types.d.ts +0 -6
  51. package/dist/widgets/mock-widgets/mock-widget.d.ts +2 -5
  52. package/dist/widgets/molecule/molecule.d.ts +0 -1
  53. package/dist/widgets/number-line/number-line.d.ts +2 -3
  54. package/dist/widgets/numeric-input/numeric-input.d.ts +2 -3
  55. package/dist/widgets/orderer/orderer.d.ts +0 -3
  56. package/dist/widgets/passage/passage.d.ts +0 -1
  57. package/dist/widgets/passage-ref/passage-ref.d.ts +0 -2
  58. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +0 -2
  59. package/dist/widgets/plotter/plotter.d.ts +2 -3
  60. package/dist/widgets/radio/radio.d.ts +2 -4
  61. package/dist/widgets/sorter/sorter.d.ts +2 -4
  62. package/dist/widgets/table/table.d.ts +2 -3
  63. package/dist/widgets/video/video.d.ts +0 -3
  64. package/dist/widgets.d.ts +1 -32
  65. package/package.json +46 -46
  66. package/dist/renderer-util.d.ts +0 -14
  67. package/dist/widgets/__shared__/score-noop.d.ts +0 -11
  68. package/dist/widgets/group/score-group.d.ts +0 -4
  69. package/dist/widgets/group/validate-group.d.ts +0 -4
  70. package/dist/widgets/interactive-graphs/mafs-supported-graph-types.d.ts +0 -4
  71. package/dist/widgets/mock-widgets/score-mock-widget.d.ts +0 -5
  72. package/dist/widgets/mock-widgets/validate-mock-widget.d.ts +0 -4
package/dist/es/index.js CHANGED
@@ -1,10 +1,9 @@
1
1
  import * as KAS from '@khanacademy/kas';
2
2
  import { MathInputI18nContext, CursorContext, getKeyTranslator, createMathField, getCursorContext, DesktopKeypad, convertDotToTimesByLocale, mathQuillInstance, KeypadInput, KeypadType, KeyArray, keypadElementPropType } from '@khanacademy/math-input';
3
- import { PerseusError, Errors, expressionLogic, getExpressionPublicWidgetOptions, getDecimalSeparator, approximateDeepEqual, pluck, getWidgetIdsFromContent, mapObject, passageRefLogic, radioLogic, getCategorizerPublicWidgetOptions, GrapherUtil as GrapherUtil$1, lockedFigureColors, lockedFigureFillStyles, approximateEqual, getMatrixSize, measurerLogic, getOrdererPublicWidgetOptions, addLibraryVersionToPerseusDebug, getWidgetIdsFromContentByType } from '@khanacademy/perseus-core';
3
+ import { PerseusError, Errors, expressionLogic, getExpressionPublicWidgetOptions, getDecimalSeparator, approximateDeepEqual, pluck, CoreWidgetRegistry, getUpgradedWidgetOptions, mapObject, getNumericInputPublicWidgetOptions, passageRefLogic, radioLogic, getRadioPublicWidgetOptions, getCategorizerPublicWidgetOptions, getCSProgramPublicWidgetOptions, getDropdownPublicWidgetOptions, GrapherUtil as GrapherUtil$1, getGrapherPublicWidgetOptions, getIFramePublicWidgetOptions, lockedFigureColors, lockedFigureFillStyles, approximateEqual, getInteractiveGraphPublicWidgetOptions, getLabelImagePublicWidgetOptions, getMatrixPublicWidgetOptions, getMatrixSize, measurerLogic, getNumberLinePublicWidgetOptions, getOrdererPublicWidgetOptions, getPlotterPublicWidgetOptions, getSorterPublicWidgetOptions, getTablePublicWidgetOptions, addLibraryVersionToPerseusDebug, upgradeWidgetInfoToLatestVersion, getWidgetIdsFromContentByType } from '@khanacademy/perseus-core';
4
4
  export { isFailure, isSuccess, parseAndMigratePerseusArticle, parseAndMigratePerseusItem, parsePerseusItem } from '@khanacademy/perseus-core';
5
5
  import * as PerseusLinter from '@khanacademy/perseus-linter';
6
6
  import { linterContextDefault, Rule } from '@khanacademy/perseus-linter';
7
- import { scoreExpression, validateExpression, KhanAnswerTypes, scoreInputNumber, inputNumberAnswerTypes, scoreNumericInput, scoreRadio, validateRadio, scoreCategorizer, validateCategorizer, scoreCSProgram, scoreDropdown, validateDropdown, scoreGrapher, scoreIframe, scoreInteractiveGraph, scoreLabelImage, scoreLabelImageMarker, scoreMatcher, scoreMatrix, validateMatrix, scoreNumberLine, validateNumberLine, scoreOrderer, validateOrderer, scorePlotter, validatePlotter, scoreSorter, validateSorter, scoreTable, validateTable } from '@khanacademy/perseus-score';
8
7
  import { View, RenderStateRoot, Id, useOnMountEffect, useLatestRef, Text as Text$2 } from '@khanacademy/wonder-blocks-core';
9
8
  import Tooltip$1 from '@khanacademy/wonder-blocks-tooltip';
10
9
  import { HeadingMedium, LabelSmall, LabelLarge, Body, LabelMedium } from '@khanacademy/wonder-blocks-typography';
@@ -21,6 +20,7 @@ import { color, spacing, font } from '@khanacademy/wonder-blocks-tokens';
21
20
  import $ from 'jquery';
22
21
  import { v4 } from 'uuid';
23
22
  import PropTypes from 'prop-types';
23
+ import { KhanAnswerTypes, emptyWidgetsFunctional, scoreWidgetsFunctional, flattenScores, inputNumberAnswerTypes, scoreRadio, scoreLabelImageMarker } from '@khanacademy/perseus-score';
24
24
  import { entries, UnreachableCaseError, keys } from '@khanacademy/wonder-stuff-core';
25
25
  import { CircularSpinner } from '@khanacademy/wonder-blocks-progress-spinner';
26
26
  import { point, KhanMath, number, vector as vector$3, line, geometry, angles, coefficients } from '@khanacademy/kmath';
@@ -249,7 +249,7 @@ const mockStrings = {
249
249
  num,
250
250
  x,
251
251
  y
252
- }) => `Point ${num} at ${x} comma ${y}`,
252
+ }) => `Point ${num} at ${x} comma ${y}.`,
253
253
  srInteractiveElements: ({
254
254
  elements
255
255
  }) => `Interactive elements: ${elements}`,
@@ -437,7 +437,32 @@ const mockStrings = {
437
437
  point2Y,
438
438
  point3X,
439
439
  point3Y
440
- }) => `Parabola with points at ${point1X} comma ${point1Y}, ${point2X} comma ${point2Y}, and ${point3X} comma ${point3Y}.`
440
+ }) => `Parabola with points at ${point1X} comma ${point1Y}, ${point2X} comma ${point2Y}, and ${point3X} comma ${point3Y}.`,
441
+ srPolygonGraph: "A polygon.",
442
+ srPolygonGraphCoordinatePlane: "A polygon on a coordinate plane.",
443
+ srPolygonGraphPointsNum: ({
444
+ num
445
+ }) => `The polygon has ${num} points.`,
446
+ srPolygonGraphPointsOne: "The polygon has 1 point.",
447
+ srPolygonElementsNum: ({
448
+ num
449
+ }) => `A polygon with ${num} points.`,
450
+ srPolygonElementsOne: "A polygon with 1 point.",
451
+ srPolygonPointAngleApprox: ({
452
+ angle
453
+ }) => `Angle approximately equal to ${angle} degrees.`,
454
+ srPolygonPointAngle: ({
455
+ angle
456
+ }) => `Angle equal to ${angle} degrees.`,
457
+ srPolygonSideLength: ({
458
+ pointNum,
459
+ length
460
+ }) => `A line segment, length equal to ${length} units, connects to point ${pointNum}.`,
461
+ srPolygonSideLengthApprox: ({
462
+ pointNum,
463
+ length
464
+ }) => `A line segment, length approximately equal to ${length} units, connects to point ${pointNum}.`,
465
+ srUnlimitedPolygonEmpty: "An empty coordinate plane."
441
466
  // The above strings are used for interactive graph SR descriptions.
442
467
  };
443
468
 
@@ -1389,7 +1414,6 @@ const ClassNames = {
1389
1414
  SELECTED: "perseus-radio-selected",
1390
1415
  OPTION_CONTENT: "perseus-radio-option-content"
1391
1416
  },
1392
- INTERACTIVE: "perseus-interactive",
1393
1417
  CORRECT: "perseus-correct",
1394
1418
  INCORRECT: "perseus-incorrect",
1395
1419
  UNANSWERED: "perseus-unanswered",
@@ -1637,7 +1661,6 @@ class Expression extends React.Component {
1637
1661
  // eslint-disable-next-line react/no-string-refs
1638
1662
  , {
1639
1663
  ref: "input",
1640
- className: ClassNames.INTERACTIVE,
1641
1664
  value: this.props.value,
1642
1665
  onChange: this.changeAndTrack,
1643
1666
  convertDotToTimes: this.props.times,
@@ -1750,7 +1773,6 @@ var Expression$1 = {
1750
1773
  name: "expression",
1751
1774
  displayName: "Expression / Equation",
1752
1775
  accessible: true,
1753
- defaultAlignment: "inline-block",
1754
1776
  widget: ExpressionWithDependencies,
1755
1777
  transform: widgetOptions => {
1756
1778
  const {
@@ -1775,12 +1797,6 @@ var Expression$1 = {
1775
1797
  propUpgrades: expressionLogic.widgetOptionsUpgrades,
1776
1798
  // For use by the editor
1777
1799
  isLintable: true,
1778
- // TODO(LEMS-2656): remove TS suppression
1779
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'.
1780
- scorer: scoreExpression,
1781
- // TODO(LEMS-2656): remove TS suppression
1782
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'.
1783
- validator: validateExpression,
1784
1800
  getPublicWidgetOptions: getExpressionPublicWidgetOptions,
1785
1801
  // TODO(LEMS-2656): remove TS suppression
1786
1802
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusExpressionRubric'.
@@ -3061,10 +3077,6 @@ const pureMdMax$1 = "1023px";
3061
3077
  const pureLgMax$1 = "1279px";
3062
3078
  // @tableBackgroundAccent: #f9f9f9; // for striping
3063
3079
  const tableBackgroundAccent = "#F9F9F9";
3064
- // @zIndexAboveScratchpad: @zIndexScratchPad + 1;
3065
- const zIndexAboveScratchpad$1 = 2;
3066
- // @zIndexInteractiveComponent: @zIndexAboveScratchpad + 1;
3067
- const zIndexInteractiveComponent$1 = 3;
3068
3080
  // @phoneMargin: 16px;
3069
3081
  const phoneMargin = 16;
3070
3082
  const negativePhoneMargin = -16;
@@ -3108,8 +3120,6 @@ var constants = /*#__PURE__*/Object.freeze({
3108
3120
  pureMdMax: pureMdMax$1,
3109
3121
  pureLgMax: pureLgMax$1,
3110
3122
  tableBackgroundAccent: tableBackgroundAccent,
3111
- zIndexAboveScratchpad: zIndexAboveScratchpad$1,
3112
- zIndexInteractiveComponent: zIndexInteractiveComponent$1,
3113
3123
  phoneMargin: phoneMargin,
3114
3124
  negativePhoneMargin: negativePhoneMargin,
3115
3125
  hintBorderWidth: hintBorderWidth,
@@ -9853,7 +9863,7 @@ class WrappedLine extends WrappedDrawing {
9853
9863
 
9854
9864
  /* eslint-disable @babel/no-invalid-this */
9855
9865
  const {
9856
- clockwise: clockwise$3,
9866
+ clockwise: clockwise$2,
9857
9867
  reverseVector: reverseVector$1
9858
9868
  } = geometry;
9859
9869
  function scaledDistanceFromAngle(angle) {
@@ -11091,7 +11101,7 @@ _.extend(GraphUtils.Graphie.prototype, {
11091
11101
  // Temporary objects
11092
11102
  _.invoke(polygon.temp, "remove");
11093
11103
  polygon.temp = [];
11094
- const isClockwise = clockwise$3(polygon.coords);
11104
+ const isClockwise = clockwise$2(polygon.coords);
11095
11105
 
11096
11106
  // Update angle labels
11097
11107
  if (polygon.angleLabels.length || polygon.showRightAngleMarkers.length) {
@@ -14482,7 +14492,7 @@ function sum(array) {
14482
14492
  return memo + arg;
14483
14493
  }, 0);
14484
14494
  }
14485
- function clockwise$2(points) {
14495
+ function clockwise$1(points) {
14486
14496
  const segments = _.zip(points, points.slice(1).concat(points.slice(0, 1)));
14487
14497
  const areas = _.map(segments, function (segment) {
14488
14498
  const p1 = segment[0];
@@ -14548,7 +14558,7 @@ const draw$1 = {
14548
14558
  const graphie = this.graphie;
14549
14559
  const self = this;
14550
14560
  const coords = _.invoke(state.points, "coord");
14551
- const isClockwise = clockwise$2(coords);
14561
+ const isClockwise = clockwise$1(coords);
14552
14562
  const n = coords.length;
14553
14563
 
14554
14564
  // graphie.labelAngle and similar methods attempt to re-use the label
@@ -16285,9 +16295,29 @@ class Zoomable extends React.Component {
16285
16295
  if (!this._isMounted) {
16286
16296
  return;
16287
16297
  }
16298
+
16299
+ // When a zoomable item is contained within a collapsed parent (such as an Explanation widget),
16300
+ // the parent node has zero width, which causes the scale to be miscalculated.
16301
+ // Therefore, we check to see if the node has a valid width, and if it doesn't,
16302
+ // then we traverse up the DOM tree and look for the closest "perseus-renderer"
16303
+ // (which should have a width).
16304
+ // To be cautious, we also account for the possibility that we can't find any parents with a valid width
16305
+ // (not that they don't exist, just that we can't find them).
16306
+ let parentNode = this._node;
16307
+ let currentNode = parentNode;
16308
+ while (currentNode && currentNode.offsetWidth === 0) {
16309
+ var _currentNode$parentEl, _currentNode$parentEl2;
16310
+ // When traversing up the DOM tree, we need to start at the "parentElement"
16311
+ // because "closest()" can match the current element,
16312
+ // and if it does so, then we are stuck in an infinite loop.
16313
+ currentNode = (_currentNode$parentEl = (_currentNode$parentEl2 = currentNode.parentElement) == null ? void 0 : _currentNode$parentEl2.closest(".perseus-renderer")) != null ? _currentNode$parentEl : null;
16314
+ }
16315
+ if (currentNode) {
16316
+ parentNode = currentNode;
16317
+ }
16288
16318
  const parentBounds = {
16289
- width: this._node.offsetWidth,
16290
- height: this._node.offsetHeight
16319
+ width: parentNode.offsetWidth,
16320
+ height: parentNode.offsetHeight
16291
16321
  };
16292
16322
  const childBounds = this.props.computeChildBounds(this._node, parentBounds);
16293
16323
 
@@ -17334,130 +17364,162 @@ class QuestionParagraph extends React.Component {
17334
17364
  }
17335
17365
  }
17336
17366
 
17337
- const noScore = {
17338
- type: "points",
17339
- earned: 0,
17340
- total: 0,
17341
- message: null
17342
- };
17343
-
17344
17367
  /**
17345
- * If a widget says that it is empty once it is graded.
17346
- * Trying to encapsulate references to the score format.
17347
- */
17348
- function scoreIsEmpty(score) {
17349
- // HACK(benkomalo): ugh. this isn't great; the Perseus score objects
17350
- // overload the type "invalid" for what should probably be three
17351
- // distinct cases:
17352
- // - truly empty or not fully filled out
17353
- // - invalid or malformed inputs
17354
- // - "almost correct" like inputs where the widget wants to give
17355
- // feedback (e.g. a fraction needs to be reduced, or `pi` should
17356
- // be used instead of 3.14)
17357
- //
17358
- // Unfortunately the coercion happens all over the place, as these
17359
- // Perseus style score objects are created *everywhere* (basically
17360
- // in every widget), so it's hard to change now. We assume that
17361
- // anything with a "message" is not truly empty, and one of the
17362
- // latter two cases for now.
17363
- return score.type === "invalid" && (!score.message || score.message.length === 0);
17364
- }
17365
-
17366
- /**
17367
- * Combine two score objects.
17368
+ * TranslationLinter asynchronously calls Khan Academy's poentry linter.
17368
17369
  *
17369
- * Given two score objects for two different widgets, combine them so that
17370
- * if one is wrong, the total score is wrong, etc.
17370
+ * The poentry linter checks for issues that prevent content from being
17371
+ * translated. This linter is KA-specific and so TranslationLinter does not do
17372
+ * anything in non-KA environments.
17371
17373
  */
17372
- function combineScores(scoreA, scoreB) {
17373
- let message;
17374
- if (scoreA.type === "points" && scoreB.type === "points") {
17375
- if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
17376
- // TODO(alpert): Figure out how to combine messages usefully
17377
- message = null;
17378
- } else {
17379
- message = scoreA.message || scoreB.message;
17380
- }
17381
- return {
17382
- type: "points",
17383
- earned: scoreA.earned + scoreB.earned,
17384
- total: scoreA.total + scoreB.total,
17385
- message: message
17386
- };
17387
- }
17388
- if (scoreA.type === "points" && scoreB.type === "invalid") {
17389
- return scoreB;
17390
- }
17391
- if (scoreA.type === "invalid" && scoreB.type === "points") {
17392
- return scoreA;
17393
- }
17394
- if (scoreA.type === "invalid" && scoreB.type === "invalid") {
17395
- if (scoreA.message && scoreB.message && scoreA.message !== scoreB.message) {
17396
- // TODO(alpert): Figure out how to combine messages usefully
17397
- message = null;
17398
- } else {
17399
- message = scoreA.message || scoreB.message;
17400
- }
17401
- return {
17402
- type: "invalid",
17403
- message: message
17404
- };
17405
- }
17406
17374
 
17375
+ // A LintCB is called once the linter has been run.
17376
+
17377
+ // msec to wait before we actually call the linter after receiving the
17378
+ // last call. The timer is reset if the lint is called during the wait.
17379
+ const DEBOUNCE_TIMEOUT = 1000;
17380
+ class TranslationLinter {
17381
+ constructor() {
17382
+ // To avoid linting when nothing has changed.
17383
+ this.previousContent = null;
17384
+ /**
17385
+ * Runs the poentry linter on the given perseus string asynchronously.
17386
+ *
17387
+ * It doesn't do anything unless we're in a KA environment.
17388
+ *
17389
+ * onLintErrorsGenerated is called even if there are no errors. It is not
17390
+ * called if perseusStr is unchanged from the last call.
17391
+ *
17392
+ * Linting is relatively expensive -- so we debounce linting each instance
17393
+ * for some amount of time.
17394
+ */
17395
+ this.runLinter = debounce$1((perseusStr, onLintErrorsGenerated) => {
17396
+ // TODO(FEI-5003): Do we still need to check for 'KA'?
17397
+ if (typeof KA === "undefined") {
17398
+ return;
17399
+ }
17400
+ if (perseusStr === this.previousContent) {
17401
+ return;
17402
+ }
17403
+ this.previousContent = perseusStr;
17404
+ if (perseusStr === "") {
17405
+ onLintErrorsGenerated([]);
17406
+ return;
17407
+ }
17408
+ fetch("/api/internal/translations/lint_poentry?preview=1&lang=en", {
17409
+ headers: {
17410
+ "Content-Type": "application/json"
17411
+ },
17412
+ body: JSON.stringify({
17413
+ // The poentry linter verifies that the translation from
17414
+ // the original text to the translated text is sane. We
17415
+ // want to ensure that the translation from English to
17416
+ // English is sane, so msgid === mstr.
17417
+ msgid: perseusStr,
17418
+ // msgid is the original text
17419
+ msgstr: perseusStr,
17420
+ // msgstr is the translated text
17421
+ format: "perseus_text",
17422
+ filename: ""
17423
+ }),
17424
+ method: "POST"
17425
+ }).then(response => {
17426
+ if (response.status >= 400) {
17427
+ return {
17428
+ status: "error",
17429
+ message: "Could not run i18n linter."
17430
+ };
17431
+ }
17432
+ return response.json();
17433
+ }, rejection => {
17434
+ return {
17435
+ status: "error",
17436
+ message: "Could not run i18n linter."
17437
+ };
17438
+ }).then(json => {
17439
+ if (json.status === "error") {
17440
+ onLintErrorsGenerated(["Some part of this text makes it untranslatable. " + "The specific message from the i18n linter was: " + json.message.replace(/\n/g, " ")]);
17441
+ } else {
17442
+ onLintErrorsGenerated([]);
17443
+ }
17444
+ });
17445
+ }, DEBOUNCE_TIMEOUT);
17446
+ }
17407
17447
  /**
17408
- * The above checks cover all combinations of score type, so if we get here
17409
- * then something is amiss with our inputs.
17448
+ * Applies an array of errors generated by linters without position
17449
+ * information (TranslationLinter and the legacy getSaveWarnings()) to the
17450
+ * top of a Perseus tree.
17410
17451
  */
17411
- throw new PerseusError("PerseusScore with unknown type encountered", Errors.InvalidInput, {
17412
- metadata: {
17413
- scoreA: JSON.stringify(scoreA),
17414
- scoreB: JSON.stringify(scoreB)
17452
+ applyLintErrors(parsedMarkdown, translationLintErrors) {
17453
+ // These lint errors do not have position data associated with
17454
+ // them, so we just plop them at the top.
17455
+ if (translationLintErrors.length) {
17456
+ const errorText = translationLintErrors.join("\n\n");
17457
+ parsedMarkdown.unshift({
17458
+ content: {
17459
+ type: "text",
17460
+ content: ""
17461
+ },
17462
+ insideTable: false,
17463
+ message: errorText,
17464
+ ruleName: "legacy-error",
17465
+ severity: Rule.Severity.ERROR,
17466
+ type: "lint"
17467
+ });
17415
17468
  }
17416
- });
17417
- }
17418
- function flattenScores(widgetScoreMap) {
17419
- return Object.values(widgetScoreMap).reduce(combineScores, noScore);
17420
- }
17421
- function isCorrect(score) {
17422
- return score.type === "points" && score.earned >= score.total;
17469
+ }
17423
17470
  }
17424
- function keScoreFromPerseusScore(score,
17425
- // It's weird, but this is what we're passing it
17426
- guess, state) {
17427
- if (score.type === "points") {
17428
- return {
17429
- empty: false,
17430
- correct: isCorrect(score),
17431
- message: score.message,
17432
- guess: guess,
17433
- state: state
17434
- };
17471
+
17472
+ /**
17473
+ * Preprocess TeX code to convert things that MathJax doesn't know how to handle
17474
+ * to things is does.
17475
+ */
17476
+ var preprocessTex = (texCode => texCode
17477
+ // Replace uses of \begin{align}...\end{align} which KaTeX doesn't
17478
+ // support (yet) with \begin{aligned}...\end{aligned} which renders
17479
+ // the same is supported by KaTeX. It does the same for align*.
17480
+ // TODO(kevinb) update content to use aligned instead of align.
17481
+ // TODO(LEMS-1608) Remove this replacement as MathJax supports the
17482
+ // "align" macro correctly (and, in fact, it is not synonymous with
17483
+ // "aligned").
17484
+ .replace(/\{align[*]?\}/g, "{aligned}")
17485
+ // Replace non-breaking spaces with regular spaces.
17486
+ .replace(/[\u00a0]/g, " "));
17487
+
17488
+ const {
17489
+ interactiveSizes
17490
+ } = constants;
17491
+
17492
+ // Note: these size cutoffs represent content-width cutoffs as specified in
17493
+ // http://zpl.io/1mVmvU (broken link - we no longer use Zeplin)
17494
+ // TODO(benkomalo): these values aren't used in JS outside of this file, but
17495
+ // are coupled to the values in styles/styles.less - DRY it up at some point
17496
+ const smMax = articleMaxWidthTableInPx;
17497
+ const mdMax = articleMaxWidthInPx;
17498
+ const containerSizeClass = {
17499
+ SMALL: "small",
17500
+ MEDIUM: "medium",
17501
+ LARGE: "large",
17502
+ XLARGE: "xlarge"
17503
+ };
17504
+ const getClassFromWidth = width => {
17505
+ if (!width) {
17506
+ return containerSizeClass.MEDIUM;
17435
17507
  }
17436
- if (score.type === "invalid") {
17437
- return {
17438
- empty: true,
17439
- correct: false,
17440
- message: score.message,
17441
- suppressAlmostThere: score.suppressAlmostThere,
17442
- guess: guess,
17443
- state: state
17444
- };
17508
+ if (width <= smMax) {
17509
+ return containerSizeClass.SMALL;
17445
17510
  }
17446
- throw new PerseusError(
17447
- // @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
17448
- "Invalid score type: " + score.type, Errors.InvalidInput, {
17449
- metadata: {
17450
- score: JSON.stringify(score),
17451
- guess: JSON.stringify(guess),
17452
- state: JSON.stringify(state)
17453
- }
17454
- });
17455
- }
17511
+ if (width <= mdMax) {
17512
+ return containerSizeClass.MEDIUM;
17513
+ }
17514
+ return containerSizeClass.LARGE;
17515
+ };
17516
+ const getInteractiveBoxFromSizeClass = sizeClass => {
17517
+ if (sizeClass === containerSizeClass.SMALL) {
17518
+ return [interactiveSizes.defaultBoxSizeSmall, interactiveSizes.defaultBoxSizeSmall];
17519
+ }
17520
+ return [interactiveSizes.defaultBoxSize, interactiveSizes.defaultBoxSize];
17521
+ };
17456
17522
 
17457
- const DEFAULT_ALIGNMENT$1 = "block";
17458
- // NOTE(kevinb): "default" is not one in `validAlignments`.
17459
- const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
17460
- const DEFAULT_STATIC = false;
17461
17523
  const DEFAULT_TRACKING = "";
17462
17524
  const DEFAULT_LINTABLE = false;
17463
17525
  const widgets = {};
@@ -17474,7 +17536,6 @@ const registerWidgets = widgets => {
17474
17536
  widgets.forEach(widget => {
17475
17537
  registerWidget(widget.name, widget);
17476
17538
  });
17477
- validateAlignments();
17478
17539
  };
17479
17540
 
17480
17541
  /**
@@ -17558,17 +17619,9 @@ const getWidgetExport = name => {
17558
17619
  var _widgets$name3;
17559
17620
  return (_widgets$name3 = widgets[name]) != null ? _widgets$name3 : null;
17560
17621
  };
17561
- const getWidgetValidator = name => {
17562
- var _widgets$name$validat, _widgets$name4;
17563
- return (_widgets$name$validat = (_widgets$name4 = widgets[name]) == null ? void 0 : _widgets$name4.validator) != null ? _widgets$name$validat : null;
17564
- };
17565
- const getWidgetScorer = name => {
17566
- var _widgets$name$scorer, _widgets$name5;
17567
- return (_widgets$name$scorer = (_widgets$name5 = widgets[name]) == null ? void 0 : _widgets$name5.scorer) != null ? _widgets$name$scorer : null;
17568
- };
17569
17622
  const getPublicWidgetOptionsFunction = name => {
17570
- var _widgets$name$getPubl, _widgets$name6;
17571
- return (_widgets$name$getPubl = (_widgets$name6 = widgets[name]) == null ? void 0 : _widgets$name6.getPublicWidgetOptions) != null ? _widgets$name$getPubl : i => i;
17623
+ var _widgets$name$getPubl, _widgets$name4;
17624
+ return (_widgets$name$getPubl = (_widgets$name4 = widgets[name]) == null ? void 0 : _widgets$name4.getPublicWidgetOptions) != null ? _widgets$name$getPubl : i => i;
17572
17625
  };
17573
17626
  const getEditor = name => {
17574
17627
  return _.has(editors, name) ? editors[name] : null;
@@ -17612,109 +17665,6 @@ const isAccessible = widgetInfo => {
17612
17665
  const getAllWidgetTypes = () => {
17613
17666
  return _.keys(widgets);
17614
17667
  };
17615
- const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
17616
- const type = oldWidgetInfo.type;
17617
- // NOTE(jeremy): This looks like it could be replaced by fixing types so
17618
- // that `type` is non-optional. But we're seeing this in Sentry today so I
17619
- // suspect we have legacy data (potentially unpublished) and we should
17620
- // figure that out before depending solely on types.
17621
- if (!_.isString(type)) {
17622
- throw new PerseusError("widget type must be a string, but was: " + type, Errors.Internal);
17623
- }
17624
- const widgetExports = widgets[type];
17625
- if (widgetExports == null) {
17626
- // If we have a widget that isn't registered, we can't upgrade it
17627
- // TODO(aria): Figure out what the best thing to do here would be
17628
- return oldWidgetInfo;
17629
- }
17630
-
17631
- // Unversioned widgets (pre-July 2014) are all implicitly 0.0
17632
- const initialVersion = oldWidgetInfo.version || {
17633
- major: 0,
17634
- minor: 0
17635
- };
17636
- const latestVersion = widgetExports.version || {
17637
- major: 0,
17638
- minor: 0
17639
- };
17640
-
17641
- // If the widget version is later than what we understand (major
17642
- // version is higher than latest, or major versions are equal and minor
17643
- // version is higher than latest), don't perform any upgrades.
17644
- if (initialVersion.major > latestVersion.major || initialVersion.major === latestVersion.major && initialVersion.minor > latestVersion.minor) {
17645
- return oldWidgetInfo;
17646
- }
17647
-
17648
- // We do a clone here so that it's safe to mutate the input parameter
17649
- // in propUpgrades functions (which I will probably accidentally do at
17650
- // some point, and we would like to not break when that happens).
17651
- let newEditorProps = _.clone(oldWidgetInfo.options) || {};
17652
- const upgradePropsMap = widgetExports.propUpgrades || {};
17653
-
17654
- // Empty props usually mean a newly created widget by the editor,
17655
- // and are always considerered up-to-date.
17656
- // Mostly, we'd rather not run upgrade functions on props that are
17657
- // not complete.
17658
- if (_.keys(newEditorProps).length !== 0) {
17659
- // We loop through all the versions after the current version of
17660
- // the loaded widget, up to and including the latest version of the
17661
- // loaded widget, and run the upgrade function to bring our loaded
17662
- // widget's props up to that version.
17663
- // There is a little subtlety here in that we call
17664
- // upgradePropsMap[1] to upgrade *to* version 1,
17665
- // (not from version 1).
17666
- for (let nextVersion = initialVersion.major + 1; nextVersion <= latestVersion.major; nextVersion++) {
17667
- if (upgradePropsMap[String(nextVersion)]) {
17668
- newEditorProps = upgradePropsMap[String(nextVersion)](newEditorProps);
17669
- } else {
17670
- // This is a Log.error because it is unlikely to be hit in
17671
- // local testing, and a Log.error is slightly less scary in
17672
- // prod than a `throw new Error`
17673
- Log.error("No upgrade found for widget. Cannot render.", Errors.Internal, {
17674
- loggedMetadata: {
17675
- type,
17676
- fromMajorVersion: nextVersion - 1,
17677
- toMajorVersion: nextVersion
17678
- }
17679
- });
17680
- // But try to keep going anyways (yolo!)
17681
- // (Throwing an error here would just break the page
17682
- // silently anyways, so that doesn't seem much better
17683
- // than a halfhearted attempt to continue, however
17684
- // shallow...)
17685
- }
17686
- }
17687
- }
17688
-
17689
- // Minor version upgrades (eg. new optional props) don't have
17690
- // transform functions. Instead, we fill in the new props with their
17691
- // defaults.
17692
- const defaultProps = type in editors ? editors[type].defaultProps : {};
17693
- newEditorProps = _extends({}, defaultProps, newEditorProps);
17694
- let alignment = oldWidgetInfo.alignment;
17695
-
17696
- // Widgets that support multiple alignments will "lock in" the
17697
- // alignment to the alignment that would be listed first in the
17698
- // select box. If the widget only supports one alignment, the
17699
- // alignment value will likely just end up as "default".
17700
- if (alignment == null || alignment === "default") {
17701
- alignment = getSupportedAlignments(type)[0];
17702
- }
17703
- let widgetStatic = oldWidgetInfo.static;
17704
- if (widgetStatic == null) {
17705
- widgetStatic = DEFAULT_STATIC;
17706
- }
17707
- return _.extend({}, oldWidgetInfo, {
17708
- // maintain other info, like type
17709
- // After upgrading we guarantee that the version is up-to-date
17710
- version: latestVersion,
17711
- // Default graded to true (so null/undefined becomes true):
17712
- graded: oldWidgetInfo.graded != null ? oldWidgetInfo.graded : true,
17713
- alignment: alignment,
17714
- static: widgetStatic,
17715
- options: newEditorProps
17716
- });
17717
- };
17718
17668
  const getRendererPropsForWidgetInfo = (widgetInfo, strings, problemNum) => {
17719
17669
  const type = widgetInfo.type;
17720
17670
  const widgetExports = widgets[type];
@@ -17756,70 +17706,6 @@ const traverseChildWidgets$3 = (widgetInfo, traverseRenderer) => {
17756
17706
  return widgetInfo;
17757
17707
  };
17758
17708
 
17759
- /**
17760
- * Handling for the optional alignments for widgets
17761
- * See widget-container.jsx for details on how alignments are implemented.
17762
- */
17763
-
17764
- /**
17765
- * Returns the list of supported alignments for the given (string) widget
17766
- * type. This is used primarily at editing time to display the choices
17767
- * for the user.
17768
- *
17769
- * Supported alignments are given as an array of strings in the exports of
17770
- * a widget's module.
17771
- */
17772
- const getSupportedAlignments = type => {
17773
- const widgetExport = widgets[type];
17774
- // @ts-expect-error - TS2322 - Type 'string[] | readonly Alignment[]' is not assignable to type 'readonly Alignment[]'.
17775
- return widgetExport && widgetExport.supportedAlignments || DEFAULT_SUPPORTED_ALIGNMENTS;
17776
- };
17777
-
17778
- /**
17779
- * For the given (string) widget type, determine the default alignment for
17780
- * the widget. This is used at rendering time to go from "default" alignment
17781
- * to the actual alignment displayed on the screen.
17782
- *
17783
- * The default alignment is given either as a string (called
17784
- * `defaultAlignment`) or a function (called `getDefaultAlignment`) on
17785
- * the exports of a widget's module.
17786
- */
17787
- const getDefaultAlignment = type => {
17788
- const widgetExports = widgets[type];
17789
- let alignment;
17790
- if (!widgetExports) {
17791
- return DEFAULT_ALIGNMENT$1;
17792
- }
17793
- if (widgetExports.getDefaultAlignment) {
17794
- alignment = widgetExports.getDefaultAlignment();
17795
- } else {
17796
- alignment = widgetExports.defaultAlignment;
17797
- }
17798
- return alignment || DEFAULT_ALIGNMENT$1;
17799
- };
17800
- const validAlignments = ["block", "inline-block", "inline", "float-left", "float-right", "full-width"];
17801
-
17802
- /**
17803
- * Used at startup to fail fast if an alignment given by a widget is
17804
- * invalid.
17805
- */
17806
- // TODO(alex): Change this to run as a testcase (vs. being run at runtime)
17807
- // TODO(LP-10707): I think this can be completely removed because our TypeScript types
17808
- // enforce this!
17809
- const validateAlignments = () => {
17810
- _.each(widgets, function (widgetInfo) {
17811
- if (widgetInfo.defaultAlignment && !_.contains(validAlignments, widgetInfo.defaultAlignment)) {
17812
- throw new PerseusError("Widget '" + widgetInfo.displayName + "' has an invalid defaultAlignment value: " + widgetInfo.defaultAlignment, Errors.InvalidInput);
17813
- }
17814
- if (widgetInfo.supportedAlignments) {
17815
- const unknownAlignments = _.difference(widgetInfo.supportedAlignments, validAlignments);
17816
- if (unknownAlignments.length) {
17817
- throw new PerseusError("Widget '" + widgetInfo.displayName + "' has an invalid value for supportedAlignments: " + unknownAlignments.join(" "), Errors.InvalidInput);
17818
- }
17819
- }
17820
- });
17821
- };
17822
-
17823
17709
  /**
17824
17710
  * Handling for static mode for widgets that support it.
17825
17711
  */
@@ -17873,8 +17759,6 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
17873
17759
  replaceDeprecatedEditors: replaceDeprecatedEditors,
17874
17760
  getWidget: getWidget,
17875
17761
  getWidgetExport: getWidgetExport,
17876
- getWidgetValidator: getWidgetValidator,
17877
- getWidgetScorer: getWidgetScorer,
17878
17762
  getPublicWidgetOptionsFunction: getPublicWidgetOptionsFunction,
17879
17763
  getEditor: getEditor,
17880
17764
  getTransform: getTransform,
@@ -17883,265 +17767,14 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
17883
17767
  getPublicWidgets: getPublicWidgets,
17884
17768
  isAccessible: isAccessible,
17885
17769
  getAllWidgetTypes: getAllWidgetTypes,
17886
- upgradeWidgetInfoToLatestVersion: upgradeWidgetInfoToLatestVersion,
17887
17770
  getRendererPropsForWidgetInfo: getRendererPropsForWidgetInfo,
17888
17771
  traverseChildWidgets: traverseChildWidgets$3,
17889
- getSupportedAlignments: getSupportedAlignments,
17890
- getDefaultAlignment: getDefaultAlignment,
17891
- validateAlignments: validateAlignments,
17892
17772
  supportsStaticMode: supportsStaticMode,
17893
17773
  getStaticTransform: getStaticTransform,
17894
17774
  getTracking: getTracking,
17895
17775
  isLintable: isLintable
17896
17776
  });
17897
17777
 
17898
- function getUpgradedWidgetOptions(oldWidgetOptions) {
17899
- return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
17900
- if (!widgetInfo.type || !widgetInfo.alignment) {
17901
- const newValues = {};
17902
- if (!widgetInfo.type) {
17903
- // TODO: why does widget have no type?
17904
- // We don't want to derive type from widget ID
17905
- // see: LEMS-1845
17906
- newValues.type = widgetId.split(" ")[0];
17907
- }
17908
- if (!widgetInfo.alignment) {
17909
- newValues.alignment = "default";
17910
- }
17911
- widgetInfo = _extends({}, widgetInfo, newValues);
17912
- }
17913
- // TODO(LEMS-2656): remove TS suppression
17914
- return upgradeWidgetInfoToLatestVersion(widgetInfo);
17915
- });
17916
- }
17917
-
17918
- /**
17919
- * Checks the given user input to see if any answerable widgets have not been
17920
- * "filled in" (ie. if they're empty). Another way to think about this
17921
- * function is that its a check to see if we can score the provided input.
17922
- */
17923
- function emptyWidgetsFunctional(widgets,
17924
- // This is a port of old code, I'm not sure why
17925
- // we need widgetIds vs the keys of the widgets object
17926
- widgetIds, userInputMap, strings, locale) {
17927
- return widgetIds.filter(id => {
17928
- const widget = widgets[id];
17929
- if (!widget || widget.static === true) {
17930
- // Static widgets shouldn't count as empty
17931
- return false;
17932
- }
17933
- const validator = getWidgetValidator(widget.type);
17934
- const userInput = userInputMap[id];
17935
- const validationData = widget.options;
17936
- const score = validator == null ? void 0 : validator(userInput, validationData, strings, locale);
17937
- if (score) {
17938
- return scoreIsEmpty(score);
17939
- }
17940
- });
17941
- }
17942
-
17943
- // TODO: combine scorePerseusItem with scoreWidgetsFunctional
17944
- // once scorePerseusItem is the only one calling scoreWidgetsFunctional
17945
- function scorePerseusItem(perseusRenderData, userInputMap,
17946
- // TODO(LEMS-2461,LEMS-2391): these probably
17947
- // need to be removed before we move this to the server
17948
- strings, locale) {
17949
- // There seems to be a chance that PerseusRenderer.widgets might include
17950
- // widget data for widgets that are not in PerseusRenderer.content,
17951
- // so this checks that the widgets are being used before scoring them
17952
- const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
17953
- const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
17954
- return flattenScores(scores);
17955
- }
17956
- function scoreWidgetsFunctional(widgets,
17957
- // This is a port of old code, I'm not sure why
17958
- // we need widgetIds vs the keys of the widgets object
17959
- widgetIds, userInputMap, strings, locale) {
17960
- const upgradedWidgets = getUpgradedWidgetOptions(widgets);
17961
- const gradedWidgetIds = widgetIds.filter(id => {
17962
- const props = upgradedWidgets[id];
17963
- const widgetIsGraded = (props == null ? void 0 : props.graded) == null || props.graded;
17964
- const widgetIsStatic = !!(props != null && props.static);
17965
- // Ungraded widgets or widgets set to static shouldn't be graded.
17966
- return widgetIsGraded && !widgetIsStatic;
17967
- });
17968
- const widgetScores = {};
17969
- gradedWidgetIds.forEach(id => {
17970
- var _validator;
17971
- const widget = upgradedWidgets[id];
17972
- if (!widget) {
17973
- return;
17974
- }
17975
- const userInput = userInputMap[id];
17976
- const validator = getWidgetValidator(widget.type);
17977
- const scorer = getWidgetScorer(widget.type);
17978
-
17979
- // We do validation (empty checks) first and then scoring. If
17980
- // validation fails, it's result is itself a PerseusScore.
17981
- const score = (_validator = validator == null ? void 0 : validator(userInput, widget.options, strings, locale)) != null ? _validator : scorer == null ? void 0 : scorer(userInput, widget.options, strings, locale);
17982
- if (score != null) {
17983
- widgetScores[id] = score;
17984
- }
17985
- });
17986
- return widgetScores;
17987
- }
17988
-
17989
- /**
17990
- * TranslationLinter asynchronously calls Khan Academy's poentry linter.
17991
- *
17992
- * The poentry linter checks for issues that prevent content from being
17993
- * translated. This linter is KA-specific and so TranslationLinter does not do
17994
- * anything in non-KA environments.
17995
- */
17996
-
17997
- // A LintCB is called once the linter has been run.
17998
-
17999
- // msec to wait before we actually call the linter after receiving the
18000
- // last call. The timer is reset if the lint is called during the wait.
18001
- const DEBOUNCE_TIMEOUT = 1000;
18002
- class TranslationLinter {
18003
- constructor() {
18004
- // To avoid linting when nothing has changed.
18005
- this.previousContent = null;
18006
- /**
18007
- * Runs the poentry linter on the given perseus string asynchronously.
18008
- *
18009
- * It doesn't do anything unless we're in a KA environment.
18010
- *
18011
- * onLintErrorsGenerated is called even if there are no errors. It is not
18012
- * called if perseusStr is unchanged from the last call.
18013
- *
18014
- * Linting is relatively expensive -- so we debounce linting each instance
18015
- * for some amount of time.
18016
- */
18017
- this.runLinter = debounce$1((perseusStr, onLintErrorsGenerated) => {
18018
- // TODO(FEI-5003): Do we still need to check for 'KA'?
18019
- if (typeof KA === "undefined") {
18020
- return;
18021
- }
18022
- if (perseusStr === this.previousContent) {
18023
- return;
18024
- }
18025
- this.previousContent = perseusStr;
18026
- if (perseusStr === "") {
18027
- onLintErrorsGenerated([]);
18028
- return;
18029
- }
18030
- fetch("/api/internal/translations/lint_poentry?preview=1&lang=en", {
18031
- headers: {
18032
- "Content-Type": "application/json"
18033
- },
18034
- body: JSON.stringify({
18035
- // The poentry linter verifies that the translation from
18036
- // the original text to the translated text is sane. We
18037
- // want to ensure that the translation from English to
18038
- // English is sane, so msgid === mstr.
18039
- msgid: perseusStr,
18040
- // msgid is the original text
18041
- msgstr: perseusStr,
18042
- // msgstr is the translated text
18043
- format: "perseus_text",
18044
- filename: ""
18045
- }),
18046
- method: "POST"
18047
- }).then(response => {
18048
- if (response.status >= 400) {
18049
- return {
18050
- status: "error",
18051
- message: "Could not run i18n linter."
18052
- };
18053
- }
18054
- return response.json();
18055
- }, rejection => {
18056
- return {
18057
- status: "error",
18058
- message: "Could not run i18n linter."
18059
- };
18060
- }).then(json => {
18061
- if (json.status === "error") {
18062
- onLintErrorsGenerated(["Some part of this text makes it untranslatable. " + "The specific message from the i18n linter was: " + json.message.replace(/\n/g, " ")]);
18063
- } else {
18064
- onLintErrorsGenerated([]);
18065
- }
18066
- });
18067
- }, DEBOUNCE_TIMEOUT);
18068
- }
18069
- /**
18070
- * Applies an array of errors generated by linters without position
18071
- * information (TranslationLinter and the legacy getSaveWarnings()) to the
18072
- * top of a Perseus tree.
18073
- */
18074
- applyLintErrors(parsedMarkdown, translationLintErrors) {
18075
- // These lint errors do not have position data associated with
18076
- // them, so we just plop them at the top.
18077
- if (translationLintErrors.length) {
18078
- const errorText = translationLintErrors.join("\n\n");
18079
- parsedMarkdown.unshift({
18080
- content: {
18081
- type: "text",
18082
- content: ""
18083
- },
18084
- insideTable: false,
18085
- message: errorText,
18086
- ruleName: "legacy-error",
18087
- severity: Rule.Severity.ERROR,
18088
- type: "lint"
18089
- });
18090
- }
18091
- }
18092
- }
18093
-
18094
- /**
18095
- * Preprocess TeX code to convert things that MathJax doesn't know how to handle
18096
- * to things is does.
18097
- */
18098
- var preprocessTex = (texCode => texCode
18099
- // Replace uses of \begin{align}...\end{align} which KaTeX doesn't
18100
- // support (yet) with \begin{aligned}...\end{aligned} which renders
18101
- // the same is supported by KaTeX. It does the same for align*.
18102
- // TODO(kevinb) update content to use aligned instead of align.
18103
- // TODO(LEMS-1608) Remove this replacement as MathJax supports the
18104
- // "align" macro correctly (and, in fact, it is not synonymous with
18105
- // "aligned").
18106
- .replace(/\{align[*]?\}/g, "{aligned}")
18107
- // Replace non-breaking spaces with regular spaces.
18108
- .replace(/[\u00a0]/g, " "));
18109
-
18110
- const {
18111
- interactiveSizes
18112
- } = constants;
18113
-
18114
- // Note: these size cutoffs represent content-width cutoffs as specified in
18115
- // http://zpl.io/1mVmvU (broken link - we no longer use Zeplin)
18116
- // TODO(benkomalo): these values aren't used in JS outside of this file, but
18117
- // are coupled to the values in styles/styles.less - DRY it up at some point
18118
- const smMax = articleMaxWidthTableInPx;
18119
- const mdMax = articleMaxWidthInPx;
18120
- const containerSizeClass = {
18121
- SMALL: "small",
18122
- MEDIUM: "medium",
18123
- LARGE: "large",
18124
- XLARGE: "xlarge"
18125
- };
18126
- const getClassFromWidth = width => {
18127
- if (!width) {
18128
- return containerSizeClass.MEDIUM;
18129
- }
18130
- if (width <= smMax) {
18131
- return containerSizeClass.SMALL;
18132
- }
18133
- if (width <= mdMax) {
18134
- return containerSizeClass.MEDIUM;
18135
- }
18136
- return containerSizeClass.LARGE;
18137
- };
18138
- const getInteractiveBoxFromSizeClass = sizeClass => {
18139
- if (sizeClass === containerSizeClass.SMALL) {
18140
- return [interactiveSizes.defaultBoxSizeSmall, interactiveSizes.defaultBoxSizeSmall];
18141
- }
18142
- return [interactiveSizes.defaultBoxSize, interactiveSizes.defaultBoxSize];
18143
- };
18144
-
18145
17778
  class WidgetContainer extends React.Component {
18146
17779
  constructor(...args) {
18147
17780
  super(...args);
@@ -18214,7 +17847,7 @@ class WidgetContainer extends React.Component {
18214
17847
  }
18215
17848
  let alignment = this.state.widgetProps.alignment;
18216
17849
  if (alignment === "default") {
18217
- alignment = getDefaultAlignment(type);
17850
+ alignment = CoreWidgetRegistry.getDefaultAlignment(type);
18218
17851
  }
18219
17852
  className += " widget-" + alignment;
18220
17853
  const apiOptions = this.state.widgetProps.apiOptions;
@@ -18222,6 +17855,7 @@ class WidgetContainer extends React.Component {
18222
17855
  // Hack to prevent interaction with static widgets: we overlay a big
18223
17856
  // div on top of the widget and overflow: hidden the container.
18224
17857
  // Ideally widgets themselves should know how to prevent interaction.
17858
+ // UPDATE HTML5: `inert` on the underlying div would be better
18225
17859
  const isStatic = this.state.widgetProps.static || apiOptions.readOnly;
18226
17860
  const staticContainerStyles = {
18227
17861
  position: "relative",
@@ -18233,7 +17867,7 @@ class WidgetContainer extends React.Component {
18233
17867
  position: "absolute",
18234
17868
  top: 0,
18235
17869
  left: 0,
18236
- zIndex: zIndexInteractiveComponent$1
17870
+ zIndex: 3
18237
17871
  };
18238
17872
 
18239
17873
  // Some widgets may include strings of markdown that we may
@@ -19417,7 +19051,7 @@ class Renderer extends React.Component {
19417
19051
  * empty if the graph is in the starting state.
19418
19052
  */
19419
19053
  emptyWidgets() {
19420
- return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
19054
+ return emptyWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.context.locale);
19421
19055
  }
19422
19056
  /**
19423
19057
  * Returns an array of the widget `.getUserInput()` results
@@ -19477,7 +19111,7 @@ class Renderer extends React.Component {
19477
19111
  * @deprecated use scorePerseusItem
19478
19112
  */
19479
19113
  score() {
19480
- const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
19114
+ const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.context.locale);
19481
19115
  const combinedScore = flattenScores(scores);
19482
19116
  return combinedScore;
19483
19117
  }
@@ -19703,7 +19337,7 @@ function _objectWithoutPropertiesLoose(r, e) {
19703
19337
  if (null == r) return {};
19704
19338
  var t = {};
19705
19339
  for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
19706
- if (e.includes(n)) continue;
19340
+ if (-1 !== e.indexOf(n)) continue;
19707
19341
  t[n] = r[n];
19708
19342
  }
19709
19343
  return t;
@@ -20017,7 +19651,7 @@ class InputWithExamples extends React.Component {
20017
19651
  };
20018
19652
  this._getInputClassName = () => {
20019
19653
  // Otherwise, we need to add these INPUT and FOCUSED tags here.
20020
- let className = ClassNames.INPUT + " " + ClassNames.INTERACTIVE;
19654
+ let className = ClassNames.INPUT;
20021
19655
  if (this.state.focused) {
20022
19656
  className += " " + ClassNames.FOCUSED;
20023
19657
  }
@@ -20429,14 +20063,10 @@ const propTransform$3 = widgetOptions => {
20429
20063
  var InputNumber$1 = {
20430
20064
  name: "input-number",
20431
20065
  displayName: "Input number (deprecated - use numeric input instead)",
20432
- defaultAlignment: "inline-block",
20433
20066
  hidden: true,
20434
20067
  widget: InputNumber,
20435
20068
  transform: propTransform$3,
20436
20069
  isLintable: true,
20437
- // TODO(LEMS-2656): remove TS suppression
20438
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInputNumberUserInput'.
20439
- scorer: scoreInputNumber,
20440
20070
  getOneCorrectAnswerFromRubric(rubric) {
20441
20071
  if (rubric.value == null) {
20442
20072
  return;
@@ -20693,14 +20323,11 @@ const propsTransform = function propsTransform(widgetOptions) {
20693
20323
  var NumericInput$1 = {
20694
20324
  name: "numeric-input",
20695
20325
  displayName: "Numeric input",
20696
- defaultAlignment: "inline-block",
20697
20326
  accessible: true,
20698
20327
  widget: NumericInput,
20699
20328
  transform: propsTransform,
20700
20329
  isLintable: true,
20701
- // TODO(LEMS-2656): remove TS suppression
20702
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumericInputUserInput'.
20703
- scorer: scoreNumericInput,
20330
+ getPublicWidgetOptions: getNumericInputPublicWidgetOptions,
20704
20331
  // TODO(LEMS-2656): remove TS suppression
20705
20332
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusNumericInputRubric'
20706
20333
  getOneCorrectAnswerFromRubric(rubric) {
@@ -20761,23 +20388,6 @@ const getPromptJSON$p = renderProps => {
20761
20388
  };
20762
20389
  };
20763
20390
 
20764
- /**
20765
- * Several widgets don't have "right"/"wrong" scoring logic,
20766
- * so this just says to move on past those widgets
20767
- *
20768
- * TODO(LEMS-2543) widgets that use this probably shouldn't have any
20769
- * scoring logic and the thing scoring an exercise
20770
- * should just know to skip these
20771
- */
20772
- function scoreNoop(points = 0) {
20773
- return {
20774
- type: "points",
20775
- earned: points,
20776
- total: points,
20777
- message: null
20778
- };
20779
- }
20780
-
20781
20391
  /**
20782
20392
  * Utility functions for manipulating ranges of highlightable content.
20783
20393
  */
@@ -22953,8 +22563,7 @@ var Passage$1 = {
22953
22563
  transform: editorProps => {
22954
22564
  return _.pick(editorProps, "passageTitle", "passageText", "footnotes", "showLineNumbers");
22955
22565
  },
22956
- isLintable: true,
22957
- scorer: () => scoreNoop()
22566
+ isLintable: true
22958
22567
  };
22959
22568
 
22960
22569
  /**
@@ -23076,16 +22685,13 @@ var PassageRef$1 = {
23076
22685
  name: "passage-ref",
23077
22686
  displayName: "PassageRef (SAT only)",
23078
22687
  hidden: true,
23079
- defaultAlignment: "inline",
23080
22688
  widget: PassageRef,
23081
22689
  transform: widgetOptions => ({
23082
22690
  passageNumber: widgetOptions.passageNumber,
23083
22691
  referenceNumber: widgetOptions.referenceNumber,
23084
22692
  summaryText: widgetOptions.summaryText
23085
22693
  }),
23086
- version: passageRefLogic.version,
23087
- // TODO: things that aren't interactive shouldn't need scoring functions
23088
- scorer: () => scoreNoop()
22694
+ version: passageRefLogic.version
23089
22695
  };
23090
22696
 
23091
22697
  /**
@@ -23127,109 +22733,6 @@ var mediaQueries = {
23127
22733
  xlOrLarger: `@media screen and (min-width: ${pureXlMin})`
23128
22734
  };
23129
22735
 
23130
- const {
23131
- zIndexAboveScratchpad,
23132
- zIndexInteractiveComponent,
23133
- radioBorderColor,
23134
- checkedColor,
23135
- circleSize,
23136
- radioMarginWidth
23137
- } = constants;
23138
- var sharedStyles = StyleSheet.create({
23139
- perseusInteractive: {
23140
- zIndex: zIndexInteractiveComponent,
23141
- position: "relative"
23142
- },
23143
- aboveScratchpad: {
23144
- position: "relative",
23145
- zIndex: zIndexAboveScratchpad
23146
- },
23147
- blankBackground: {
23148
- // TODO(emily): Use WB colors?
23149
- backgroundColor: "#FDFDFD"
23150
- },
23151
- perseusSrOnly: {
23152
- border: 0,
23153
- clip: "rect(0,0,0,0)",
23154
- height: 1,
23155
- margin: -1,
23156
- overflow: "hidden",
23157
- padding: 0,
23158
- position: "absolute",
23159
- width: 1
23160
- },
23161
- responsiveLabel: {
23162
- // NOTE(charlie): The values used here should be kept in sync with the
23163
- // caption text sizing in Perseus' styles.less.
23164
- // TODO(charlie): Migrate the captions over to using this style.
23165
- [mediaQueries.smOrSmaller]: {
23166
- fontSize: 14,
23167
- lineHeight: 1.3
23168
- },
23169
- [mediaQueries.md]: {
23170
- fontSize: 17,
23171
- lineHeight: 1.4
23172
- },
23173
- [mediaQueries.lgOrLarger]: {
23174
- fontSize: 20,
23175
- lineHeight: 1.4
23176
- }
23177
- },
23178
- responsiveInput: {
23179
- display: "inline-block",
23180
- WebkitAppearance: "none",
23181
- appearance: "none",
23182
- "::-ms-check": {
23183
- display: "none"
23184
- },
23185
- backgroundColor: "#fff",
23186
- border: "2px solid #fff",
23187
- boxShadow: `0 0px 0px 1px ${radioBorderColor}`,
23188
- outline: "none",
23189
- boxSizing: "border-box",
23190
- flexShrink: 0,
23191
- marginBottom: 1,
23192
- marginLeft: 1,
23193
- marginRight: 1,
23194
- marginTop: 1,
23195
- height: circleSize - 2,
23196
- width: circleSize - 2
23197
- },
23198
- responsiveRadioInput: {
23199
- borderRadius: "50%",
23200
- ":checked": {
23201
- backgroundColor: checkedColor,
23202
- border: "none",
23203
- borderRadius: "50%",
23204
- boxShadow: `inset 0px 0px 0px 2px white, ` + `0 0px 0px 2px ${checkedColor}`,
23205
- marginTop: radioMarginWidth,
23206
- marginBottom: radioMarginWidth,
23207
- marginLeft: radioMarginWidth,
23208
- marginRight: radioMarginWidth,
23209
- height: circleSize - 2 * radioMarginWidth,
23210
- width: circleSize - 2 * radioMarginWidth
23211
- }
23212
- },
23213
- responsiveRadioInputActive: {
23214
- backgroundColor: "#fff",
23215
- border: "2px solid #fff",
23216
- borderRadius: "50%",
23217
- boxShadow: `0 0px 0px 2px ${checkedColor}`,
23218
- marginTop: radioMarginWidth,
23219
- marginBottom: radioMarginWidth,
23220
- marginLeft: radioMarginWidth,
23221
- marginRight: radioMarginWidth,
23222
- height: circleSize - 2 * radioMarginWidth,
23223
- width: circleSize - 2 * radioMarginWidth,
23224
- ":checked": {
23225
- backgroundColor: "#fff"
23226
- }
23227
- },
23228
- disableTextSelection: {
23229
- userSelect: "none"
23230
- }
23231
- });
23232
-
23233
22736
  /**
23234
22737
  * A small collection of useful scrolling utility functions.
23235
22738
  */
@@ -23603,8 +23106,8 @@ const styles$s = StyleSheet.create({
23603
23106
  crossOutLine: {
23604
23107
  // Center the icon within the container.
23605
23108
  position: "absolute",
23606
- top: `0px`,
23607
- left: `0px`
23109
+ top: `2px`,
23110
+ left: `2px`
23608
23111
  }
23609
23112
  });
23610
23113
 
@@ -23631,43 +23134,6 @@ function ChoiceInner(props) {
23631
23134
  }
23632
23135
  return /*#__PURE__*/React.createElement(InlineIcon, iconMinus);
23633
23136
  }
23634
-
23635
- // Handle dynamic styling of the multiple choice icon. Most
23636
- // MC icon styles are constant, but we do allow the caller
23637
- // to specify the selected color, and thus must control styles
23638
- // related to the selected state dynamically.
23639
- function getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, correct) {
23640
- let backgroundColor;
23641
- let borderColor;
23642
- let color$1;
23643
- if (!showCorrectness && pressed) {
23644
- borderColor = color.blue;
23645
- color$1 = color.blue;
23646
- backgroundColor = "transparent";
23647
- } else if (checked) {
23648
- const bg = showCorrectness && correct ? color.green : color.blue;
23649
- color$1 = white;
23650
- backgroundColor = bg;
23651
- borderColor = bg;
23652
- } else {
23653
- borderColor = color.offBlack64;
23654
- color$1 = color.offBlack64;
23655
- }
23656
-
23657
- // define shape
23658
- let borderRadius;
23659
- if (multipleSelect) {
23660
- borderRadius = 3;
23661
- } else {
23662
- borderRadius = CHOICE_ICON_SIZE;
23663
- }
23664
- return {
23665
- backgroundColor,
23666
- borderColor,
23667
- color: color$1,
23668
- borderRadius
23669
- };
23670
- }
23671
23137
  const ChoiceIcon = function ChoiceIcon(props) {
23672
23138
  const {
23673
23139
  checked,
@@ -23679,9 +23145,41 @@ const ChoiceIcon = function ChoiceIcon(props) {
23679
23145
  multipleSelect,
23680
23146
  pos,
23681
23147
  previouslyAnswered,
23148
+ // Used in "review mode"/"show rationale" to show that option was previously chosen
23682
23149
  pressed
23683
23150
  } = props;
23684
- const dynamicStyles = getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, correct);
23151
+
23152
+ // Accounts for incorrect choice still displaying as learner tries again
23153
+ const choiceIsChecked = checked || showCorrectness && !correct && previouslyAnswered;
23154
+
23155
+ // Core styling
23156
+ const choiceStyling = [styles$r.choiceBase, multipleSelect ? styles$r.multiSelectShape : styles$r.singleSelectShape, showCorrectness ? styles$r.choiceHasIcon : styles$r.choiceHasLetter, choiceIsChecked ? styles$r.choiceIsChecked : styles$r.choiceIsUnchecked];
23157
+
23158
+ // Color styling
23159
+ // Handle dynamic styling of the multiple choice icon. Most
23160
+ // MC icon styles are constant, but we do allow the caller
23161
+ // to specify the selected color, and thus must control styles
23162
+ // related to the selected state dynamically.
23163
+ let crossOutColor;
23164
+ if (showCorrectness && correct && checked) {
23165
+ choiceStyling.push(styles$r.choiceCorrect);
23166
+ crossOutColor = color.green;
23167
+ } else if (showCorrectness && !correct && (checked || previouslyAnswered)) {
23168
+ choiceStyling.push(styles$r.choiceIncorrect);
23169
+ crossOutColor = color.red;
23170
+ } else if (checked) {
23171
+ // Show filled neutral blue color (showCorrectness is false)
23172
+ choiceStyling.push(styles$r.choiceNeutral);
23173
+ crossOutColor = color.blue;
23174
+ } else if (pressed) {
23175
+ // Show outlined neutral blue color (showCorrectness is false)
23176
+ choiceStyling.push(styles$r.activeNeutral);
23177
+ crossOutColor = color.blue;
23178
+ } else {
23179
+ // choice is not checked
23180
+ choiceStyling.push(styles$r.uncheckedColors);
23181
+ crossOutColor = color.offBlack64;
23182
+ }
23685
23183
  return /*#__PURE__*/React.createElement("div", {
23686
23184
  className: css(styles$t.iconWrapper)
23687
23185
  }, /*#__PURE__*/React.createElement(FocusRing, {
@@ -23689,9 +23187,8 @@ const ChoiceIcon = function ChoiceIcon(props) {
23689
23187
  visible: focused || hovered,
23690
23188
  multipleSelect: multipleSelect
23691
23189
  }, /*#__PURE__*/React.createElement("div", {
23692
- style: dynamicStyles,
23693
23190
  "data-testid": "choice-icon__library-choice-icon",
23694
- className: css(styles$r.circle, showCorrectness && correct && styles$r.circleCorrect, showCorrectness && !correct && styles$r.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$r.circleIncorrectAnswered)
23191
+ className: css(...choiceStyling)
23695
23192
  // used in BaseRadio editMode to check
23696
23193
  // if we actually clicked on the radio icon
23697
23194
  ,
@@ -23703,7 +23200,7 @@ const ChoiceIcon = function ChoiceIcon(props) {
23703
23200
  showCorrectness: showCorrectness,
23704
23201
  correct: correct
23705
23202
  })))), crossedOut && /*#__PURE__*/React.createElement(CrossOutLine, {
23706
- color: dynamicStyles.borderColor
23203
+ color: crossOutColor
23707
23204
  }));
23708
23205
  };
23709
23206
  const styles$r = StyleSheet.create({
@@ -23715,8 +23212,7 @@ const styles$r = StyleSheet.create({
23715
23212
  alignItems: "center",
23716
23213
  justifyContent: "center"
23717
23214
  },
23718
- circle: {
23719
- // Make the circle
23215
+ choiceBase: {
23720
23216
  width: CHOICE_ICON_SIZE,
23721
23217
  height: CHOICE_ICON_SIZE,
23722
23218
  boxSizing: "border-box",
@@ -23728,30 +23224,48 @@ const styles$r = StyleSheet.create({
23728
23224
  // "bold font family" so that characters which fall back to the default
23729
23225
  // font get bolded too.
23730
23226
  fontWeight: "bold",
23731
- fontSize: 12,
23732
23227
  // Center the icon wrapper.
23733
23228
  display: "flex",
23734
23229
  alignItems: "center",
23735
- justifyContent: "center",
23736
- // HACK(emily): I don't know why adding this line height makes the text
23737
- // appear centered better than any other value, but it does. In
23738
- // particular, at large zoom levels this line height does almost
23739
- // nothing, but at the default size this shifts the letter down one
23740
- // pixel so it is much better centered.
23741
- lineHeight: "1px"
23742
- },
23743
- circleCorrect: {
23230
+ justifyContent: "center"
23231
+ },
23232
+ choiceHasLetter: {
23233
+ fontSize: 12
23234
+ },
23235
+ choiceHasIcon: {
23744
23236
  fontSize: CHOICE_ICON_SIZE
23745
23237
  },
23746
- circleIncorrect: {
23747
- fontSize: CHOICE_ICON_SIZE,
23748
- borderColor: gray68,
23749
- color: gray68
23238
+ choiceIsChecked: {
23239
+ color: color.white
23750
23240
  },
23751
- circleIncorrectAnswered: {
23241
+ choiceIsUnchecked: {
23242
+ color: color.offBlack64
23243
+ },
23244
+ choiceCorrect: {
23245
+ backgroundColor: color.green,
23246
+ borderColor: color.green
23247
+ },
23248
+ choiceIncorrect: {
23752
23249
  backgroundColor: color.red,
23753
- borderColor: color.red,
23754
- color: color.white
23250
+ borderColor: color.red
23251
+ },
23252
+ choiceNeutral: {
23253
+ backgroundColor: color.blue,
23254
+ borderColor: color.blue
23255
+ },
23256
+ activeNeutral: {
23257
+ color: color.blue,
23258
+ borderColor: color.blue,
23259
+ backgroundColor: "transparent"
23260
+ },
23261
+ multiSelectShape: {
23262
+ borderRadius: 3
23263
+ },
23264
+ singleSelectShape: {
23265
+ borderRadius: CHOICE_ICON_SIZE
23266
+ },
23267
+ uncheckedColors: {
23268
+ borderColor: color.offBlack64
23755
23269
  }
23756
23270
  });
23757
23271
 
@@ -24288,7 +23802,7 @@ const BaseRadio = function BaseRadio({
24288
23802
  const aphroditeClassName = checked => {
24289
23803
  // Whether or not to show correctness borders
24290
23804
  // for this choice and the next choice.
24291
- return css(sharedStyles.aboveScratchpad, styles$o.item, styles$o.responsiveItem, checked && styles$o.selectedItem, checked && choice.highlighted && styles$o.aboveBackdrop, checked && choice.highlighted && apiOptions.isMobile && styles$o.aboveBackdropMobile, nextChoiceHighlighted && apiOptions.isMobile && styles$o.nextHighlighted);
23805
+ return css(styles$o.item, styles$o.responsiveItem, checked && styles$o.selectedItem, checked && choice.highlighted && styles$o.aboveBackdrop, checked && choice.highlighted && apiOptions.isMobile && styles$o.aboveBackdropMobile, nextChoiceHighlighted && apiOptions.isMobile && styles$o.nextHighlighted);
24292
23806
  };
24293
23807
 
24294
23808
  // HACK(abdulrahman): Preloads the selection-state
@@ -24878,17 +24392,105 @@ var Radio = {
24878
24392
  version: radioLogic.version,
24879
24393
  propUpgrades: radioLogic.widgetOptionsUpgrades,
24880
24394
  isLintable: true,
24881
- // TODO(LEMS-2656): remove TS suppression
24882
- // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput
24883
- scorer: scoreRadio,
24884
- // TODO(LEMS-2656): remove TS suppression
24885
- // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput
24886
- validator: validateRadio
24395
+ getPublicWidgetOptions: getRadioPublicWidgetOptions
24887
24396
  };
24888
24397
 
24889
24398
  /* eslint-disable import/no-named-as-default */
24890
24399
  var basicWidgets = [Radio, InputNumber$1, NumericInput$1, Expression$1];
24891
24400
 
24401
+ const {
24402
+ radioBorderColor,
24403
+ checkedColor,
24404
+ circleSize,
24405
+ radioMarginWidth
24406
+ } = constants;
24407
+ var sharedStyles = StyleSheet.create({
24408
+ blankBackground: {
24409
+ // TODO(emily): Use WB colors?
24410
+ backgroundColor: "#FDFDFD"
24411
+ },
24412
+ perseusSrOnly: {
24413
+ border: 0,
24414
+ clip: "rect(0,0,0,0)",
24415
+ height: 1,
24416
+ margin: -1,
24417
+ overflow: "hidden",
24418
+ padding: 0,
24419
+ position: "absolute",
24420
+ width: 1
24421
+ },
24422
+ responsiveLabel: {
24423
+ // NOTE(charlie): The values used here should be kept in sync with the
24424
+ // caption text sizing in Perseus' styles.less.
24425
+ // TODO(charlie): Migrate the captions over to using this style.
24426
+ [mediaQueries.smOrSmaller]: {
24427
+ fontSize: 14,
24428
+ lineHeight: 1.3
24429
+ },
24430
+ [mediaQueries.md]: {
24431
+ fontSize: 17,
24432
+ lineHeight: 1.4
24433
+ },
24434
+ [mediaQueries.lgOrLarger]: {
24435
+ fontSize: 20,
24436
+ lineHeight: 1.4
24437
+ }
24438
+ },
24439
+ responsiveInput: {
24440
+ display: "inline-block",
24441
+ WebkitAppearance: "none",
24442
+ appearance: "none",
24443
+ "::-ms-check": {
24444
+ display: "none"
24445
+ },
24446
+ backgroundColor: "#fff",
24447
+ border: "2px solid #fff",
24448
+ boxShadow: `0 0px 0px 1px ${radioBorderColor}`,
24449
+ outline: "none",
24450
+ boxSizing: "border-box",
24451
+ flexShrink: 0,
24452
+ marginBottom: 1,
24453
+ marginLeft: 1,
24454
+ marginRight: 1,
24455
+ marginTop: 1,
24456
+ height: circleSize - 2,
24457
+ width: circleSize - 2
24458
+ },
24459
+ responsiveRadioInput: {
24460
+ borderRadius: "50%",
24461
+ ":checked": {
24462
+ backgroundColor: checkedColor,
24463
+ border: "none",
24464
+ borderRadius: "50%",
24465
+ boxShadow: `inset 0px 0px 0px 2px white, ` + `0 0px 0px 2px ${checkedColor}`,
24466
+ marginTop: radioMarginWidth,
24467
+ marginBottom: radioMarginWidth,
24468
+ marginLeft: radioMarginWidth,
24469
+ marginRight: radioMarginWidth,
24470
+ height: circleSize - 2 * radioMarginWidth,
24471
+ width: circleSize - 2 * radioMarginWidth
24472
+ }
24473
+ },
24474
+ responsiveRadioInputActive: {
24475
+ backgroundColor: "#fff",
24476
+ border: "2px solid #fff",
24477
+ borderRadius: "50%",
24478
+ boxShadow: `0 0px 0px 2px ${checkedColor}`,
24479
+ marginTop: radioMarginWidth,
24480
+ marginBottom: radioMarginWidth,
24481
+ marginLeft: radioMarginWidth,
24482
+ marginRight: radioMarginWidth,
24483
+ height: circleSize - 2 * radioMarginWidth,
24484
+ width: circleSize - 2 * radioMarginWidth,
24485
+ ":checked": {
24486
+ backgroundColor: "#fff"
24487
+ }
24488
+ },
24489
+ disableTextSelection: {
24490
+ userSelect: "none"
24491
+ }
24492
+ });
24493
+
24892
24494
  const getPromptJSON$n = (renderProps, userInput) => {
24893
24495
  return {
24894
24496
  type: "categorizer",
@@ -24979,7 +24581,6 @@ class Categorizer extends React.Component {
24979
24581
  className: "category " + css(styles$n.cell),
24980
24582
  key: catNum
24981
24583
  }, /*#__PURE__*/React.createElement("div", {
24982
- className: ClassNames.INTERACTIVE,
24983
24584
  role: "button",
24984
24585
  "aria-label": catName,
24985
24586
  onClick: () => this.onChange(itemNum, catNum)
@@ -25078,12 +24679,6 @@ var Categorizer$1 = {
25078
24679
  return _.pick(editorProps, "items", "categories", "values", "randomizeItems");
25079
24680
  },
25080
24681
  isLintable: true,
25081
- // TODO(LEMS-2656): remove TS suppression
25082
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
25083
- scorer: scoreCategorizer,
25084
- // TODO(LEMS-2656): remove TS suppression
25085
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
25086
- validator: validateCategorizer,
25087
24682
  getPublicWidgetOptions: getCategorizerPublicWidgetOptions
25088
24683
  };
25089
24684
 
@@ -25289,12 +24884,9 @@ const styles$m = StyleSheet.create({
25289
24884
  var CSProgram$1 = {
25290
24885
  name: "cs-program",
25291
24886
  displayName: "CS Program",
25292
- supportedAlignments: ["block", "full-width"],
25293
24887
  widget: CSProgram,
25294
24888
  hidden: true,
25295
- // TODO(LEMS-2656): remove TS suppression
25296
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
25297
- scorer: scoreCSProgram
24889
+ getPublicWidgetOptions: getCSProgramPublicWidgetOptions
25298
24890
  };
25299
24891
 
25300
24892
  const getPromptJSON$l = renderProps => {
@@ -25367,11 +24959,8 @@ var Definition$1 = {
25367
24959
  name: "definition",
25368
24960
  displayName: "Definition",
25369
24961
  accessible: true,
25370
- defaultAlignment: "inline",
25371
24962
  widget: Definition,
25372
- transform: x => x,
25373
- // TODO: things that aren't interactive shouldn't need scoring functions
25374
- scorer: () => scoreNoop()
24963
+ transform: x => x
25375
24964
  };
25376
24965
 
25377
24966
  // There are no required props, but this component can receive props from any
@@ -25401,9 +24990,7 @@ var DeprecatedStandin$1 = {
25401
24990
  name: "deprecated-standin",
25402
24991
  displayName: "Deprecated Standin",
25403
24992
  widget: DeprecatedStandin,
25404
- hidden: true,
25405
- // TODO: things that aren't interactive shouldn't need scoring functions
25406
- scorer: () => scoreNoop(1)
24993
+ hidden: true
25407
24994
  };
25408
24995
 
25409
24996
  const getPromptJSON$k = (renderProps, userInput) => {
@@ -25515,16 +25102,10 @@ const optionsTransform = widgetOptions => {
25515
25102
  var Dropdown$1 = {
25516
25103
  name: "dropdown",
25517
25104
  displayName: "Drop down",
25518
- defaultAlignment: "inline-block",
25519
25105
  accessible: true,
25520
25106
  widget: Dropdown,
25521
25107
  transform: optionsTransform,
25522
- // TODO(LEMS-2656): remove TS suppression
25523
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'.
25524
- scorer: scoreDropdown,
25525
- // TODO(LEMS-2656): remove TS suppression
25526
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'.
25527
- validator: validateDropdown
25108
+ getPublicWidgetOptions: getDropdownPublicWidgetOptions
25528
25109
  };
25529
25110
 
25530
25111
  const getPromptJSON$j = renderProps => {
@@ -25679,12 +25260,9 @@ var Explanation$1 = {
25679
25260
  name: "explanation",
25680
25261
  displayName: "Explanation",
25681
25262
  accessible: true,
25682
- defaultAlignment: "inline",
25683
25263
  widget: Explanation,
25684
25264
  transform: _.identity,
25685
- isLintable: true,
25686
- // TODO: things that aren't interactive shouldn't need scoring functions
25687
- scorer: () => scoreNoop()
25265
+ isLintable: true
25688
25266
  };
25689
25267
 
25690
25268
  const getPromptJSON$i = (title, rendererJSON, hintRendererJSON) => {
@@ -26784,7 +26362,7 @@ class FunctionGrapher extends React.Component {
26784
26362
  boxSizing: "initial"
26785
26363
  }
26786
26364
  }, /*#__PURE__*/React.createElement("div", {
26787
- className: "graphie-container above-scratchpad",
26365
+ className: "graphie-container blank-background",
26788
26366
  style: {
26789
26367
  width: box[0],
26790
26368
  height: box[1]
@@ -26923,8 +26501,7 @@ class Grapher extends React.Component {
26923
26501
  const coords = this.props.plot.coords;
26924
26502
  const asymptote = this.props.plot.asymptote;
26925
26503
  const typeSelector = /*#__PURE__*/React.createElement("div", {
26926
- style: typeSelectorStyle,
26927
- className: "above-scratchpad"
26504
+ style: typeSelectorStyle
26928
26505
  }, /*#__PURE__*/React.createElement(ButtonGroup, {
26929
26506
  value: type,
26930
26507
  allowEmpty: true,
@@ -27003,9 +26580,7 @@ var Grapher$1 = {
27003
26580
  widget: Grapher,
27004
26581
  transform: propTransform$2,
27005
26582
  staticTransform: staticTransform$4,
27006
- // TODO(LEMS-2656): remove TS suppression
27007
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGrapherUserInput'.
27008
- scorer: scoreGrapher
26583
+ getPublicWidgetOptions: getGrapherPublicWidgetOptions
27009
26584
  };
27010
26585
 
27011
26586
  const getPromptJSON$f = rendererJSON => {
@@ -27021,24 +26596,6 @@ const getPromptJSON$f = rendererJSON => {
27021
26596
  });
27022
26597
  };
27023
26598
 
27024
- // The `group` widget is basically a widget hosting a full Perseus system in
27025
- // it. As such, scoring a group means scoring all widgets it contains.
27026
- function scoreGroup(userInput, rubric, strings, locale) {
27027
- const scores = scoreWidgetsFunctional(rubric.widgets, Object.keys(rubric.widgets), userInput, strings, locale);
27028
- return flattenScores(scores);
27029
- }
27030
-
27031
- function validateGroup(userInput, validationData, strings, locale) {
27032
- const emptyWidgets = emptyWidgetsFunctional(validationData.widgets, Object.keys(validationData.widgets), userInput, strings, locale);
27033
- if (emptyWidgets.length === 0) {
27034
- return null;
27035
- }
27036
- return {
27037
- type: "invalid",
27038
- message: null
27039
- };
27040
- }
27041
-
27042
26599
  // exports has no 'transform'
27043
26600
 
27044
26601
  class Group extends React.Component {
@@ -27193,12 +26750,6 @@ var Group$1 = {
27193
26750
  displayName: "Group (SAT only)",
27194
26751
  widget: Group,
27195
26752
  traverseChildWidgets: traverseChildWidgets,
27196
- // TODO(LEMS-2656): remove TS suppression
27197
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGroupUserInput'.
27198
- scorer: scoreGroup,
27199
- // TODO(LEMS-2656): remove TS suppression
27200
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGroupUserInput'.
27201
- validator: validateGroup,
27202
26753
  hidden: true,
27203
26754
  isLintable: true
27204
26755
  };
@@ -27335,9 +26886,7 @@ var Iframe$1 = {
27335
26886
  widget: Iframe,
27336
26887
  // Let's not expose it to all content creators yet
27337
26888
  hidden: true,
27338
- // TODO(LEMS-2656): remove TS suppression
27339
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusIframeUserInput'.
27340
- scorer: scoreIframe
26889
+ getPublicWidgetOptions: getIFramePublicWidgetOptions
27341
26890
  };
27342
26891
 
27343
26892
  const getPromptJSON$d = renderProps => {
@@ -27359,8 +26908,6 @@ const defaultBackgroundImage$2 = {
27359
26908
  width: 0,
27360
26909
  height: 0
27361
26910
  };
27362
- const editorAlignments = ["block", "full-width"];
27363
- const DEFAULT_ALIGNMENT = "block";
27364
26911
 
27365
26912
  // there is no transform as part of exports
27366
26913
 
@@ -27509,7 +27056,7 @@ class ImageWidget extends React.Component {
27509
27056
  }
27510
27057
  ImageWidget.contextType = PerseusI18nContext;
27511
27058
  ImageWidget.defaultProps = {
27512
- alignment: DEFAULT_ALIGNMENT,
27059
+ alignment: "block",
27513
27060
  title: "",
27514
27061
  range: [defaultRange, defaultRange],
27515
27062
  box: [defaultBoxSize, defaultBoxSize],
@@ -27527,13 +27074,9 @@ var Image$1 = {
27527
27074
  const bgImage = widgetOptions.backgroundImage;
27528
27075
  return !(bgImage && bgImage.url && !widgetOptions.alt);
27529
27076
  },
27530
- defaultAlignment: DEFAULT_ALIGNMENT,
27531
- supportedAlignments: editorAlignments,
27532
27077
  displayName: "Image",
27533
27078
  widget: ImageWidget,
27534
- isLintable: true,
27535
- // TODO: things that aren't interactive shouldn't need scoring functions
27536
- scorer: () => scoreNoop()
27079
+ isLintable: true
27537
27080
  };
27538
27081
 
27539
27082
  const getPromptJSON$c = () => {
@@ -28131,9 +27674,7 @@ var Interactive = {
28131
27674
  displayName: "Interaction",
28132
27675
  widget: Interaction,
28133
27676
  transform: _.identity,
28134
- hidden: true,
28135
- // TODO: things that aren't interactive shouldn't need scoring functions
28136
- scorer: () => scoreNoop()
27677
+ hidden: true
28137
27678
  };
28138
27679
 
28139
27680
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -28402,7 +27943,7 @@ class Graph extends React.Component {
28402
27943
  image = null;
28403
27944
  }
28404
27945
  return /*#__PURE__*/React.createElement("div", {
28405
- className: "graphie-container above-scratchpad",
27946
+ className: "graphie-container blank-background",
28406
27947
  style: {
28407
27948
  width: this.props.box[0],
28408
27949
  height: this.props.box[1]
@@ -29037,7 +28578,8 @@ const tickLabelSize = 14;
29037
28578
  const YGridTick = ({
29038
28579
  y,
29039
28580
  range,
29040
- tickStep
28581
+ tickStep,
28582
+ showPi
29041
28583
  }) => {
29042
28584
  // If the graph requires out-of-bounds labels, we want to make sure to set the
29043
28585
  // coordinates to the edge of the visible range of the graph. Otherwise,
@@ -29073,6 +28615,7 @@ const YGridTick = ({
29073
28615
  // If the graph displays both the y and x axis lines within the graph, we want
29074
28616
  // to hide the label at -1 on the y-axis to prevent overlap with the x-axis label
29075
28617
  const showLabel = shouldShowLabel(y, range, tickStep);
28618
+ const yLabel = showPi ? divideByAndShowPi(y) : y.toString();
29076
28619
  return /*#__PURE__*/React.createElement("g", {
29077
28620
  className: "tick",
29078
28621
  "aria-hidden": true
@@ -29090,11 +28633,12 @@ const YGridTick = ({
29090
28633
  textAnchor: "end",
29091
28634
  x: xPositionText,
29092
28635
  y: yPositionText
29093
- }, y.toString()));
28636
+ }, yLabel));
29094
28637
  };
29095
28638
  const XGridTick = ({
29096
28639
  x,
29097
- range
28640
+ range,
28641
+ showPi
29098
28642
  }) => {
29099
28643
  // If the graph requires out-of-bounds labels, we want to make sure to set the
29100
28644
  // coordinates to the edge of the visible range of the graph. Otherwise,
@@ -29133,6 +28677,7 @@ const XGridTick = ({
29133
28677
  // Apply the adjustments to the x and y positions for the text
29134
28678
  const xPositionText = xPosition + xAdjustment;
29135
28679
  const yPositionText = yPosition + yAdjustment;
28680
+ const xLabel = showPi ? divideByAndShowPi(x) : x.toString();
29136
28681
  return /*#__PURE__*/React.createElement("g", {
29137
28682
  className: "tick",
29138
28683
  "aria-hidden": true
@@ -29150,7 +28695,7 @@ const XGridTick = ({
29150
28695
  textAnchor: "middle",
29151
28696
  x: xPositionText,
29152
28697
  y: yPositionText
29153
- }, x.toString()));
28698
+ }, xLabel));
29154
28699
  };
29155
28700
 
29156
28701
  // Determines whether to show the label for the given tick
@@ -29197,6 +28742,22 @@ const countSignificantDecimals = number => {
29197
28742
  }
29198
28743
  return numStr.split(".")[1].length;
29199
28744
  };
28745
+
28746
+ // Show the given value as a multiple of pi (already assumed to be
28747
+ // a multiple of pi). Exported for testing
28748
+ function divideByAndShowPi(value) {
28749
+ const dividedValue = value / Math.PI;
28750
+ switch (dividedValue) {
28751
+ case 1:
28752
+ return "π";
28753
+ case -1:
28754
+ return "-π";
28755
+ case 0:
28756
+ return "0";
28757
+ default:
28758
+ return dividedValue + "π";
28759
+ }
28760
+ }
29200
28761
  const AxisTicks = () => {
29201
28762
  const {
29202
28763
  tickStep,
@@ -29219,6 +28780,10 @@ const AxisTicks = () => {
29219
28780
  key: `y-grid-tick-${y}`,
29220
28781
  range: range,
29221
28782
  tickStep: tickStep[Y]
28783
+ // Show the tick labels as multiples of pi
28784
+ // if the tick step is a multiple of pi.
28785
+ ,
28786
+ showPi: tickStep[Y] % Math.PI === 0
29222
28787
  });
29223
28788
  })), /*#__PURE__*/React.createElement("g", {
29224
28789
  className: "x-axis-ticks"
@@ -29227,6 +28792,10 @@ const AxisTicks = () => {
29227
28792
  x: x,
29228
28793
  key: `x-grid-tick-${x}`,
29229
28794
  range: range
28795
+ // Show the tick labels as multiples of pi
28796
+ // if the tick step is a multiple of pi.
28797
+ ,
28798
+ showPi: tickStep[X] % Math.PI === 0
29230
28799
  });
29231
28800
  })));
29232
28801
  };
@@ -29534,6 +29103,15 @@ const styles$d = StyleSheet.create({
29534
29103
  }
29535
29104
  });
29536
29105
 
29106
+ function isPiMultiple(value) {
29107
+ // Early return for integers that clearly aren't multiples
29108
+ // of pi to save some computation.
29109
+ if (Number.isInteger(value)) {
29110
+ return false;
29111
+ }
29112
+ return value % Math.PI === 0 || value % (Math.PI / 2) === 0 || value % (Math.PI / 3) === 0 || value % (Math.PI / 4) === 0 || value % (Math.PI / 6) === 0;
29113
+ }
29114
+
29537
29115
  const _excluded$1 = ["onFormatChange", "checkValidity", "useArrowKeys"];
29538
29116
  const {
29539
29117
  firstNumericalParse,
@@ -29671,6 +29249,19 @@ class NumberInput extends React.Component {
29671
29249
  this._getInput().value = toNumericString(val, format);
29672
29250
  };
29673
29251
  }
29252
+ componentDidMount() {
29253
+ // If the value is a multiple of pi, but it is not in the pi format,
29254
+ // then convert it to the pi format and show it as a multiple of pi.
29255
+ const value = this.getValue();
29256
+ if (this.props.allowPiTruncation && value !== null && value !== 0) {
29257
+ if (this.state.format !== "pi" && isPiMultiple(value)) {
29258
+ this._setValue(value / Math.PI, "pi");
29259
+ this.setState({
29260
+ format: "pi"
29261
+ });
29262
+ }
29263
+ }
29264
+ }
29674
29265
  componentDidUpdate(prevProps) {
29675
29266
  if (!number.equal(this.getValue(), this.props.value)) {
29676
29267
  this._setValue(this.props.value, this.state.format);
@@ -29713,7 +29304,8 @@ NumberInput.propTypes = {
29713
29304
  onFormatChange: PropTypes.func,
29714
29305
  checkValidity: PropTypes.func,
29715
29306
  size: PropTypes.oneOf(["mini", "small", "normal"]),
29716
- label: PropTypes.oneOf(["put your labels outside your inputs!"])
29307
+ label: PropTypes.oneOf(["put your labels outside your inputs!"]),
29308
+ allowPiTruncation: PropTypes.bool
29717
29309
  };
29718
29310
  NumberInput.defaultProps = {
29719
29311
  value: null,
@@ -29752,14 +29344,16 @@ class RangeInput extends React.Component {
29752
29344
  // eslint-disable-next-line react/jsx-no-bind
29753
29345
  ,
29754
29346
  onChange: this.onChange.bind(this, 0),
29755
- placeholder: this.props.placeholder[0]
29347
+ placeholder: this.props.placeholder[0],
29348
+ allowPiTruncation: this.props.allowPiTruncation
29756
29349
  })), /*#__PURE__*/React.createElement(NumberInput, _extends({}, this.props, {
29757
29350
  value: value[1],
29758
29351
  checkValidity: val => checkValidity([value[0], val])
29759
29352
  // eslint-disable-next-line react/jsx-no-bind
29760
29353
  ,
29761
29354
  onChange: this.onChange.bind(this, 1),
29762
- placeholder: this.props.placeholder[1]
29355
+ placeholder: this.props.placeholder[1],
29356
+ allowPiTruncation: this.props.allowPiTruncation
29763
29357
  })));
29764
29358
  }
29765
29359
  }
@@ -29767,7 +29361,8 @@ RangeInput.propTypes = {
29767
29361
  value: PropTypes.array.isRequired,
29768
29362
  onChange: PropTypes.func.isRequired,
29769
29363
  placeholder: PropTypes.array,
29770
- checkValidity: PropTypes.func
29364
+ checkValidity: PropTypes.func,
29365
+ allowPiTruncation: PropTypes.bool
29771
29366
  };
29772
29367
  RangeInput.defaultProps = {
29773
29368
  placeholder: [null, null]
@@ -29998,10 +29593,10 @@ function Vector(props) {
29998
29593
  }));
29999
29594
  }
30000
29595
 
30001
- function srFormatNumber(a, locale) {
29596
+ function srFormatNumber(a, locale, maximumFractionDigits) {
30002
29597
  // adding zero here converts negative zero to positive zero.
30003
29598
  return (0 + a).toLocaleString(locale, {
30004
- maximumFractionDigits: 3,
29599
+ maximumFractionDigits: maximumFractionDigits != null ? maximumFractionDigits : 3,
30005
29600
  useGrouping: false // no thousands separators
30006
29601
  });
30007
29602
  }
@@ -30177,6 +29772,95 @@ a, b, c) {
30177
29772
  return [x1, x2];
30178
29773
  }
30179
29774
 
29775
+ /**
29776
+ * Given a list of points, return the angle made by the point at index i
29777
+ * with the points at indices i - 1 and i + 1.
29778
+ */
29779
+ function getAngleFromPoints(points, i) {
29780
+ // Make sure the index is valid.
29781
+ if (i < 0 || i >= points.length || !Number.isInteger(i)) {
29782
+ return null;
29783
+ }
29784
+
29785
+ // If there are fewer than 3 points, there is no angle.
29786
+ if (points.length < 3) {
29787
+ return null;
29788
+ }
29789
+
29790
+ // Get the points at the current index and the two adjacent points.
29791
+ const point = points.at(i);
29792
+ const pt1 = points.at(i - 1);
29793
+ const pt2 = points[(i + 1) % points.length];
29794
+ if (!point || !pt1 || !pt2) {
29795
+ return null;
29796
+ }
29797
+
29798
+ // Get the lengths of adjacent sides, making a triangle
29799
+ // we can use to calculate the angle.
29800
+ const a = vec.dist(point, pt1);
29801
+ const b = vec.dist(point, pt2);
29802
+ const c = vec.dist(pt1, pt2);
29803
+
29804
+ // Law of cosines
29805
+ const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
29806
+ return angle;
29807
+ }
29808
+ function getSideLengthsFromPoints(points, i, isPolygonOpen) {
29809
+ if (i < 0 || i >= points.length || !Number.isInteger(i)) {
29810
+ return [];
29811
+ }
29812
+ if (points.length < 2) {
29813
+ return [];
29814
+ }
29815
+ const returnArray = [];
29816
+ const point = points[i];
29817
+ // If this point is the first point, then side 1 is the
29818
+ // last point in the list.
29819
+ const point1Index = i === 0 ? points.length - 1 : i - 1;
29820
+ const point1 = points[point1Index];
29821
+ // Make sure the previous point is not the same
29822
+ // as the current point.
29823
+ const side1 = i !== point1Index ? vec.dist(point, point1) : null;
29824
+ // The first side of the first point does not exist if
29825
+ // the polygon is open.
29826
+ if (side1 && !(isPolygonOpen && i === 0)) {
29827
+ returnArray.push({
29828
+ pointIndex: point1Index,
29829
+ sideLength: side1
29830
+ });
29831
+ }
29832
+
29833
+ // See if there is a side 2.
29834
+ const point2Index = (i + 1) % points.length;
29835
+ const point2 = points[point2Index];
29836
+ // Make sure that the next point is not the same as the
29837
+ // current point, and don't repeat the first point.
29838
+ const side2 = i !== point2Index && point2Index !== point1Index ? vec.dist(point, point2) : null;
29839
+ // The second side of the last point does not exist if
29840
+ // the polygon is open.
29841
+ if (side2 && !(isPolygonOpen && i === points.length - 1)) {
29842
+ returnArray.push({
29843
+ pointIndex: point2Index,
29844
+ sideLength: side2
29845
+ });
29846
+ }
29847
+ return returnArray;
29848
+ }
29849
+
29850
+ /**
29851
+ * Determine whether to determine the string with the
29852
+ * exact side length or the approximate side length.
29853
+ */
29854
+ function getPolygonSideString(sideLength, pointIndex, strings, locale) {
29855
+ return Number.isInteger(sideLength) ? strings.srPolygonSideLength({
29856
+ pointNum: pointIndex + 1,
29857
+ length: `${sideLength}`
29858
+ }) : strings.srPolygonSideLengthApprox({
29859
+ pointNum: pointIndex + 1,
29860
+ length: srFormatNumber(sideLength, locale, 1)
29861
+ });
29862
+ }
29863
+
30180
29864
  const {
30181
29865
  calculateAngleInDegrees: calculateAngleInDegrees$2
30182
29866
  } = angles;
@@ -30622,7 +30306,7 @@ const SvgDefs = () => /*#__PURE__*/React__default.createElement("defs", null, /*
30622
30306
  })));
30623
30307
 
30624
30308
  const {
30625
- clockwise: clockwise$1
30309
+ clockwise
30626
30310
  } = geometry;
30627
30311
  const {
30628
30312
  getAngleFromVertex: getAngleFromVertex$1
@@ -30636,7 +30320,7 @@ const PolygonAngle = ({
30636
30320
  snapTo
30637
30321
  }) => {
30638
30322
  const [centerX, centerY] = centerPoint;
30639
- const areClockwise = clockwise$1([centerPoint, ...endPoints]);
30323
+ const areClockwise = clockwise([centerPoint, ...endPoints]);
30640
30324
  const [[startX, startY], [endX, endY]] = areClockwise ? endPoints : endPoints.reverse();
30641
30325
  const radius = 0.3;
30642
30326
  const a = vec.dist(centerPoint, endPoints[0]);
@@ -30708,7 +30392,7 @@ const Angle = ({
30708
30392
  range
30709
30393
  }) => {
30710
30394
  // Check if the points are clockwise or not, depending on whether we allow reflex angles
30711
- const areClockwise = clockwise$1([...coords, vertex]);
30395
+ const areClockwise = clockwise([...coords, vertex]);
30712
30396
  const shouldReverseCoords = areClockwise && !allowReflexAngles;
30713
30397
 
30714
30398
  // Reverse the coordinates accordingly to ensure the angle is calculated correctly
@@ -31641,9 +31325,6 @@ function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
31641
31325
  };
31642
31326
  }
31643
31327
 
31644
- const {
31645
- clockwise
31646
- } = geometry;
31647
31328
  function getGradableGraph(state, initialGraph) {
31648
31329
  if (!state.hasBeenInteractedWith) {
31649
31330
  return _extends({}, initialGraph);
@@ -31707,16 +31388,8 @@ function getGradableGraph(state, initialGraph) {
31707
31388
  });
31708
31389
  }
31709
31390
  if (state.type === "angle" && initialGraph.type === "angle") {
31710
- // We're going to reverse the coords for scoring if the angle is clockwise and
31711
- // we don't allow reflex angles. This is because the angle is largely defined
31712
- // by the order of the points in the coords array, and we want to maintain
31713
- // the same angle scoring with the legacy graph (which had a bug).
31714
- // (LEMS-2190): When we remove the legacy graph, move this logic to the scoring function.
31715
- const areClockwise = clockwise([state.coords[0], state.coords[2], state.coords[1]]);
31716
- const shouldReverseCoords = areClockwise && !state.allowReflexAngles;
31717
- const coords = shouldReverseCoords ? state.coords.slice().reverse() : state.coords;
31718
31391
  return _extends({}, initialGraph, {
31719
- coords,
31392
+ coords: state.coords,
31720
31393
  allowReflexAngles: state.allowReflexAngles
31721
31394
  });
31722
31395
  }
@@ -32355,17 +32028,20 @@ function describePointGraph(state, i18n) {
32355
32028
  y: srFormatNumber(y, locale)
32356
32029
  }));
32357
32030
  return strings.srInteractiveElements({
32358
- elements: pointDescriptions.join(", ")
32031
+ elements: pointDescriptions.join(" ")
32359
32032
  });
32360
32033
  }
32361
32034
 
32362
- function renderPolygonGraph(state, dispatch) {
32035
+ const {
32036
+ convertRadiansToDegrees
32037
+ } = angles;
32038
+ function renderPolygonGraph(state, dispatch, i18n, markings) {
32363
32039
  return {
32364
32040
  graph: /*#__PURE__*/React.createElement(PolygonGraph, {
32365
32041
  graphState: state,
32366
32042
  dispatch: dispatch
32367
32043
  }),
32368
- interactiveElementsDescription: null
32044
+ interactiveElementsDescription: getPolygonGraphDescription(state, i18n, markings)
32369
32045
  };
32370
32046
  }
32371
32047
  const PolygonGraph = props => {
@@ -32450,7 +32126,7 @@ const PolygonGraph = props => {
32450
32126
  focusVisible,
32451
32127
  setFocusVisible
32452
32128
  });
32453
- return numSides === "unlimited" ? UnlimitedPolygonGraph(statefulProps) : LimitedPolygonGraph(statefulProps);
32129
+ return numSides === "unlimited" ? /*#__PURE__*/React.createElement(UnlimitedPolygonGraph, statefulProps) : /*#__PURE__*/React.createElement(LimitedPolygonGraph, statefulProps);
32454
32130
  };
32455
32131
  const LimitedPolygonGraph = statefulProps => {
32456
32132
  const {
@@ -32475,8 +32151,39 @@ const LimitedPolygonGraph = statefulProps => {
32475
32151
  const {
32476
32152
  disableKeyboardInteraction
32477
32153
  } = graphConfig;
32154
+ const {
32155
+ strings,
32156
+ locale
32157
+ } = usePerseusI18n();
32158
+ const id = React.useId();
32159
+ const pointsOffArray = Array(points.length).fill("off");
32160
+ // When moving an element, set its aria-live to "polite" and the others
32161
+ // to "off". Otherwise, other connected elements that move at the same
32162
+ // time might override the currently focused element's aria live.
32163
+ const [ariaLives, setAriaLives] = React.useState([
32164
+ // First one represents the aria-live value for the polygon itself.
32165
+ "off",
32166
+ // The rest represent the points.
32167
+ ...pointsOffArray]);
32478
32168
  const lines = getLines(points);
32479
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Polygon, {
32169
+ const polygonPointsNumId = id + "-points-num";
32170
+ const polygonPointsId = id + "-points";
32171
+
32172
+ // Aria label srings
32173
+ const {
32174
+ srPolygonGraph,
32175
+ srPolygonGraphPointsNum,
32176
+ srPolygonGraphPoints,
32177
+ srPolygonElementsNum
32178
+ } = describePolygonGraph(statefulProps.graphState, {
32179
+ strings,
32180
+ locale
32181
+ }, statefulProps.graphConfig.markings);
32182
+ return /*#__PURE__*/React.createElement("g", {
32183
+ // Outer graph minimal description
32184
+ "aria-label": srPolygonGraph,
32185
+ "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
32186
+ }, /*#__PURE__*/React.createElement(Polygon, {
32480
32187
  points: [...points],
32481
32188
  color: "var(--movable-line-stroke-color)",
32482
32189
  svgPolygonProps: {
@@ -32501,13 +32208,17 @@ const LimitedPolygonGraph = statefulProps => {
32501
32208
  snapTo: snapTo
32502
32209
  });
32503
32210
  }), showSides && lines.map(([start, end], i) => {
32211
+ // Use x and y to find the position of the label
32504
32212
  const [x, y] = vec.midpoint(start, end);
32505
- const length = parseFloat(vec.dist(start, end).toFixed(snapTo === "sides" ? 0 : 1));
32213
+ const length = vec.dist(start, end);
32214
+ // Check if the length needs to indicate
32215
+ // that it's an approximation.
32216
+ const isApprox = !Number.isInteger(length);
32506
32217
  return /*#__PURE__*/React.createElement(TextLabel, {
32507
32218
  key: "side-" + i,
32508
32219
  x: x,
32509
32220
  y: y
32510
- }, !Number.isInteger(length) && "≈ ", length);
32221
+ }, isApprox ? `≈ ${length.toFixed(snapTo === "sides" ? 0 : 1)}` : length);
32511
32222
  }), /*#__PURE__*/React.createElement(Polygon, {
32512
32223
  points: [...points],
32513
32224
  color: "transparent",
@@ -32527,27 +32238,80 @@ const LimitedPolygonGraph = statefulProps => {
32527
32238
  setFocusVisible(hasFocusVisible(polygonRef.current));
32528
32239
  },
32529
32240
  // Required for lines to darken on focus
32530
- onFocus: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
32241
+ onFocus: () => {
32242
+ setFocusVisible(hasFocusVisible(polygonRef.current));
32243
+ setAriaLives(() => ["polite", ...pointsOffArray]);
32244
+ },
32531
32245
  // Required for line weighting to update on blur. Without this,
32532
32246
  // the user has to hover over the shape for it to update
32533
32247
  onBlur: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
32534
- className: "movable-polygon"
32535
- }
32536
- }), points.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
32537
- key: "point-" + i,
32538
- constrain: constrain,
32539
- point: point,
32540
- sequenceNumber: i + 1,
32541
- onMove: destination => {
32542
- const now = Date.now();
32543
- const targetFPS = 40;
32544
- const moveThresholdTime = 1000 / targetFPS;
32545
- if (now - lastMoveTimeRef.current > moveThresholdTime) {
32546
- dispatch(actions.polygon.movePoint(i, destination));
32547
- lastMoveTimeRef.current = now;
32548
- }
32248
+ className: "movable-polygon",
32249
+ // Accessibility-related fields
32250
+ role: "button",
32251
+ "aria-label": srPolygonGraphPoints ? `${srPolygonElementsNum} ${srPolygonGraphPoints}` : srPolygonElementsNum,
32252
+ "aria-live": ariaLives[0]
32549
32253
  }
32550
- })));
32254
+ }), points.map((point, i) => {
32255
+ const angleId = `${id}-angle-${i}`;
32256
+ const side1Id = `${id}-point-${i}-side-1`;
32257
+ const side2Id = `${id}-point-${i}-side-2`;
32258
+
32259
+ // Limited polygons always have an angle at each vertex.
32260
+ const angle = getAngleFromPoints(points, i);
32261
+ const angleDegree = angle ? convertRadiansToDegrees(angle) : null;
32262
+
32263
+ // Limited polygons always two sides attached to each vertex.
32264
+ const sidesArray = getSideLengthsFromPoints(points, i);
32265
+ const {
32266
+ pointIndex: point1Index,
32267
+ sideLength: side1Length
32268
+ } = sidesArray[0];
32269
+ const {
32270
+ pointIndex: point2Index,
32271
+ sideLength: side2Length
32272
+ } = sidesArray[1];
32273
+ return /*#__PURE__*/React.createElement("g", {
32274
+ key: "point-" + i
32275
+ }, /*#__PURE__*/React.createElement(MovablePoint$1, {
32276
+ ariaDescribedBy: `${angleId} ${side1Id} ${side2Id}`,
32277
+ ariaLive: ariaLives[i + 1],
32278
+ constrain: constrain,
32279
+ point: point,
32280
+ sequenceNumber: i + 1,
32281
+ onMove: destination => {
32282
+ const now = Date.now();
32283
+ const targetFPS = 40;
32284
+ const moveThresholdTime = 1000 / targetFPS;
32285
+ if (now - lastMoveTimeRef.current > moveThresholdTime) {
32286
+ dispatch(actions.polygon.movePoint(i, destination));
32287
+ lastMoveTimeRef.current = now;
32288
+ }
32289
+ },
32290
+ onFocus: () => {
32291
+ const newPointAriaLives = [...pointsOffArray];
32292
+ newPointAriaLives[i] = "polite";
32293
+ // Whole polygon is "off", and the current
32294
+ // point is "polite".
32295
+ setAriaLives(["off", ...newPointAriaLives]);
32296
+ }
32297
+ }), angleDegree && /*#__PURE__*/React.createElement("g", {
32298
+ id: angleId
32299
+ }, Number.isInteger(angleDegree) ? strings.srPolygonPointAngle({
32300
+ angle: angleDegree
32301
+ }) : strings.srPolygonPointAngleApprox({
32302
+ angle: srFormatNumber(angleDegree, locale, 1)
32303
+ })), /*#__PURE__*/React.createElement("g", {
32304
+ id: side1Id
32305
+ }, getPolygonSideString(side1Length, point1Index, strings, locale)), /*#__PURE__*/React.createElement("g", {
32306
+ id: side2Id
32307
+ }, getPolygonSideString(side2Length, point2Index, strings, locale)));
32308
+ }), /*#__PURE__*/React.createElement("g", {
32309
+ id: polygonPointsNumId,
32310
+ style: a11y.srOnly
32311
+ }, srPolygonGraphPointsNum), srPolygonGraphPoints && /*#__PURE__*/React.createElement("g", {
32312
+ id: polygonPointsId,
32313
+ style: a11y.srOnly
32314
+ }, srPolygonGraphPoints));
32551
32315
  };
32552
32316
  const UnlimitedPolygonGraph = statefulProps => {
32553
32317
  const {
@@ -32562,6 +32326,19 @@ const UnlimitedPolygonGraph = statefulProps => {
32562
32326
  coords,
32563
32327
  closedPolygon
32564
32328
  } = statefulProps.graphState;
32329
+ const {
32330
+ strings,
32331
+ locale
32332
+ } = usePerseusI18n();
32333
+ const id = React.useId();
32334
+ const polygonPointsNumId = id + "-points-num";
32335
+ const polygonPointsId = id + "-points";
32336
+
32337
+ // When moving an element, set its aria-live to "polite" and the others
32338
+ // to "off". Otherwise, other connected elements that move at the same
32339
+ // time might override the currently focused element's aria live.
32340
+ const pointsOffArray = Array(points.length).fill("off");
32341
+ const [ariaLives, setAriaLives] = React.useState(pointsOffArray);
32565
32342
 
32566
32343
  // If the polygon is closed, return a LimitedPolygon component.
32567
32344
  if (closedPolygon) {
@@ -32575,7 +32352,22 @@ const UnlimitedPolygonGraph = statefulProps => {
32575
32352
  } = graphConfig;
32576
32353
  const widthPx = graphDimensionsInPixels[0];
32577
32354
  const heightPx = graphDimensionsInPixels[1];
32578
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Polyline, {
32355
+
32356
+ // Aria label srings
32357
+ const emptyGraph = coords.length === 0;
32358
+ const {
32359
+ srPolygonGraph,
32360
+ srPolygonGraphPointsNum,
32361
+ srPolygonGraphPoints
32362
+ } = describePolygonGraph(statefulProps.graphState, {
32363
+ strings,
32364
+ locale
32365
+ }, statefulProps.graphConfig.markings);
32366
+ return /*#__PURE__*/React.createElement("g", {
32367
+ // Outer graph minimal description
32368
+ "aria-label": emptyGraph ? strings.srUnlimitedPolygonEmpty : srPolygonGraph,
32369
+ "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
32370
+ }, /*#__PURE__*/React.createElement(Polyline, {
32579
32371
  points: [...points],
32580
32372
  color: "var(--movable-line-stroke-color)",
32581
32373
  svgPolylineProps: {
@@ -32600,28 +32392,68 @@ const UnlimitedPolygonGraph = statefulProps => {
32600
32392
  const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
32601
32393
  dispatch(actions.polygon.addPoint(graphCoordinates[0]));
32602
32394
  }
32603
- }), coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
32604
- key: i,
32605
- point: point,
32606
- sequenceNumber: i + 1,
32607
- onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
32608
- ref: ref => {
32609
- pointsRef.current[i] = ref;
32610
- },
32611
- onFocus: () => {
32612
- dispatch(actions.polygon.focusPoint(i));
32613
- },
32614
- onClick: () => {
32615
- // If the point being clicked is the first point and
32616
- // there's enough non-duplicated points to form
32617
- // a polygon (3 or more), close the shape before
32618
- // setting focus.
32619
- if (i === 0 && getArrayWithoutDuplicates(coords).length >= 3) {
32620
- dispatch(actions.polygon.closePolygon());
32621
- }
32622
- dispatch(actions.polygon.clickPoint(i));
32395
+ }), coords.map((point, i) => {
32396
+ const angleId = `${id}-angle-${i}`;
32397
+ let sideIds = "";
32398
+
32399
+ // This point only has an angle if it's not the first
32400
+ // or last point in this open polygon.
32401
+ const hasAngle = i > 0 && i < coords.length - 1;
32402
+ const angle = hasAngle ? getAngleFromPoints(points, i) : null;
32403
+ const angleDegree = angle ? convertRadiansToDegrees(angle) : null;
32404
+ const sidesArray = getSideLengthsFromPoints(points, i, true);
32405
+ for (let sideIndex = 0; sideIndex < sidesArray.length; sideIndex++) {
32406
+ sideIds += `${id}-point-${i}-side-${sideIndex} `;
32623
32407
  }
32624
- })));
32408
+ return /*#__PURE__*/React.createElement("g", {
32409
+ key: "point-" + i
32410
+ }, /*#__PURE__*/React.createElement(MovablePoint$1, {
32411
+ ariaDescribedBy: `${angleId} ${sideIds}`,
32412
+ ariaLive: ariaLives[i],
32413
+ point: point,
32414
+ sequenceNumber: i + 1,
32415
+ onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
32416
+ ref: ref => {
32417
+ pointsRef.current[i] = ref;
32418
+ },
32419
+ onFocus: () => {
32420
+ dispatch(actions.polygon.focusPoint(i));
32421
+ const newPointAriaLives = [...pointsOffArray];
32422
+ newPointAriaLives[i] = "polite";
32423
+ // Whole polygon is "off", and the current
32424
+ // point is "polite".
32425
+ setAriaLives([...newPointAriaLives]);
32426
+ },
32427
+ onClick: () => {
32428
+ // If the point being clicked is the first point and
32429
+ // there's enough non-duplicated points to form
32430
+ // a polygon (3 or more), close the shape before
32431
+ // setting focus.
32432
+ if (i === 0 && getArrayWithoutDuplicates(coords).length >= 3) {
32433
+ dispatch(actions.polygon.closePolygon());
32434
+ }
32435
+ dispatch(actions.polygon.clickPoint(i));
32436
+ }
32437
+ }), angleDegree && /*#__PURE__*/React.createElement("g", {
32438
+ id: angleId
32439
+ }, Number.isInteger(angleDegree) ? strings.srPolygonPointAngle({
32440
+ angle: angleDegree
32441
+ }) : strings.srPolygonPointAngleApprox({
32442
+ angle: srFormatNumber(angleDegree, locale, 1)
32443
+ })), sidesArray.map(({
32444
+ pointIndex,
32445
+ sideLength
32446
+ }, j) => /*#__PURE__*/React.createElement("g", {
32447
+ key: `${id}-point-${i}-side-${j}`,
32448
+ id: `${id}-point-${i}-side-${j}`
32449
+ }, getPolygonSideString(sideLength, pointIndex, strings, locale))));
32450
+ }), coords.length > 0 && /*#__PURE__*/React.createElement("g", {
32451
+ id: polygonPointsNumId,
32452
+ style: a11y.srOnly
32453
+ }, srPolygonGraphPointsNum), srPolygonGraphPoints && /*#__PURE__*/React.createElement("g", {
32454
+ id: polygonPointsId,
32455
+ style: a11y.srOnly
32456
+ }, srPolygonGraphPoints));
32625
32457
  };
32626
32458
  function getLines(points) {
32627
32459
  return points.map((point, i) => {
@@ -32643,6 +32475,54 @@ const hasFocusVisible = element => {
32643
32475
  return matches(":focus");
32644
32476
  }
32645
32477
  };
32478
+ function getPolygonGraphDescription(state, i18n, markings) {
32479
+ const strings = describePolygonGraph(state, i18n, markings);
32480
+ return strings.srPolygonInteractiveElements;
32481
+ }
32482
+ function describePolygonGraph(state, i18n, markings) {
32483
+ const {
32484
+ strings,
32485
+ locale
32486
+ } = i18n;
32487
+ const {
32488
+ coords
32489
+ } = state;
32490
+ const isCoordinatePlane = markings === "axes" || markings === "graph";
32491
+ const hasOnePoint = coords.length === 1;
32492
+
32493
+ // Figure out graph aria label based on markings.
32494
+ const srPolygonGraph = isCoordinatePlane ? strings.srPolygonGraphCoordinatePlane : strings.srPolygonGraph;
32495
+ const srPolygonGraphPointsNum = hasOnePoint ? strings.srPolygonGraphPointsOne : strings.srPolygonGraphPointsNum({
32496
+ num: coords.length
32497
+ });
32498
+ let srPolygonGraphPoints;
32499
+ // Figure out graph description based on markings.
32500
+ // If the graph is not on a coordinate plane, we should not include
32501
+ // the points' coordinates in the description.
32502
+ if (isCoordinatePlane) {
32503
+ const pointsString = coords.map((coord, i) => {
32504
+ return strings.srPointAtCoordinates({
32505
+ num: i + 1,
32506
+ x: srFormatNumber(coord[0], locale),
32507
+ y: srFormatNumber(coord[1], locale)
32508
+ });
32509
+ });
32510
+ srPolygonGraphPoints = pointsString.join(" ");
32511
+ }
32512
+ const srPolygonElementsNum = hasOnePoint ? strings.srPolygonElementsOne : strings.srPolygonElementsNum({
32513
+ num: coords.length
32514
+ });
32515
+ const srPolygonInteractiveElements = coords.length > 0 ? strings.srInteractiveElements({
32516
+ elements: [srPolygonElementsNum, srPolygonGraphPoints].join(" ")
32517
+ }) : null;
32518
+ return {
32519
+ srPolygonGraph,
32520
+ srPolygonGraphPointsNum,
32521
+ srPolygonGraphPoints,
32522
+ srPolygonElementsNum,
32523
+ srPolygonInteractiveElements
32524
+ };
32525
+ }
32646
32526
 
32647
32527
  function renderQuadraticGraph(state, dispatch, i18n) {
32648
32528
  return {
@@ -33342,7 +33222,8 @@ const MafsGraph = props => {
33342
33222
  } = renderGraphElements({
33343
33223
  state,
33344
33224
  dispatch,
33345
- i18n
33225
+ i18n,
33226
+ markings: props.markings
33346
33227
  });
33347
33228
  return /*#__PURE__*/React.createElement(GraphConfigContext.Provider, {
33348
33229
  value: {
@@ -33377,7 +33258,7 @@ const MafsGraph = props => {
33377
33258
  handleKeyboardEvent(event, state, dispatch);
33378
33259
  },
33379
33260
  "aria-label": fullGraphAriaLabel,
33380
- "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && "unlimited-graph-keyboard-prompt"),
33261
+ "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && unlimitedGraphKeyboardPromptId),
33381
33262
  ref: graphRef,
33382
33263
  tabIndex: 0,
33383
33264
  onFocus: event => {
@@ -33403,7 +33284,19 @@ const MafsGraph = props => {
33403
33284
  bottom: 0,
33404
33285
  left: 0
33405
33286
  }
33406
- }, (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisLabels, null)), /*#__PURE__*/React.createElement(Mafs, {
33287
+ }, (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisLabels, null)), /*#__PURE__*/React.createElement(View
33288
+ // If we have locked figures, we set aria-hidden
33289
+ // to false so that screen readers can read the
33290
+ // locked figures. If we don't have any locked
33291
+ // figures, we need to set aria-hidden to true so
33292
+ // that screen readers don't read this Mafs element
33293
+ // as an empty image.
33294
+ // Note: Adding this in a wrapping View element
33295
+ // so that `aria-hidden` is a valid property.
33296
+ // It does not work on `Mafs` or svg elements.
33297
+ , {
33298
+ "aria-hidden": props.lockedFigures && props.lockedFigures.length > 0 ? false : true
33299
+ }, /*#__PURE__*/React.createElement(Mafs, {
33407
33300
  preserveAspectRatio: false,
33408
33301
  viewBox: {
33409
33302
  x: state.range[X],
@@ -33423,10 +33316,10 @@ const MafsGraph = props => {
33423
33316
  height: height
33424
33317
  })),
33425
33318
  // Only render the axis ticks and arrows if the markings are set to a full "graph"
33426
- (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisTicks, null), /*#__PURE__*/React.createElement(AxisArrows, null)), /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
33319
+ (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisTicks, null), /*#__PURE__*/React.createElement(AxisArrows, null)), props.lockedFigures && props.lockedFigures.length > 0 && /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, /*#__PURE__*/React.createElement(GraphLockedLayer, {
33427
33320
  lockedFigures: props.lockedFigures,
33428
33321
  range: state.range
33429
- }))), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLabelsLayer, {
33322
+ })))), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLabelsLayer, {
33430
33323
  lockedFigures: props.lockedFigures
33431
33324
  }), /*#__PURE__*/React.createElement(View, {
33432
33325
  style: {
@@ -33721,7 +33614,8 @@ const renderGraphElements = props => {
33721
33614
  const {
33722
33615
  state,
33723
33616
  dispatch,
33724
- i18n
33617
+ i18n,
33618
+ markings
33725
33619
  } = props;
33726
33620
  const {
33727
33621
  type
@@ -33738,7 +33632,7 @@ const renderGraphElements = props => {
33738
33632
  case "ray":
33739
33633
  return renderRayGraph(state, dispatch);
33740
33634
  case "polygon":
33741
- return renderPolygonGraph(state, dispatch);
33635
+ return renderPolygonGraph(state, dispatch, i18n, markings);
33742
33636
  case "point":
33743
33637
  return renderPointGraph(state, dispatch);
33744
33638
  case "circle":
@@ -36375,26 +36269,17 @@ class InteractiveGraph extends React.Component {
36375
36269
  }
36376
36270
  render() {
36377
36271
  var _this$props$apiOption;
36378
- // Mafs shim
36379
- const mafsFlags = (_this$props$apiOption = this.props.apiOptions) == null || (_this$props$apiOption = _this$props$apiOption.flags) == null ? void 0 : _this$props$apiOption["mafs"];
36380
- if (shouldUseMafs(mafsFlags, this.props.graph)) {
36381
- var _this$props$apiOption2, _this$props$apiOption3;
36382
- const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
36383
- const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
36384
- const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
36385
- return /*#__PURE__*/React.createElement(StatefulMafsGraph, _extends({}, this.props, {
36386
- flags: (_this$props$apiOption2 = this.props.apiOptions) == null ? void 0 : _this$props$apiOption2.flags,
36387
- ref: this.mafsRef,
36388
- gridStep: gridStep,
36389
- snapStep: snapStep,
36390
- box: box,
36391
- showTooltips: !!this.props.showTooltips,
36392
- readOnly: (_this$props$apiOption3 = this.props.apiOptions) == null ? void 0 : _this$props$apiOption3.readOnly
36393
- }));
36394
- }
36395
- return /*#__PURE__*/React.createElement(LegacyInteractiveGraph, _extends({
36396
- ref: this.legacyGraphRef
36397
- }, this.props));
36272
+ const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
36273
+ const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
36274
+ const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
36275
+ return /*#__PURE__*/React.createElement(StatefulMafsGraph, _extends({}, this.props, {
36276
+ ref: this.mafsRef,
36277
+ gridStep: gridStep,
36278
+ snapStep: snapStep,
36279
+ box: box,
36280
+ showTooltips: !!this.props.showTooltips,
36281
+ readOnly: (_this$props$apiOption = this.props.apiOptions) == null ? void 0 : _this$props$apiOption.readOnly
36282
+ }));
36398
36283
  }
36399
36284
 
36400
36285
  /**
@@ -36710,7 +36595,7 @@ class InteractiveGraph extends React.Component {
36710
36595
  }
36711
36596
  }
36712
36597
 
36713
- // exported for testing
36598
+ // We don't need to change any of the original props for static mode
36714
36599
  InteractiveGraph.defaultProps = {
36715
36600
  labels: ["x", "y"],
36716
36601
  range: [[-10, 10], [-10, 10]],
@@ -36723,37 +36608,13 @@ InteractiveGraph.defaultProps = {
36723
36608
  type: "linear"
36724
36609
  }
36725
36610
  };
36726
- function shouldUseMafs(mafsFlags, graph) {
36727
- if (typeof mafsFlags === "boolean" || typeof mafsFlags === "undefined") {
36728
- return false;
36729
- }
36730
- switch (graph.type) {
36731
- case "none":
36732
- return true;
36733
- case "point":
36734
- if (graph.numPoints === UNLIMITED) {
36735
- return Boolean(mafsFlags["unlimited-point"]);
36736
- }
36737
- return Boolean(mafsFlags["point"]);
36738
- case "polygon":
36739
- if (graph.numSides === UNLIMITED) {
36740
- return Boolean(mafsFlags["unlimited-polygon"]);
36741
- }
36742
- return Boolean(mafsFlags["polygon"]);
36743
- default:
36744
- return Boolean(mafsFlags[graph.type]);
36745
- }
36746
- }
36747
- // We don't need to change any of the original props for static mode
36748
36611
  const staticTransform$3 = _.identity;
36749
36612
  var InteractiveGraph$1 = {
36750
36613
  name: "interactive-graph",
36751
36614
  displayName: "Interactive graph (Assessments only)",
36752
36615
  widget: InteractiveGraph,
36753
36616
  staticTransform: staticTransform$3,
36754
- // TODO(LEMS-2656): remove TS suppression
36755
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInteractiveGraphUserInput'.
36756
- scorer: scoreInteractiveGraph
36617
+ getPublicWidgetOptions: getInteractiveGraphPublicWidgetOptions
36757
36618
  };
36758
36619
 
36759
36620
  // TODO: can we use WB typography?
@@ -37695,9 +37556,7 @@ var LabelImage$1 = {
37695
37556
  widget: LabelImageWithDependencies,
37696
37557
  accessible: true,
37697
37558
  isLintable: true,
37698
- // TODO(LEMS-2656): remove TS suppression
37699
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusLabelImageUserInput'.
37700
- scorer: scoreLabelImage
37559
+ getPublicWidgetOptions: getLabelImagePublicWidgetOptions
37701
37560
  };
37702
37561
 
37703
37562
  var ItemState = /*#__PURE__*/function (ItemState) {
@@ -37930,7 +37789,7 @@ class Draggable extends React.Component {
37930
37789
  // We need to keep backwards compatbility with rules specified directly
37931
37790
  // in CSS. Hence the hacky tacking on of manual classNames.
37932
37791
  // See sortable.less for details.
37933
- let className = css(styles$8.card, styles$8.draggable, layout === "horizontal" && styles$8.horizontalCard, layout === "vertical" && styles$8.verticalCard, type === ItemState.DRAGGING && styles$8.dragging, type === ItemState.DISABLED && styles$8.disabled, !includePadding && styles$8.unpaddedCard) + " " + ClassNames.INTERACTIVE + " perseus-sortable-draggable";
37792
+ let className = css(styles$8.card, styles$8.draggable, layout === "horizontal" && styles$8.horizontalCard, layout === "vertical" && styles$8.verticalCard, type === ItemState.DRAGGING && styles$8.dragging, type === ItemState.DISABLED && styles$8.disabled, !includePadding && styles$8.unpaddedCard) + " " + "perseus-sortable-draggable";
37934
37793
  if (!includePadding) {
37935
37794
  className += " perseus-sortable-draggable-unpadded";
37936
37795
  }
@@ -38631,10 +38490,7 @@ var Matcher$1 = {
38631
38490
  name: "matcher",
38632
38491
  displayName: "Matcher (two column)",
38633
38492
  widget: Matcher,
38634
- isLintable: true,
38635
- // TODO(LEMS-2656): remove TS suppression
38636
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatcherUserInput'.
38637
- scorer: scoreMatcher
38493
+ isLintable: true
38638
38494
  };
38639
38495
 
38640
38496
  const getPromptJSON$8 = (renderProps, userInput) => {
@@ -39039,12 +38895,7 @@ var Matrix$1 = {
39039
38895
  transform: propTransform$1,
39040
38896
  staticTransform: staticTransform$2,
39041
38897
  isLintable: true,
39042
- // TODO(LEMS-2656): remove TS suppression
39043
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
39044
- scorer: scoreMatrix,
39045
- // TODO(LEMS-2656): remove TS suppression
39046
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
39047
- validator: validateMatrix
38898
+ getPublicWidgetOptions: getMatrixPublicWidgetOptions
39048
38899
  };
39049
38900
 
39050
38901
  const getPromptJSON$7 = () => {
@@ -39124,7 +38975,7 @@ class Measurer extends React.Component {
39124
38975
  // TODO(scottgrant): This isn't a11y-friendly! We should insist on
39125
38976
  // finding some valid alt text when this widget is used.
39126
38977
  return /*#__PURE__*/React.createElement("div", {
39127
- className: "perseus-widget perseus-widget-measurer " + "graphie-container above-scratchpad",
38978
+ className: "perseus-widget perseus-widget-measurer " + "graphie-container blank-background",
39128
38979
  style: {
39129
38980
  width: this.props.box[0],
39130
38981
  height: this.props.box[1]
@@ -39161,9 +39012,7 @@ var Measurer$1 = {
39161
39012
  hidden: true,
39162
39013
  widget: Measurer,
39163
39014
  version: measurerLogic.version,
39164
- propUpgrades: measurerLogic.widgetOptionsUpgrades,
39165
- // TODO: things that aren't interactive shouldn't need scoring functions
39166
- scorer: () => scoreNoop(1)
39015
+ propUpgrades: measurerLogic.widgetOptionsUpgrades
39167
39016
  };
39168
39017
 
39169
39018
  /**
@@ -40051,8 +39900,7 @@ var Molecule$1 = {
40051
39900
  name: "molecule-renderer",
40052
39901
  displayName: "Molecule renderer",
40053
39902
  hidden: true,
40054
- widget: MoleculeWidget,
40055
- scorer: () => scoreNoop()
39903
+ widget: MoleculeWidget
40056
39904
  };
40057
39905
 
40058
39906
  const getPromptJSON$6 = (renderProps, userInput) => {
@@ -40621,12 +40469,7 @@ var NumberLine$1 = {
40621
40469
  widget: NumberLine,
40622
40470
  transform: numberLineTransform,
40623
40471
  staticTransform: staticTransform$1,
40624
- // TODO(LEMS-2656): remove TS suppression
40625
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
40626
- scorer: scoreNumberLine,
40627
- // TODO(LEMS-2656): remove TS suppression
40628
- // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
40629
- validator: validateNumberLine
40472
+ getPublicWidgetOptions: getNumberLinePublicWidgetOptions
40630
40473
  };
40631
40474
 
40632
40475
  const getPromptJSON$5 = (renderProps, userInput) => {
@@ -40644,7 +40487,7 @@ const getPromptJSON$5 = (renderProps, userInput) => {
40644
40487
  class PlaceholderCard extends React.Component {
40645
40488
  render() {
40646
40489
  return /*#__PURE__*/React.createElement("div", {
40647
- className: "card-wrap " + ClassNames.INTERACTIVE,
40490
+ className: "card-wrap",
40648
40491
  style: {
40649
40492
  width: this.props.width
40650
40493
  }
@@ -40659,7 +40502,7 @@ class PlaceholderCard extends React.Component {
40659
40502
  class DragHintCard extends React.Component {
40660
40503
  render() {
40661
40504
  return /*#__PURE__*/React.createElement("div", {
40662
- className: "card-wrap " + ClassNames.INTERACTIVE
40505
+ className: "card-wrap"
40663
40506
  }, /*#__PURE__*/React.createElement("div", {
40664
40507
  className: "card drag-hint"
40665
40508
  }));
@@ -40795,7 +40638,7 @@ class Card extends React.Component {
40795
40638
  const rendererProps = _.pick(this.props, "content");
40796
40639
  const onMouseDown = this.props.animating ? $.noop : this.onMouseDown;
40797
40640
  return /*#__PURE__*/React.createElement("div", {
40798
- className: "card-wrap " + ClassNames.INTERACTIVE,
40641
+ className: "card-wrap",
40799
40642
  style: style,
40800
40643
  onMouseDown: onMouseDown,
40801
40644
  onTouchStart: onMouseDown,
@@ -41166,7 +41009,7 @@ class Orderer extends React.Component {
41166
41009
  });
41167
41010
  }, this));
41168
41011
  return /*#__PURE__*/React.createElement("div", {
41169
- className: "draggy-boxy-thing orderer " + "height-" + this.props.height + " " + "layout-" + this.props.layout + " " + "above-scratchpad blank-background " + "perseus-clearfix " + ClassNames.INTERACTIVE
41012
+ className: "draggy-boxy-thing orderer " + "height-" + this.props.height + " " + "layout-" + this.props.layout + " " + "blank-background " + "perseus-clearfix "
41170
41013
  // eslint-disable-next-line react/no-string-refs
41171
41014
  ,
41172
41015
  ref: "orderer"
@@ -41187,12 +41030,6 @@ var Orderer$1 = {
41187
41030
  hidden: true,
41188
41031
  widget: Orderer,
41189
41032
  isLintable: true,
41190
- // TODO(LEMS-2656): remove TS suppression
41191
- // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
41192
- scorer: scoreOrderer,
41193
- // TODO(LEMS-2656): remove TS suppression
41194
- // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
41195
- validator: validateOrderer,
41196
41033
  getPublicWidgetOptions: getOrdererPublicWidgetOptions
41197
41034
  };
41198
41035
 
@@ -41231,15 +41068,12 @@ PassageRefTarget.defaultProps = {
41231
41068
  var PassageRefTarget$1 = {
41232
41069
  name: "passage-ref-target",
41233
41070
  displayName: "PassageRefTarget",
41234
- defaultAlignment: "inline",
41235
41071
  widget: PassageRefTarget,
41236
41072
  hidden: true,
41237
41073
  transform: editorProps => {
41238
41074
  return _.pick(editorProps, "content");
41239
41075
  },
41240
- isLintable: true,
41241
- // TODO: things that aren't interactive shouldn't need scoring functions
41242
- scorer: () => scoreNoop()
41076
+ isLintable: true
41243
41077
  };
41244
41078
 
41245
41079
  const getPromptJSON$4 = () => {
@@ -42226,7 +42060,7 @@ class Plotter extends React.Component {
42226
42060
  marginBottom: this.props.labels[0] ? paddingForBottomLabel : 0
42227
42061
  };
42228
42062
  return /*#__PURE__*/React.createElement("div", {
42229
- className: "perseus-widget-plotter graphie " + ClassNames.INTERACTIVE
42063
+ className: "perseus-widget-plotter graphie"
42230
42064
  // eslint-disable-next-line react/no-string-refs
42231
42065
  ,
42232
42066
  ref: "graphieDiv",
@@ -42257,12 +42091,7 @@ var Plotter$1 = {
42257
42091
  hidden: true,
42258
42092
  widget: Plotter,
42259
42093
  staticTransform: staticTransform,
42260
- // TODO(LEMS-2656): remove TS suppression
42261
- // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
42262
- scorer: scorePlotter,
42263
- // TODO(LEMS-2656): remove TS suppression
42264
- // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
42265
- validator: validatePlotter
42094
+ getPublicWidgetOptions: getPlotterPublicWidgetOptions
42266
42095
  };
42267
42096
 
42268
42097
  const getPromptJSON$2 = () => {
@@ -42425,12 +42254,7 @@ var Sorter$1 = {
42425
42254
  displayName: "Sorter",
42426
42255
  widget: Sorter,
42427
42256
  isLintable: true,
42428
- // TODO(LEMS-2656): remove TS suppression
42429
- // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
42430
- scorer: scoreSorter,
42431
- // TODO(LEMS-2656): remove TS suppression
42432
- // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
42433
- validator: validateSorter
42257
+ getPublicWidgetOptions: getSorterPublicWidgetOptions
42434
42258
  };
42435
42259
 
42436
42260
  const {
@@ -42650,12 +42474,7 @@ var Table$1 = {
42650
42474
  transform: propTransform,
42651
42475
  hidden: true,
42652
42476
  isLintable: true,
42653
- // TODO(LEMS-2656): remove TS suppression
42654
- // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
42655
- scorer: scoreTable,
42656
- // TODO(LEMS-2656): remove TS suppression
42657
- // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
42658
- validator: validateTable
42477
+ getPublicWidgetOptions: getTablePublicWidgetOptions
42659
42478
  };
42660
42479
 
42661
42480
  const getPromptJSON = () => {
@@ -42815,11 +42634,7 @@ Video.contextType = PerseusI18nContext;
42815
42634
  var Video$1 = {
42816
42635
  name: "video",
42817
42636
  displayName: "Video",
42818
- defaultAlignment: "block",
42819
- supportedAlignments: ["block", "float-left", "float-right", "full-width"],
42820
- widget: Video,
42821
- // TODO: things that aren't interactive shouldn't need scoring functions
42822
- scorer: () => scoreNoop()
42637
+ widget: Video
42823
42638
  };
42824
42639
 
42825
42640
  /* eslint-disable import/no-named-as-default */
@@ -42838,7 +42653,7 @@ const init = function init() {
42838
42653
 
42839
42654
  // This file is processed by a Rollup plugin (replace) to inject the production
42840
42655
  const libName = "@khanacademy/perseus";
42841
- const libVersion = "52.0.1";
42656
+ const libVersion = "53.1.0";
42842
42657
  addLibraryVersionToPerseusDebug(libName, libVersion);
42843
42658
 
42844
42659
  const apiVersion = {
@@ -43705,6 +43520,42 @@ const ref = /*#__PURE__*/React.forwardRef((props, ref) => /*#__PURE__*/React.cre
43705
43520
  ref: ref
43706
43521
  }))));
43707
43522
 
43523
+ function isCorrect(score) {
43524
+ return score.type === "points" && score.earned >= score.total;
43525
+ }
43526
+ function keScoreFromPerseusScore(score,
43527
+ // It's weird, but this is what we're passing it
43528
+ guess, state) {
43529
+ if (score.type === "points") {
43530
+ return {
43531
+ empty: false,
43532
+ correct: isCorrect(score),
43533
+ message: score.message,
43534
+ guess: guess,
43535
+ state: state
43536
+ };
43537
+ }
43538
+ if (score.type === "invalid") {
43539
+ return {
43540
+ empty: true,
43541
+ correct: false,
43542
+ message: score.message,
43543
+ suppressAlmostThere: score.suppressAlmostThere,
43544
+ guess: guess,
43545
+ state: state
43546
+ };
43547
+ }
43548
+ throw new PerseusError(
43549
+ // @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
43550
+ "Invalid score type: " + score.type, Errors.InvalidInput, {
43551
+ metadata: {
43552
+ score: JSON.stringify(score),
43553
+ guess: JSON.stringify(guess),
43554
+ state: JSON.stringify(state)
43555
+ }
43556
+ });
43557
+ }
43558
+
43708
43559
  /**
43709
43560
  * Traverses a {content, widgets, images} renderer props object,
43710
43561
  * such as `itemData.question`
@@ -45081,5 +44932,5 @@ const ScoringUtil = {
45081
44932
  * Types
45082
44933
  */
45083
44934
 
45084
- export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, JiptParagraphs, KhanColors, context as LoadingContext, Log, Matrix$1 as MatrixWidget, NumericInput$1 as NumericInput, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, Radio, Renderer, ScoringUtil, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, apiVersion, bodyXsmallBold, components, containerSizeClass, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, ItemVersion as itemVersion, libVersion, makeSafeUrl, mathOnlyParser, mediaQueries, parseDataFromJSONP, preprocessTex, registerAllWidgetsForTesting, scorePerseusItem, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
44935
+ export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, JiptParagraphs, KhanColors, context as LoadingContext, Log, Matrix$1 as MatrixWidget, NumericInput$1 as NumericInput, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, Radio, Renderer, ScoringUtil, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, apiVersion, bodyXsmallBold, components, containerSizeClass, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, ItemVersion as itemVersion, libVersion, makeSafeUrl, mathOnlyParser, mediaQueries, parseDataFromJSONP, preprocessTex, registerAllWidgetsForTesting, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
45085
44936
  //# sourceMappingURL=index.js.map