@khanacademy/perseus 26.1.0 → 27.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -19195,6 +19195,7 @@ const replaceDeprecatedWidgets = () => {
19195
19195
  replaceWidget("reaction-diagram", "deprecated-standin");
19196
19196
  replaceWidget("sequence", "deprecated-standin");
19197
19197
  replaceWidget("simulator", "deprecated-standin");
19198
+ replaceWidget("unit-input", "deprecated-standin");
19198
19199
  };
19199
19200
  const registerEditors = editorsToRegister => {
19200
19201
  editorsToRegister.forEach(editor => {
@@ -19228,6 +19229,7 @@ const replaceDeprecatedEditors = () => {
19228
19229
  replaceEditor("reaction-diagram", "deprecated-standin");
19229
19230
  replaceEditor("sequence", "deprecated-standin");
19230
19231
  replaceEditor("simulator", "deprecated-standin");
19232
+ replaceEditor("unit-input", "deprecated-standin");
19231
19233
  };
19232
19234
  const getWidget = name => {
19233
19235
  // TODO(alex): Consider referring to these as renderers to avoid
@@ -28588,7 +28590,8 @@ const defaultGraphConfig = {
28588
28590
  graphDimensionsInPixels: [1, 1],
28589
28591
  width: 0,
28590
28592
  height: 0,
28591
- labels: []
28593
+ labels: [],
28594
+ hintMode: false
28592
28595
  };
28593
28596
  const GraphConfigContext = /*#__PURE__*/React.createContext(defaultGraphConfig);
28594
28597
  function useGraphConfig() {
@@ -30073,8 +30076,6 @@ const ItemExtras = [
30073
30076
 
30074
30077
  // prettier-ignore
30075
30078
 
30076
- // prettier-ignore
30077
-
30078
30079
  //prettier-ignore
30079
30080
 
30080
30081
  // A background image applied to various widgets.
@@ -30600,6 +30601,16 @@ function useDraggable(args) {
30600
30601
  const isKeyboard = type.includes("key");
30601
30602
  if (isKeyboard) {
30602
30603
  event?.preventDefault();
30604
+
30605
+ // When a key is held down, we see multiple "keydown" events,
30606
+ // followed by a single "keyup" event.
30607
+ // For a single keypress, we only see a "keydown" event, no
30608
+ // "keyup".
30609
+ // We never want to process the keyup event as an intent to
30610
+ // move so we bail on further processing here.
30611
+ if (type === "keyup") {
30612
+ return;
30613
+ }
30603
30614
  const {
30604
30615
  direction: yDownDirection,
30605
30616
  altKey,
@@ -30700,7 +30711,8 @@ const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
30700
30711
  const {
30701
30712
  range,
30702
30713
  markings,
30703
- showTooltips
30714
+ showTooltips,
30715
+ hintMode
30704
30716
  } = useGraphConfig();
30705
30717
  const {
30706
30718
  point,
@@ -30745,7 +30757,7 @@ const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
30745
30757
  cursor
30746
30758
  },
30747
30759
  "data-testid": "movable-point",
30748
- tabIndex: tabIndex(focusBehavior)
30760
+ tabIndex: hintMode ? -1 : tabIndex(focusBehavior)
30749
30761
  }, /*#__PURE__*/React__namespace.createElement("circle", {
30750
30762
  className: "movable-point-hitbox",
30751
30763
  r: hitboxSizePx / 2,
@@ -30833,7 +30845,8 @@ const MovableLine = props => {
30833
30845
  };
30834
30846
  function useControlPoint(point, color, onMovePoint) {
30835
30847
  const {
30836
- snapStep
30848
+ snapStep,
30849
+ hintMode
30837
30850
  } = useGraphConfig();
30838
30851
  const [focused, setFocused] = React.useState(false);
30839
30852
  const keyboardHandleRef = React.useRef(null);
@@ -30855,7 +30868,7 @@ function useControlPoint(point, color, onMovePoint) {
30855
30868
  const focusableHandle = /*#__PURE__*/React__namespace.createElement("g", {
30856
30869
  "data-testid": "movable-point__focusable-handle",
30857
30870
  className: "movable-point__focusable-handle",
30858
- tabIndex: 0,
30871
+ tabIndex: hintMode ? -1 : 0,
30859
30872
  ref: keyboardHandleRef,
30860
30873
  onFocus: () => setFocused(true),
30861
30874
  onBlur: () => setFocused(false)
@@ -30888,7 +30901,8 @@ const Line$1 = props => {
30888
30901
  const {
30889
30902
  range,
30890
30903
  graphDimensionsInPixels,
30891
- snapStep
30904
+ snapStep,
30905
+ hintMode
30892
30906
  } = useGraphConfig();
30893
30907
  let startExtend = undefined;
30894
30908
  let endExtend = undefined;
@@ -30910,7 +30924,7 @@ const Line$1 = props => {
30910
30924
  });
30911
30925
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("g", {
30912
30926
  ref: line,
30913
- tabIndex: 0,
30927
+ tabIndex: hintMode ? -1 : 0,
30914
30928
  className: "movable-line",
30915
30929
  "data-testid": "movable-line",
30916
30930
  style: {
@@ -31429,6 +31443,9 @@ const PolygonGraph = props => {
31429
31443
  snapStep,
31430
31444
  snapTo
31431
31445
  } = props.graphState;
31446
+ const {
31447
+ hintMode
31448
+ } = useGraphConfig();
31432
31449
 
31433
31450
  // TODO(benchristel): can the default set of points be removed here? I don't
31434
31451
  // think coords can be null.
@@ -31448,12 +31465,16 @@ const PolygonGraph = props => {
31448
31465
  constrain: p => ["angles", "sides"].includes(snapToValue) ? p : snap(snapStep, p)
31449
31466
  });
31450
31467
  const active = hovered || focused || dragging;
31468
+ const lastMoveTime = React__namespace.useRef(0);
31451
31469
  const lines = getLines(points);
31452
31470
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
31453
31471
  points: [...points],
31454
31472
  color: "var(--movable-line-stroke-color)",
31455
31473
  svgPolygonProps: {
31456
- strokeWidth: active ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)"
31474
+ strokeWidth: active ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)",
31475
+ style: {
31476
+ fill: "transparent"
31477
+ }
31457
31478
  }
31458
31479
  }), points.map((point, i) => {
31459
31480
  const pt1 = points.at(i - 1);
@@ -31485,10 +31506,11 @@ const PolygonGraph = props => {
31485
31506
  color: "transparent",
31486
31507
  svgPolygonProps: {
31487
31508
  ref,
31488
- tabIndex: 0,
31509
+ tabIndex: hintMode ? -1 : 0,
31489
31510
  strokeWidth: TARGET_SIZE,
31490
31511
  style: {
31491
- cursor: dragging ? "grabbing" : "grab"
31512
+ cursor: dragging ? "grabbing" : "grab",
31513
+ fill: hovered ? "var(--mafs-blue)" : "transparent"
31492
31514
  },
31493
31515
  onFocus: () => setFocused(true),
31494
31516
  onBlur: () => setFocused(false),
@@ -31500,7 +31522,15 @@ const PolygonGraph = props => {
31500
31522
  key: "point-" + i,
31501
31523
  snapTo: snapTo,
31502
31524
  point: point,
31503
- onMove: destination => dispatch(actions.polygon.movePoint(i, destination))
31525
+ onMove: destination => {
31526
+ const now = Date.now();
31527
+ const targetFPS = 40;
31528
+ const moveThresholdTime = 1000 / targetFPS;
31529
+ if (now - lastMoveTime.current > moveThresholdTime) {
31530
+ dispatch(actions.polygon.movePoint(i, destination));
31531
+ lastMoveTime.current = now;
31532
+ }
31533
+ }
31504
31534
  })));
31505
31535
  };
31506
31536
  function getLines(points) {
@@ -31621,7 +31651,8 @@ function MovableCircle(props) {
31621
31651
  onMove
31622
31652
  } = props;
31623
31653
  const {
31624
- snapStep
31654
+ snapStep,
31655
+ hintMode
31625
31656
  } = useGraphConfig();
31626
31657
  const draggableRef = React.useRef(null);
31627
31658
  const {
@@ -31636,7 +31667,7 @@ function MovableCircle(props) {
31636
31667
  const [radiiPx] = useTransformDimensionsToPixels([radius, radius]);
31637
31668
  return /*#__PURE__*/React__namespace.createElement("g", {
31638
31669
  ref: draggableRef,
31639
- tabIndex: 0,
31670
+ tabIndex: hintMode ? -1 : 0,
31640
31671
  className: `movable-circle ${dragging ? "movable-circle--dragging" : ""}`
31641
31672
  }, /*#__PURE__*/React__namespace.createElement("ellipse", {
31642
31673
  className: "focus-ring",
@@ -32040,7 +32071,8 @@ const MafsGraph = props => {
32040
32071
  const {
32041
32072
  state,
32042
32073
  dispatch,
32043
- labels
32074
+ labels,
32075
+ hintMode
32044
32076
  } = props;
32045
32077
  const [width, height] = props.box;
32046
32078
  const tickStep = props.step;
@@ -32055,7 +32087,8 @@ const MafsGraph = props => {
32055
32087
  graphDimensionsInPixels: props.box,
32056
32088
  width,
32057
32089
  height,
32058
- labels
32090
+ labels,
32091
+ hintMode
32059
32092
  }
32060
32093
  }, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
32061
32094
  className: "mafs-graph",
@@ -32066,7 +32099,8 @@ const MafsGraph = props => {
32066
32099
  padding: "25px 25px 0 0",
32067
32100
  boxSizing: "content-box",
32068
32101
  marginLeft: "20px",
32069
- marginBottom: "20px"
32102
+ marginBottom: "20px",
32103
+ pointerEvents: hintMode ? "none" : "auto"
32070
32104
  }
32071
32105
  }, /*#__PURE__*/React__namespace.createElement(LegacyGrid, {
32072
32106
  box: props.box,
@@ -32097,7 +32131,7 @@ const MafsGraph = props => {
32097
32131
  }), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
32098
32132
  lockedFigures: props.lockedFigures,
32099
32133
  range: state.range
32100
- }), "/* Protractor */", props.showProtractor && /*#__PURE__*/React__namespace.createElement(Protractor, null), renderGraph({
32134
+ }), props.showProtractor && /*#__PURE__*/React__namespace.createElement(Protractor, null), renderGraph({
32101
32135
  state,
32102
32136
  dispatch
32103
32137
  })))));
@@ -33198,7 +33232,6 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
33198
33232
  const snapTo = graph.type === "polygon" ? graph.snapTo : null;
33199
33233
  const showAngles = graph.type === "polygon" ? graph.showAngles : null;
33200
33234
  const showSides = graph.type === "polygon" ? graph.showSides : null;
33201
- const startCoords = graph.startCoords ?? null;
33202
33235
  const originalPropsRef = React.useRef(props);
33203
33236
  const latestPropsRef = wonderBlocksCore.useLatestRef(props);
33204
33237
  React.useEffect(() => {
@@ -33209,7 +33242,7 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
33209
33242
  if (latestPropsRef.current !== originalPropsRef.current) {
33210
33243
  dispatch(reinitialize(latestPropsRef.current));
33211
33244
  }
33212
- }, [graph.type, numSegments, numSides, snapTo, showAngles, showSides, startCoords, latestPropsRef, startCoords]);
33245
+ }, [graph.type, numSegments, numSides, snapTo, showAngles, showSides, latestPropsRef, graph.startCoords]);
33213
33246
  return /*#__PURE__*/React__namespace.createElement(MafsGraph, _extends({}, props, {
33214
33247
  state: state,
33215
33248
  dispatch: dispatch
@@ -33300,9 +33333,6 @@ class LegacyInteractiveGraph extends React__namespace.Component {
33300
33333
  markings: "graph",
33301
33334
  showTooltips: false,
33302
33335
  showProtractor: false,
33303
- showRuler: false,
33304
- rulerLabel: "",
33305
- rulerTicks: 10,
33306
33336
  graph: {
33307
33337
  type: "linear"
33308
33338
  }
@@ -34465,9 +34495,6 @@ class LegacyInteractiveGraph extends React__namespace.Component {
34465
34495
  markings: this.props.markings,
34466
34496
  backgroundImage: this.props.backgroundImage,
34467
34497
  showProtractor: this.props.showProtractor,
34468
- showRuler: this.props.showRuler,
34469
- rulerLabel: this.props.rulerLabel,
34470
- rulerTicks: this.props.rulerTicks,
34471
34498
  onMouseDown: onMouseDown,
34472
34499
  onGraphieUpdated: this.setGraphie,
34473
34500
  setDrawingAreaAvailable: this.props.apiOptions.setDrawingAreaAvailable,
@@ -34486,9 +34513,6 @@ class InteractiveGraph extends React__namespace.Component {
34486
34513
  markings: "graph",
34487
34514
  showTooltips: false,
34488
34515
  showProtractor: false,
34489
- showRuler: false,
34490
- rulerLabel: "",
34491
- rulerTicks: 10,
34492
34516
  graph: {
34493
34517
  type: "linear"
34494
34518
  }
@@ -44207,1231 +44231,316 @@ var Table$1 = {
44207
44231
  isLintable: true
44208
44232
  };
44209
44233
 
44210
- var isObject = function (obj) {
44211
- return obj === Object(obj);
44234
+ /**
44235
+ * This is a component that adds a link to the transcript for embedded videos in articles.
44236
+ */
44237
+ const IS_URL$1 = /^https?:\/\//;
44238
+ const getYoutubeId = url => {
44239
+ const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
44240
+ const match = url.match(regExp);
44241
+ if (match && match[7].length === 11) {
44242
+ return match[7];
44243
+ }
44244
+ return "videoNotFound";
44212
44245
  };
44213
- var merge = function () {
44214
- var obj = {};
44215
- for (var i = 0; i < arguments.length; i++) {
44216
- var source = arguments[i];
44217
- if (source) {
44218
- for (var prop in source) {
44219
- obj[prop] = source[prop];
44246
+
44247
+ // will accept a youtube link or a video's slug name from embedded video and then query accordingly.
44248
+
44249
+ /**
44250
+ * Video Transcript Link Component.
44251
+ */
44252
+ const VideoTranscriptLink = props => {
44253
+ const {
44254
+ location
44255
+ } = props;
44256
+ const {
44257
+ useVideo
44258
+ } = getDependencies();
44259
+ const [id, kind] = IS_URL$1.test(location) ? [getYoutubeId(location), "YOUTUBE_ID"] : [location, "READABLE_ID"];
44260
+ // The result value conforms to the wonder-blocks-data `Result` type
44261
+ // which is used by our GraphQL framework.
44262
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'VideoKind'.
44263
+ const result = useVideo(id, kind);
44264
+ const {
44265
+ strings
44266
+ } = usePerseusI18n();
44267
+ switch (result.status) {
44268
+ case "loading":
44269
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.loading);
44270
+ case "success":
44271
+ {
44272
+ const video = result.data?.video;
44273
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
44274
+ style: styles$4.transcriptLink
44275
+ }, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.Text, null, video?.title), /*#__PURE__*/React__namespace.createElement(wonderBlocksLayout.Strut, {
44276
+ size: 10
44277
+ }), /*#__PURE__*/React__namespace.createElement(Link__default["default"], {
44278
+ href: "/transcript/" + (video?.contentId || "videoNotFound"),
44279
+ target: "_blank",
44280
+ className: "visited-no-recolor"
44281
+ }, strings.videoTranscript));
44220
44282
  }
44221
- }
44283
+ case "error":
44284
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.somethingWrong);
44285
+ case "aborted":
44286
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.somethingWrong);
44287
+ default:
44288
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.somethingWrong);
44222
44289
  }
44223
- return obj;
44224
44290
  };
44225
- var clone = function (obj) {
44226
- if (!isObject(obj)) {
44227
- return obj;
44291
+ const styles$4 = aphrodite.StyleSheet.create({
44292
+ transcriptLink: {
44293
+ flexDirection: "row",
44294
+ width: "100%",
44295
+ justifyContent: "center"
44228
44296
  }
44229
- return Array.isArray(obj) ? obj.slice() : merge(obj);
44230
- };
44297
+ });
44231
44298
 
44232
- /* TODO batch *all* mutations
44233
- * idea: freeze / thaw implementations for all types
44234
- * lens constructor thaws, freeze delegates to type's freeze
44299
+ /**
44300
+ * This is a video widget for embedding videos in articles.
44235
44301
  */
44302
+ // Current default is 720p, based on the typical videos we upload currently
44303
+ const DEFAULT_WIDTH = 1280;
44304
+ const DEFAULT_HEIGHT = 720;
44305
+ const KA_EMBED = "{host}/embed_video?slug={slug}" + "&internal_video_only=1";
44306
+ const IS_URL = /^https?:\/\//;
44307
+ const IS_KA_SITE = /(khanacademy\.org|localhost)/;
44308
+ const IS_VIMEO = /(vimeo\.com)/;
44236
44309
 
44237
- // equivalents, without requiring it
44238
- // find the implementation to use for a given object
44239
- var dispatch = function (x) {
44240
- if (Array.isArray(x)) {
44241
- return arr;
44242
- } else if (isObject(x)) {
44243
- return obj;
44244
- } else if (typeof x === "string") {
44245
- return str;
44246
- }
44247
- };
44310
+ // exports has no 'transform'
44248
44311
 
44249
- // This is underscore with a different name
44250
- var lens = function (obj) {
44251
- if (obj instanceof lens) {
44252
- return obj;
44312
+ /**
44313
+ * Video renderer.
44314
+ */
44315
+ class Video extends React__namespace.Component {
44316
+ static contextType = PerseusI18nContext;
44317
+ /**
44318
+ * This is the widget's grading function.
44319
+ * Points for videos are tallied by the embedded video itself, in the case
44320
+ * of Khan Academy videos.
44321
+ */
44322
+ static validate(userInput, rubric) {
44323
+ return {
44324
+ type: "points",
44325
+ earned: 0,
44326
+ total: 0,
44327
+ message: null
44328
+ };
44253
44329
  }
44254
- if (!(this instanceof lens)) {
44255
- return new lens(obj);
44330
+ getUserInput = () => {
44331
+ return null;
44332
+ };
44333
+ simpleValidate = rubric => {
44334
+ return Video.validate(null, rubric);
44335
+ };
44336
+ change = (() => {
44337
+ var _this = this;
44338
+ return function () {
44339
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
44340
+ args[_key] = arguments[_key];
44341
+ }
44342
+ // @ts-expect-error - TS2345 - Argument of type 'readonly unknown[]' is not assignable to parameter of type 'any[]'.
44343
+ return change.apply(_this, args);
44344
+ };
44345
+ })();
44346
+ render() {
44347
+ const {
44348
+ InitialRequestUrl
44349
+ } = getDependencies();
44350
+ const location = this.props.location;
44351
+ if (!location) {
44352
+ return /*#__PURE__*/React__namespace.createElement("div", null);
44353
+ }
44354
+ let url;
44355
+ if (IS_URL.test(location)) {
44356
+ url = location;
44357
+ if (IS_VIMEO.test(url)) {
44358
+ // If this is a vimeo video, we need to add the query string
44359
+ // parameter "dnt" so that analytics/tracking cookies aren't set.
44360
+ // https://help.vimeo.com/hc/en-us/articles/12426260232977-Player-parameters-overview
44361
+ if (url.indexOf("?") === -1) {
44362
+ url += "?dnt=1";
44363
+ } else {
44364
+ url += "&dnt=1";
44365
+ }
44366
+ }
44367
+ } else {
44368
+ url = KA_EMBED.replace("{slug}", location);
44369
+ let embedHostname = "https://www.khanacademy.org";
44370
+ if (IS_KA_SITE.test(InitialRequestUrl.host)) {
44371
+ embedHostname = InitialRequestUrl.origin;
44372
+ }
44373
+ url = url.replace("{host}", embedHostname);
44374
+ }
44375
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, /*#__PURE__*/React__namespace.createElement(FixedToResponsive, {
44376
+ width: DEFAULT_WIDTH,
44377
+ height: DEFAULT_HEIGHT
44378
+ // The key is here for the benefit of the editor, to ensure that
44379
+ // any changes cause a re-rendering of the frame.
44380
+ ,
44381
+ key: location + this.props.alignment
44382
+ }, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
44383
+ style: a11y.srOnly
44384
+ }, this.context.strings.videoWrapper), /*#__PURE__*/React__namespace.createElement("iframe", {
44385
+ // TODO(joshuan): Consider not using iframes when we're
44386
+ // loading this from webapp. This iframe is problematic
44387
+ // for screenreaders.
44388
+ className: "perseus-video-widget",
44389
+ sandbox: "allow-same-origin allow-scripts",
44390
+ width: DEFAULT_WIDTH,
44391
+ height: DEFAULT_HEIGHT,
44392
+ src: url,
44393
+ allowFullScreen: true
44394
+ })), /*#__PURE__*/React__namespace.createElement(VideoTranscriptLink, {
44395
+ location: location
44396
+ }));
44256
44397
  }
44257
- var ops = dispatch(obj);
44258
- this._wrapped = ops.thaw ? ops.thaw(obj) : obj;
44259
- };
44260
- lens.prototype.freeze = function () {
44261
- var obj = this._wrapped;
44262
- var ops = dispatch(obj);
44263
- return ops.freeze ? ops.freeze(obj) : obj;
44398
+ }
44399
+ var Video$1 = {
44400
+ name: "video",
44401
+ displayName: "Video",
44402
+ defaultAlignment: "block",
44403
+ supportedAlignments: ["block", "float-left", "float-right", "full-width"],
44404
+ widget: Video
44264
44405
  };
44265
- lens.prototype.zoom = function (lensArr) {
44266
- if (this._zoomStack === undefined) {
44267
- this._zoomStack = [];
44406
+
44407
+ /* eslint-disable import/no-named-as-default */
44408
+ var extraWidgets = [Categorizer$1, CSProgram$1, Dropdown$1, Explanation$1, Definition$1, Grapher$1, GradedGroup$1, GradedGroupSet$1, Group$1, Iframe$1, Image$1, Interactive, InteractiveGraph$1, LabelImage$1, Matrix$1, Matcher$1, Measurer$1, Molecule$1, NumberLine$1, Orderer$1, Passage$1, PassageRef$1, PassageRefTarget$1, Plotter$1, PythonProgram$1, Sorter$1, Table$1, Video$1, DeprecatedStandin$1];
44409
+
44410
+ /**
44411
+ * This should be called by all clients, specifying whether extra widgets are
44412
+ * needed via `loadExtraWidgets`. It is idempotent, so it's not a problem to
44413
+ * call it multiple times.
44414
+ *
44415
+ * skipMathJax:
44416
+ * If false/undefined, MathJax will be configured, and the
44417
+ * promise will wait for MathJax to load (if it hasn't already).
44418
+ */
44419
+ const init = function (options) {
44420
+ registerWidgets(basicWidgets);
44421
+ registerWidgets(extraWidgets);
44422
+ replaceDeprecatedWidgets();
44423
+
44424
+ // Pass skipMathJax: true if MathJax is already loaded and configured.
44425
+ const skipMathJax = options.skipMathJax;
44426
+ if (skipMathJax) {
44427
+ // @ts-expect-error - TS2322 - Type 'Promise<void>' is not assignable to type 'Promise<undefined>'.
44428
+ return Promise.resolve();
44268
44429
  }
44269
- this._zoomStack.push({
44270
- zoom: lensArr,
44271
- wrapped: this._wrapped
44272
- });
44273
- this._wrapped = lens(this._wrapped).get(lensArr);
44274
- return this;
44275
- };
44276
- lens.prototype.deZoom = function () {
44277
- var frame = this._zoomStack.pop();
44278
- this._wrapped = lens(frame.wrapped).set(frame.zoom, this._wrapped).freeze();
44279
- return this;
44280
- };
44281
- lens.prototype.get = function (lensArr) {
44282
- var obj = this._wrapped;
44283
- for (var i = 0; i < lensArr.length; i++) {
44284
- obj = dispatch(obj).get(obj, lensArr[i]);
44285
- }
44286
- return obj;
44287
- };
44288
- lens.prototype.mod = function (lensArr, f) {
44289
- var obj = this._wrapped;
44290
- var newObj = clone(obj);
44291
- var ops = dispatch(obj);
44292
- if (lensArr.length === 0) {
44293
- this._wrapped = f(this._wrapped);
44294
- } else if (lensArr.length === 1) {
44295
- this._wrapped = ops.mod(newObj, lensArr[0], f);
44296
- } else {
44297
- var monocle = lensArr[0];
44298
- var shortLens = lensArr.slice(1);
44299
-
44300
- // newObj = ops.mod(obj[monocle], shortLens, f);
44301
-
44302
- newObj[monocle] = lens(obj[monocle]).mod(shortLens, f).freeze();
44303
- this._wrapped = newObj;
44304
- }
44305
- return this;
44306
- };
44307
-
44308
- // TODO - move to individual files
44309
- lens.prototype.merge = function (lensArr, props) {
44310
- this._wrapped = lens(this._wrapped).mod(lensArr, function (oldProps) {
44311
- return merge(oldProps, props);
44312
- }).freeze();
44313
- return this;
44314
- };
44315
-
44316
- // Lens must have length >= 1 or there would be nothing to return
44317
- lens.prototype.del = function (lensArr) {
44318
- var obj = this._wrapped;
44319
- var ops = dispatch(obj);
44320
- if (lensArr.length === 1) {
44321
- this._wrapped = ops.del(obj, lensArr[0]);
44322
- } else {
44323
- var monocle = lensArr[0];
44324
- var shortLens = lensArr.slice(1);
44325
- var newObj = clone(obj);
44326
- newObj[monocle] = lens(obj[monocle]).del(shortLens).freeze();
44327
- this._wrapped = newObj;
44328
- }
44329
- return this;
44330
- };
44331
- lens.prototype.set = function (lensArr, set) {
44332
- return this.mod(lensArr, function () {
44333
- return set;
44430
+ return new Promise((resolve, reject) => {
44431
+ MathJax.Hub.Config({
44432
+ messageStyle: "none",
44433
+ skipStartupTypeset: "none",
44434
+ "HTML-CSS": {
44435
+ availableFonts: ["TeX"],
44436
+ imageFont: null,
44437
+ scale: 100,
44438
+ showMathMenu: false
44439
+ }
44440
+ });
44441
+ MathJax.Hub.Configured();
44442
+ MathJax.Hub.Queue(resolve);
44334
44443
  });
44335
44444
  };
44336
44445
 
44337
- // ARRAY FUNCTIONS
44446
+ // This file is processed by a Rollup plugin (replace) to inject the production
44447
+ const libName = "@khanacademy/perseus";
44448
+ const libVersion = "27.0.0";
44449
+ perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
44338
44450
 
44339
- const arr = {
44340
- get: function (arr, monocle) {
44341
- return arr[monocle];
44342
- },
44343
- set: function (arr, monocle, val) {
44344
- var newArr = arr.splice();
44345
- newArr[monocle] = val;
44346
- return newArr;
44347
- },
44348
- mod: function (arr, monocle, f) {
44349
- var newArr = arr.slice();
44350
- newArr[monocle] = f(arr[monocle]);
44351
- return newArr;
44352
- },
44353
- del: function (arr, monocle) {
44354
- var newArr = arr.slice();
44355
- newArr.splice(monocle, 1);
44356
- return newArr;
44357
- }
44451
+ const apiVersion = {
44452
+ major: 12,
44453
+ minor: 0
44358
44454
  };
44359
-
44360
- /*
44361
- // Lens must point to a member of an array. We'll insert into that array.
44362
- lens.prototype.insertAt = function(lensArr, toInsert) {
44363
- var obj = this._wrapped;
44364
-
44365
- var arrLens = lensArr.slice(0, -1);
44366
- var arr = lens(obj).get(arrLens).slice(); // slice to copy
44367
-
44368
- var arrIdx = lensArr[lensArr.length-1];
44369
- arr.splice(arrIdx, 0, toInsert);
44370
- return lens(obj).set(arrLens, arr);
44455
+ const itemDataVersion = {
44456
+ major: 0,
44457
+ minor: 1
44371
44458
  };
44372
44459
 
44373
- lens.prototype.insertBefore = lens.prototype.insertAt;
44374
- lens.prototype.insertAfter = function(lensArr, toInsert) {
44375
- var newLens = lensArr.slice();
44376
- newLens[newLens.length-1] += 1;
44377
- return lens(this._wrapped).insertAt(newLens, toInsert);
44378
- };
44379
- */
44460
+ var allWidgets = [...basicWidgets, ...extraWidgets];
44380
44461
 
44381
- // OBJECT FUNCTIONS
44462
+ registerWidgets(allWidgets);
44463
+ const ItemVersion = getVersionVector();
44464
+ ItemVersion["::renderer::"] = itemDataVersion;
44382
44465
 
44383
- const obj = {
44384
- get: function (obj, monocle) {
44385
- return obj[monocle];
44386
- },
44387
- set: function (obj, monocle, val) {
44388
- var newObj = clone(obj);
44389
- newObj[monocle] = val;
44390
- return newObj;
44391
- },
44392
- mod: function (obj, monocle, f) {
44393
- var newObj = clone(obj);
44394
- newObj[monocle] = f(obj[monocle]);
44395
- return newObj;
44396
- },
44397
- del: function (obj, monocle) {
44398
- var newObj = clone(obj);
44399
- delete newObj[monocle];
44400
- return newObj;
44466
+ class ArticleRenderer extends React__namespace.Component {
44467
+ static contextType = PerseusI18nContext;
44468
+ _currentFocus;
44469
+ sectionRenderers = [];
44470
+ static defaultProps = {
44471
+ apiOptions: ApiOptions.defaults,
44472
+ useNewStyles: false,
44473
+ linterContext: PerseusLinter__namespace.linterContextDefault
44474
+ };
44475
+ constructor(props) {
44476
+ super(props);
44401
44477
  }
44402
- };
44403
-
44404
- // STRING FUNCTIONS
44405
-
44406
- const str = {
44407
- get: function (arr, monocle) {
44408
- return arr[monocle];
44409
- },
44410
- set: function (arr, monocle, val) {
44411
- var newArr = arr.splice();
44412
- newArr[monocle] = val;
44413
- return newArr;
44414
- },
44415
- mod: function (arr, monocle, f) {
44416
- var newArr = arr.splice();
44417
- newArr[monocle] = f(arr[monocle]);
44418
- return newArr;
44419
- },
44420
- del: function (arr, monocle) {
44421
- var newArr = arr.slice();
44422
- newArr.splice(monocle);
44423
- return newArr;
44478
+ componentDidMount() {
44479
+ this._currentFocus = null;
44424
44480
  }
44425
- };
44426
-
44427
- /* eslint-disable eqeqeq, @typescript-eslint/no-unused-vars */
44428
- /*
44429
- * A class for formatting number to significant digits.
44430
- * Copyright (C) 2001 Stephen Ostermiller
44431
- * http://ostermiller.org/contact.pl?regarding?JavaScript+Significant+Figures
44432
- *
44433
- * This program is free software; you can redistribute it and/or modify
44434
- * it under the terms of the GNU General Public License as published by
44435
- * the Free Software Foundation; either version 2 of the License, or
44436
- * (at your option) any later version.
44437
- *
44438
- * This program is distributed in the hope that it will be useful,
44439
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
44440
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
44441
- * GNU General Public License for more details.
44442
- */
44481
+ shouldComponentUpdate(nextProps) {
44482
+ return nextProps !== this.props;
44483
+ }
44484
+ _handleFocusChange = (newFocusPath, oldFocusPath) => {
44485
+ // TODO(charlie): DRY this up--some of this logic is repeated in
44486
+ // ItemRenderer.
44487
+ if (newFocusPath) {
44488
+ this._setCurrentFocus(newFocusPath);
44489
+ } else {
44490
+ this._onRendererBlur(oldFocusPath);
44491
+ }
44492
+ };
44493
+ _setCurrentFocus = newFocusPath => {
44494
+ const {
44495
+ keypadElement,
44496
+ apiOptions
44497
+ } = this.props;
44498
+ const {
44499
+ isMobile
44500
+ } = apiOptions;
44501
+ const prevFocusPath = this._currentFocus;
44502
+ this._currentFocus = newFocusPath;
44443
44503
 
44444
- /**
44445
- * An object tho both scans floating points to
44446
- * determine the number of significant figures.
44447
- * and can display a floating point using any number
44448
- * of significant figures.
44449
- *
44450
- * @param s A string representation of a floating point.
44451
- */
44452
- class SignificantFigures {
44453
- order;
44454
- mantissa;
44455
- positive;
44456
- sigFigs;
44457
- sigDecs;
44458
- constructor(s) {
44459
- this.order = parseOrder(s);
44460
- this.mantissa = parseMantissa(s);
44461
- this.positive = parseSign(s);
44504
+ // Use the section prefix to extract the relevant Renderer's input
44505
+ // paths, so as to check whether the focused path represents an
44506
+ // input.
44507
+ let didFocusInput = false;
44508
+ let focusedInput;
44509
+ if (this._currentFocus) {
44510
+ const [sectionIndex, ...focusPath] = this._currentFocus;
44511
+ const inputPaths = this.sectionRenderers[sectionIndex].getInputPaths();
44512
+ didFocusInput = inputPaths.some(inputPath => {
44513
+ return Util.inputPathsEqual(inputPath, focusPath);
44514
+ });
44515
+ focusedInput = this.sectionRenderers[sectionIndex].getDOMNodeForPath(focusPath);
44516
+ }
44517
+ const {
44518
+ onFocusChange
44519
+ } = this.props.apiOptions;
44520
+ if (onFocusChange) {
44521
+ // Wait for the keypad to mount before getting the height
44522
+ setTimeout(() => {
44523
+ const keypadDomNode = keypadElement?.getDOMNode();
44524
+ const keypadHeight = keypadDomNode && didFocusInput ? keypadDomNode.getBoundingClientRect().height : 0;
44525
+ onFocusChange(this._currentFocus, prevFocusPath, keypadHeight, didFocusInput ? focusedInput : null);
44526
+ }, 0);
44527
+ }
44528
+ if (keypadElement && isMobile) {
44529
+ if (didFocusInput) {
44530
+ keypadElement.activate();
44531
+ } else {
44532
+ keypadElement.dismiss();
44533
+ }
44534
+ }
44535
+ };
44536
+ _onRendererBlur = blurPath => {
44537
+ const blurringFocusPath = this._currentFocus;
44462
44538
 
44463
- /**
44464
- * Get the number of significant figures this object uses.
44465
- * Leading zeros are not significant. Traling zeros up to
44466
- * and after the decimal point are significant.
44467
- * Significant figures is good to know when the number is
44468
- * used in multiplication.
44469
- *
44470
- * @return the number of significant figures.
44471
- */
44472
- this.sigFigs = this.mantissa.length;
44473
-
44474
- /**
44475
- * Get the least significant decimal this object uses.
44476
- * This is useful to know if a number is being used
44477
- * in addition.
44478
- * 400 - 2 the hundreds place (10^2) is the least significant decimal.
44479
- * 75 - 0 the ones place (10^0) is the least significant decimal.
44480
- * .543 - -3 the 1/1000's place (10^-3) is the least significant decimal.
44481
- *
44482
- * @return an integer representing the least significant decimal place.
44483
- */
44484
- this.sigDecs = this.order - this.mantissa.length;
44485
- }
44486
- }
44487
-
44488
- /**
44489
- * Format a floating point for display using the specified
44490
- * number of significant figures and least significant decimal.
44491
- * Scientific notation may used by this method if this
44492
- * object is very small, very large, has many significant
44493
- * figures, or the number of significant figures would be
44494
- * ambiguous in the output if scientific notation were not
44495
- * used.
44496
- *
44497
- * @param f A floating point number that should be displayed
44498
- * @param sigFigs desired number of significant figures (integer).
44499
- * @param sigDecs the least significant decimal place (integer).
44500
- * @param scientific true iff scientific notation should always be used.
44501
- * @return a string of this object formatted correctly.
44502
- */
44503
- function displaySigFigs(f, sigFigs, sigDecs, scientific) {
44504
- const s = "" + f;
44505
- let order = parseOrder(s);
44506
- let mantissa = parseMantissa(s);
44507
- let positive = parseSign(s);
44508
- let add;
44509
- let decAdd;
44510
- let sigAdd;
44511
- let zeroScientific = false;
44512
- if (f == 0 || mantissa == "" || mantissa == "0") {
44513
- mantissa = "";
44514
- for (let i = 0; i < sigFigs; i++) {
44515
- mantissa += "0";
44516
- }
44517
- order = sigFigs + sigDecs;
44518
- if (sigDecs < 0 && -sigDecs >= sigFigs) {
44519
- zeroScientific = true;
44520
- }
44521
- } else {
44522
- decAdd = order - mantissa.length - sigDecs;
44523
- sigAdd = sigFigs - mantissa.length;
44524
- add = Math.min(sigAdd, decAdd);
44525
- if (add < 0) {
44526
- let rounded = round(mantissa, -add);
44527
- if (rounded.length > mantissa.length + add) {
44528
- order++;
44529
- if (decAdd > sigAdd) {
44530
- rounded = round(rounded, 1);
44531
- }
44532
- }
44533
- mantissa = rounded;
44534
- } else if (add > 0) {
44535
- for (let i = 0; i < add; i++) {
44536
- mantissa += "0";
44537
- }
44538
- }
44539
- if (mantissa == "" || mantissa == "0") {
44540
- mantissa = "0";
44541
- positive = true;
44542
- order = 1 + sigDecs;
44543
- if (order != 0) {
44544
- zeroScientific = true;
44545
- }
44546
- }
44547
- }
44548
- const useScientific = scientific || mantissa.length > 20 || order > 4 || order < -2 || order - mantissa.length > 0 && trailingZeros(mantissa) > 0 || zeroScientific;
44549
- let returnVal = "";
44550
- if (!positive) {
44551
- returnVal += "-";
44552
- }
44553
- if (useScientific) {
44554
- returnVal += mantissa.charAt(0);
44555
- if (mantissa.length > 1) {
44556
- returnVal += "." + mantissa.substring(1, mantissa.length);
44557
- }
44558
- if (order - 1 != 0) {
44559
- returnVal += " x 10^" + (order - 1);
44560
- }
44561
- } else {
44562
- let wholePart = "";
44563
- let fractPart = "";
44564
- let needDot = true;
44565
- if (order > 0) {
44566
- if (mantissa.length > order) {
44567
- wholePart = mantissa.substring(0, order);
44568
- fractPart = mantissa.substring(order, mantissa.length);
44569
- } else {
44570
- wholePart = mantissa;
44571
- needDot = trailingZeros(mantissa) != 0;
44572
- for (let i = 0; i < order - mantissa.length; i++) {
44573
- wholePart += "0";
44574
- }
44575
- }
44576
- } else {
44577
- for (let i = 0; i < -order; i++) {
44578
- fractPart += "0";
44579
- }
44580
- fractPart += mantissa;
44581
- }
44582
- returnVal += (wholePart == "" ? "0" : wholePart) + (needDot ? "." : "") + fractPart;
44583
- }
44584
- return returnVal;
44585
- }
44586
-
44587
- /**
44588
- * Count the significant trailing zeros on this object.
44589
- *
44590
- * @return the number of trailing zeros
44591
- */
44592
- function trailingZeros(mantissa) {
44593
- let zeros = 0;
44594
- for (let i = mantissa.length - 1; i >= 0; i--) {
44595
- const c = mantissa.charAt(i);
44596
- if (c == "0") {
44597
- zeros++;
44598
- } else {
44599
- return zeros;
44600
- }
44601
- }
44602
- return zeros;
44603
- }
44604
-
44605
- /**
44606
- * Parse a string representation of a floating point
44607
- * and pull out the sign.
44608
- *
44609
- * @param s the string representation of a floating point.
44610
- * @return true iff this is a positive number
44611
- */
44612
- function parseSign(s) {
44613
- let beginning = true;
44614
- let seenDot = false;
44615
- let seenSomething = false;
44616
- let zeros = "";
44617
- let all = "";
44618
- let pos = true;
44619
- for (let i = 0; i < s.length; i++) {
44620
- const c = s.charAt(i);
44621
- if (c >= "1" && c <= "9") {
44622
- all += zeros + c;
44623
- zeros = "";
44624
- seenSomething = true;
44625
- beginning = false;
44626
- } else if (c == "0") {
44627
- if (seenDot) {
44628
- if (seenSomething) {
44629
- all += zeros + c;
44630
- zeros = "";
44631
- }
44632
- } else {
44633
- if (seenSomething) {
44634
- zeros += c;
44635
- }
44636
- }
44637
- beginning = false;
44638
- } else if (!seenDot && c == ".") {
44639
- all += zeros;
44640
- zeros = "";
44641
- seenDot = true;
44642
- beginning = false;
44643
- } else if (c == "e" || c == "E" && i + 1 < s.length) {
44644
- parseInt(s.substring(i + 1, s.length));
44645
- i = s.length;
44646
- } else if (beginning && (c == "+" || c == "-")) {
44647
- if (c == "-") {
44648
- pos = !pos;
44649
- }
44650
- }
44651
- }
44652
- if (all == "") {
44653
- return true;
44654
- }
44655
- return pos;
44656
- }
44657
-
44658
- /**
44659
- * Parse a string representation of a floating point
44660
- * and pull out the mantissa.
44661
- *
44662
- * @param s the string representation of a floating point.
44663
- * @return the mantissa of this number.
44664
- */
44665
- function parseMantissa(s) {
44666
- let seenDot = false;
44667
- let seenSomething = false;
44668
- let zeros = "";
44669
- let leadZeros = "";
44670
- let all = "";
44671
- for (let i = 0; i < s.length; i++) {
44672
- const c = s.charAt(i);
44673
- if (c >= "1" && c <= "9") {
44674
- all += zeros + c;
44675
- zeros = "";
44676
- seenSomething = true;
44677
- } else if (c == "0") {
44678
- if (seenDot) {
44679
- if (seenSomething) {
44680
- all += zeros + c;
44681
- zeros = "";
44682
- } else {
44683
- leadZeros += c;
44684
- }
44685
- } else {
44686
- if (seenSomething) {
44687
- leadZeros += c;
44688
- zeros += c;
44689
- } else {
44690
- leadZeros += c;
44691
- }
44692
- }
44693
- } else if (!seenDot && c == ".") {
44694
- all += zeros;
44695
- zeros = "";
44696
- seenDot = true;
44697
- } else if (c == "e" || c == "E" && i + 1 < s.length) {
44698
- parseInt(s.substring(i + 1, s.length));
44699
- i = s.length;
44700
- } else ;
44701
- }
44702
- if (all == "") {
44703
- return leadZeros;
44704
- }
44705
- return all;
44706
- }
44707
-
44708
- /**
44709
- * Parse a string representation of a floating point
44710
- * and pull out the exponent.
44711
- *
44712
- * @param s the string representation of a floating point.
44713
- * @return (integer) the number after the e.
44714
- */
44715
- function parseOrder(s) {
44716
- let seenDot = false;
44717
- let seenSomething = false;
44718
- let zeros = "";
44719
- let all = "";
44720
- let decPlaces = 0;
44721
- let totalDecs = 0;
44722
- for (let i = 0; i < s.length; i++) {
44723
- const c = s.charAt(i);
44724
- if (c >= "1" && c <= "9") {
44725
- all += zeros + c;
44726
- zeros = "";
44727
- seenSomething = true;
44728
- if (!seenDot) {
44729
- totalDecs++;
44730
- decPlaces++;
44731
- }
44732
- } else if (c == "0") {
44733
- if (seenDot) {
44734
- if (seenSomething) {
44735
- all += zeros + c;
44736
- zeros = "";
44737
- } else {
44738
- decPlaces--;
44739
- }
44740
- } else {
44741
- totalDecs++;
44742
- if (seenSomething) {
44743
- decPlaces++;
44744
- zeros += c;
44745
- }
44746
- }
44747
- } else if (!seenDot && c == ".") {
44748
- all += zeros;
44749
- zeros = "";
44750
- seenDot = true;
44751
- } else if (c == "e" || c == "E" && i + 1 < s.length) {
44752
- const raised = parseInt(s.substring(i + 1, s.length));
44753
- decPlaces += raised;
44754
- totalDecs += raised;
44755
- i = s.length;
44756
- } else ;
44757
- }
44758
- if (all == "") {
44759
- return totalDecs;
44760
- }
44761
- return decPlaces;
44762
- }
44763
-
44764
- /**
44765
- * Remove the specified number of digits from string by
44766
- * rounding. Proper rounding rules for scientific purposes
44767
- * are followed.
44768
- * This method may cause an extra significant figure
44769
- * to be added to the number. For example, if 999999
44770
- * were rounded, A one would carry over and become
44771
- * a significant figure. Those who call this method
44772
- * should check for this and call round again if needed.
44773
- *
44774
- * @param mantissa A string representing an whole number of arbitrary length.
44775
- * @param digits A number of digits to remove
44776
- * @return A string represted the rounded version of mantissa
44777
- */
44778
- function round(mantissa, digits) {
44779
- const last = mantissa.length - digits - 1;
44780
- if (last < 0) {
44781
- return "";
44782
- }
44783
- if (last >= mantissa.length - 1) {
44784
- return mantissa;
44785
- }
44786
- const nextToLast = mantissa.charAt(last + 1);
44787
- const lastChar = mantissa.charAt(last);
44788
- let roundUp = false;
44789
- if (nextToLast > "5") {
44790
- roundUp = true;
44791
- } else if (nextToLast == "5") {
44792
- for (let j = last + 2; j < mantissa.length; j++) {
44793
- if (mantissa.charAt(j) != "0") {
44794
- roundUp = true;
44795
- }
44796
- }
44797
- // @ts-expect-error - TS2362 - The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
44798
- if (lastChar % 2 == 1) {
44799
- roundUp = true;
44800
- }
44801
- }
44802
- let result = "";
44803
- for (let i = last; i >= 0; i--) {
44804
- const c = mantissa.charAt(i);
44805
- if (roundUp) {
44806
- let nextChar;
44807
- if (c == "9") {
44808
- nextChar = "0";
44809
- } else {
44810
- switch (c) {
44811
- case "0":
44812
- nextChar = "1";
44813
- break;
44814
- case "1":
44815
- nextChar = "2";
44816
- break;
44817
- case "2":
44818
- nextChar = "3";
44819
- break;
44820
- case "3":
44821
- nextChar = "4";
44822
- break;
44823
- case "4":
44824
- nextChar = "5";
44825
- break;
44826
- case "5":
44827
- nextChar = "6";
44828
- break;
44829
- case "6":
44830
- nextChar = "7";
44831
- break;
44832
- case "7":
44833
- nextChar = "8";
44834
- break;
44835
- case "8":
44836
- nextChar = "9";
44837
- break;
44838
- }
44839
- roundUp = false;
44840
- }
44841
- result = nextChar + result;
44842
- } else {
44843
- result = c + result;
44844
- }
44845
- }
44846
- if (roundUp) {
44847
- result = "1" + result;
44848
- }
44849
- return result;
44850
- }
44851
-
44852
- // TODO(joel): teach KAS how to accept an answer only if it's expressed in
44853
- const ALL = "all";
44854
- const countSigfigs = function (value) {
44855
- return new SignificantFigures(value).sigFigs;
44856
- };
44857
- /* I just wrote this, but it's old by analogy to `OldExpression`, in that it's
44858
- * the version that non-mathquill platforms get stuck with. Constructed with an
44859
- * <input>, a parser, popsicle sticks, and glue.
44860
- *
44861
- * In the same way as OldExpression, this parses continuously as you type, then
44862
- * shows and hides an error buddy. The error message is only shown after a
44863
- * rolling two second delay, but hidden immediately on further typing.
44864
- */
44865
- class OldUnitInput extends React__namespace.Component {
44866
- static contextType = PerseusI18nContext;
44867
- _errorTimeout;
44868
- static defaultProps = {
44869
- apiOptions: ApiOptions.defaults,
44870
- value: ""
44871
- };
44872
- static validate(userInput, rubric, strings) {
44873
- const answer = KAS__namespace.unitParse(rubric.value).expr;
44874
- const guess = KAS__namespace.unitParse(userInput);
44875
- if (!guess.parsed) {
44876
- return {
44877
- type: "invalid",
44878
- message: strings.dontUnderstandUnits
44879
- };
44880
- }
44881
-
44882
- // Note: we check sigfigs, then numerical correctness, then units, so
44883
- // the most significant things come last, that way the user will see
44884
- // the most important message.
44885
- let message = null;
44886
-
44887
- // did the user specify the right number of sigfigs?
44888
- // TODO(joel) - add a grading mode where the wrong number of sigfigs
44889
- // isn't marked wrong
44890
- const sigfigs = rubric.sigfigs;
44891
- const sigfigsCorrect = countSigfigs(guess.coefficient) === sigfigs;
44892
- if (!sigfigsCorrect) {
44893
- message = strings.checkSigFigs;
44894
- }
44895
-
44896
- // now we need to check that the answer is correct to the precision we
44897
- // require.
44898
- let numericallyCorrect;
44899
- try {
44900
- const x = new KAS__namespace.Var("x");
44901
- const equality = new KAS__namespace.Eq(answer.simplify(), "=", new KAS__namespace.Mul(x, guess.expr.simplify()));
44902
- const conversion = equality.solveLinearEquationForVariable(x);
44903
-
44904
- // Make sure the conversion factor between the user's input answer
44905
- // and the canonical answer is 1, to sigfig places.
44906
- // TODO(joel) is this sound?
44907
- numericallyCorrect = Number(conversion.eval()).toPrecision(sigfigs) === Number(1).toPrecision(sigfigs);
44908
- } catch (e) {
44909
- numericallyCorrect = false;
44910
- }
44911
- if (!numericallyCorrect) {
44912
- message = strings.answerNumericallyIncorrect;
44913
- }
44914
- let kasCorrect;
44915
- const guessUnit = primUnits(guess.expr.simplify());
44916
- const answerUnit = primUnits(answer.simplify());
44917
- if (rubric.accepting === ALL) {
44918
- // We're accepting all units - KAS does the hard work of figuring
44919
- // out if the user's unit is equivalent to the author's unit.
44920
- kasCorrect = KAS__namespace.compare(guessUnit, answerUnit).equal;
44921
- } else {
44922
- // Are any of the accepted units the same as what the user entered?
44923
- kasCorrect = ___default["default"](rubric.acceptingUnits).any(unit => {
44924
- const thisAnswerUnit = primUnits(KAS__namespace.unitParse(unit).unit.simplify());
44925
- return KAS__namespace.compare(thisAnswerUnit, guessUnit
44926
- // TODO(joel) - make this work as intended.
44927
- // { form: true }
44928
- ).equal;
44929
- });
44930
- }
44931
- if (!kasCorrect) {
44932
- message = strings.checkUnits;
44933
- }
44934
- const correct = kasCorrect && numericallyCorrect && sigfigsCorrect;
44935
- return {
44936
- type: "points",
44937
- earned: correct ? 1 : 0,
44938
- total: 1,
44939
- message
44940
- };
44941
- }
44942
- componentDidUpdate() {
44943
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
44944
- // eslint-disable-next-line no-restricted-syntax
44945
- // @ts-expect-error - TS2769 - No overload matches this call.
44946
- clearTimeout(this._errorTimeout);
44947
- if (KAS__namespace.unitParse(this.props.value).parsed) {
44948
- this._hideError();
44949
- } else {
44950
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
44951
- // eslint-disable-next-line no-restricted-syntax
44952
- // @ts-expect-error - TS2322 - Type 'Timeout' is not assignable to type 'number'.
44953
- this._errorTimeout = setTimeout(this._showError, 2000);
44954
- }
44955
- }
44956
- componentWillUnmount() {
44957
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
44958
- // eslint-disable-next-line no-restricted-syntax
44959
- // @ts-expect-error - TS2769 - No overload matches this call.
44960
- clearTimeout(this._errorTimeout);
44961
- }
44962
- _showError = () => {
44963
- if (this.props.value === "") {
44964
- return;
44965
- }
44966
-
44967
- // eslint-disable-next-line react/no-string-refs
44968
- // @ts-expect-error - TS2769 - No overload matches this call.
44969
- const $error = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.error));
44970
- // @ts-expect-error - TS2339 - Property 'is' does not exist on type 'JQueryStatic'.
44971
- if (!$error.is(":visible")) {
44972
- $error
44973
- // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
44974
- .css({
44975
- top: 50,
44976
- opacity: 0.1
44977
- }).show().animate({
44978
- top: 0,
44979
- opacity: 1.0
44980
- }, 300);
44981
- }
44982
- };
44983
- _hideError = () => {
44984
- // eslint-disable-next-line react/no-string-refs
44985
- // @ts-expect-error - TS2769 - No overload matches this call.
44986
- const $error = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.error));
44987
- // @ts-expect-error - TS2339 - Property 'is' does not exist on type 'JQueryStatic'.
44988
- if ($error.is(":visible")) {
44989
- // @ts-expect-error - TS2339 - Property 'animate' does not exist on type 'JQueryStatic'.
44990
- $error.animate({
44991
- top: 50,
44992
- opacity: 0.1
44993
- }, 300, function () {
44994
- // eslint-disable-next-line @babel/no-invalid-this
44995
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
44996
- // eslint-disable-next-line @babel/no-invalid-this
44997
- $__default["default"](this).hide();
44998
- });
44999
- }
45000
- };
45001
- change = (() => {
45002
- var _this = this;
45003
- return function () {
45004
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
45005
- args[_key] = arguments[_key];
45006
- }
45007
- // @ts-expect-error - TS2345 - Argument of type 'readonly any[]' is not assignable to parameter of type 'any[]'.
45008
- return change.apply(_this, args);
45009
- };
45010
- })();
45011
- handleBlur = () => {
45012
- this.props.onBlur([]);
45013
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
45014
- // eslint-disable-next-line no-restricted-syntax
45015
- // @ts-expect-error - TS2769 - No overload matches this call.
45016
- clearTimeout(this._errorTimeout);
45017
- if (!KAS__namespace.unitParse(this.props.value).parsed) {
45018
- this._showError();
45019
- }
45020
- };
45021
- handleChange = event => {
45022
- this._hideError();
45023
- this.props.onChange({
45024
- value: event.target.value
45025
- });
45026
- };
45027
- simpleValidate = (rubric, onInputError) => {
45028
- return OldUnitInput.validate(this.getUserInput(), rubric, this.context.strings);
45029
- };
45030
- getUserInput = () => {
45031
- return this.props.value;
45032
- };
45033
-
45034
- // begin mobile stuff
45035
-
45036
- getInputPaths = () => {
45037
- // The widget itself is an input, so we return a single empty list to
45038
- // indicate this.
45039
- return [[]];
45040
- };
45041
- focusInputPath = inputPath => {
45042
- // eslint-disable-next-line react/no-string-refs
45043
- const input = ReactDOM__default["default"].findDOMNode(this.refs.input);
45044
- if (input) {
45045
- /**
45046
- * TODO(somewhatabstract, JIRA-XXXX):
45047
- * Change to using a ref callback so that focus() can be
45048
- * accessed.
45049
- */
45050
- // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'Element | Text'.
45051
- input.focus();
45052
- }
45053
- };
45054
- handleFocus = () => {
45055
- this.props.onFocus([]);
45056
- };
45057
- blurInputPath = inputPath => {
45058
- // eslint-disable-next-line react/no-string-refs
45059
- const input = ReactDOM__default["default"].findDOMNode(this.refs.input);
45060
- if (input) {
45061
- /**
45062
- * TODO(somewhatabstract, JIRA-XXXX):
45063
- * Change to using a ref callback so that focus() can be
45064
- * accessed.
45065
- */
45066
- // @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'Element | Text'.
45067
- input.blur();
45068
- }
45069
- };
45070
- setInputValue = (path, newValue, cb) => {
45071
- this.props.onChange({
45072
- value: newValue
45073
- }, cb);
45074
- };
45075
- getDOMNodeForPath = () => {
45076
- // eslint-disable-next-line react/no-string-refs
45077
- return ReactDOM__default["default"].findDOMNode(this.refs.input);
45078
- };
45079
- getGrammarTypeForPath = inputPath => {
45080
- return "unit";
45081
- };
45082
-
45083
- // end mobile stuff
45084
-
45085
- // TODO(joel) think about showing the error buddy
45086
- render() {
45087
- return /*#__PURE__*/React__namespace.createElement("div", {
45088
- className: "old-unit-input"
45089
- }, /*#__PURE__*/React__namespace.createElement("input", {
45090
- onChange: this.handleChange,
45091
- ref: "input" // eslint-disable-line react/no-string-refs
45092
- ,
45093
- className: ClassNames.INTERACTIVE,
45094
- value: this.props.value,
45095
- onFocus: this.handleFocus,
45096
- onBlur: this.handleBlur
45097
- }), /*#__PURE__*/React__namespace.createElement("div", {
45098
- ref: "error",
45099
- className: "error",
45100
- style: {
45101
- display: "none"
45102
- }
45103
- }, this.context.strings.dontUnderstand));
45104
- }
45105
- }
45106
-
45107
- // Extract the primitive units from a unit expression. This first simplifies
45108
- // `expr` to a `Mul` like "5 kg m / s^2" then removes the first term.
45109
- const primUnits = function (expr) {
45110
- return expr.simplify().asMul().partition()[1].flatten().simplify();
45111
- };
45112
- var Unit = {
45113
- name: "unit-input",
45114
- displayName: "Unit input",
45115
- defaultAlignment: "inline-block",
45116
- widget: OldUnitInput,
45117
- transform: x => lens(x).del(["value"]).freeze(),
45118
- version: {
45119
- major: 0,
45120
- minor: 1
45121
- },
45122
- hidden: true
45123
- };
45124
-
45125
- /**
45126
- * This is a component that adds a link to the transcript for embedded videos in articles.
45127
- */
45128
- const IS_URL$1 = /^https?:\/\//;
45129
- const getYoutubeId = url => {
45130
- const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
45131
- const match = url.match(regExp);
45132
- if (match && match[7].length === 11) {
45133
- return match[7];
45134
- }
45135
- return "videoNotFound";
45136
- };
45137
-
45138
- // will accept a youtube link or a video's slug name from embedded video and then query accordingly.
45139
-
45140
- /**
45141
- * Video Transcript Link Component.
45142
- */
45143
- const VideoTranscriptLink = props => {
45144
- const {
45145
- location
45146
- } = props;
45147
- const {
45148
- useVideo
45149
- } = getDependencies();
45150
- const [id, kind] = IS_URL$1.test(location) ? [getYoutubeId(location), "YOUTUBE_ID"] : [location, "READABLE_ID"];
45151
- // The result value conforms to the wonder-blocks-data `Result` type
45152
- // which is used by our GraphQL framework.
45153
- // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'VideoKind'.
45154
- const result = useVideo(id, kind);
45155
- const {
45156
- strings
45157
- } = usePerseusI18n();
45158
- switch (result.status) {
45159
- case "loading":
45160
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.loading);
45161
- case "success":
45162
- {
45163
- const video = result.data?.video;
45164
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
45165
- style: styles$4.transcriptLink
45166
- }, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.Text, null, video?.title), /*#__PURE__*/React__namespace.createElement(wonderBlocksLayout.Strut, {
45167
- size: 10
45168
- }), /*#__PURE__*/React__namespace.createElement(Link__default["default"], {
45169
- href: "/transcript/" + (video?.contentId || "videoNotFound"),
45170
- target: "_blank",
45171
- className: "visited-no-recolor"
45172
- }, strings.videoTranscript));
45173
- }
45174
- case "error":
45175
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.somethingWrong);
45176
- case "aborted":
45177
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.somethingWrong);
45178
- default:
45179
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, strings.somethingWrong);
45180
- }
45181
- };
45182
- const styles$4 = aphrodite.StyleSheet.create({
45183
- transcriptLink: {
45184
- flexDirection: "row",
45185
- width: "100%",
45186
- justifyContent: "center"
45187
- }
45188
- });
45189
-
45190
- /**
45191
- * This is a video widget for embedding videos in articles.
45192
- */
45193
- // Current default is 720p, based on the typical videos we upload currently
45194
- const DEFAULT_WIDTH = 1280;
45195
- const DEFAULT_HEIGHT = 720;
45196
- const KA_EMBED = "{host}/embed_video?slug={slug}" + "&internal_video_only=1";
45197
- const IS_URL = /^https?:\/\//;
45198
- const IS_KA_SITE = /(khanacademy\.org|localhost)/;
45199
- const IS_VIMEO = /(vimeo\.com)/;
45200
-
45201
- // exports has no 'transform'
45202
-
45203
- /**
45204
- * Video renderer.
45205
- */
45206
- class Video extends React__namespace.Component {
45207
- static contextType = PerseusI18nContext;
45208
- /**
45209
- * This is the widget's grading function.
45210
- * Points for videos are tallied by the embedded video itself, in the case
45211
- * of Khan Academy videos.
45212
- */
45213
- static validate(userInput, rubric) {
45214
- return {
45215
- type: "points",
45216
- earned: 0,
45217
- total: 0,
45218
- message: null
45219
- };
45220
- }
45221
- getUserInput = () => {
45222
- return null;
45223
- };
45224
- simpleValidate = rubric => {
45225
- return Video.validate(null, rubric);
45226
- };
45227
- change = (() => {
45228
- var _this = this;
45229
- return function () {
45230
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
45231
- args[_key] = arguments[_key];
45232
- }
45233
- // @ts-expect-error - TS2345 - Argument of type 'readonly unknown[]' is not assignable to parameter of type 'any[]'.
45234
- return change.apply(_this, args);
45235
- };
45236
- })();
45237
- render() {
45238
- const {
45239
- InitialRequestUrl
45240
- } = getDependencies();
45241
- const location = this.props.location;
45242
- if (!location) {
45243
- return /*#__PURE__*/React__namespace.createElement("div", null);
45244
- }
45245
- let url;
45246
- if (IS_URL.test(location)) {
45247
- url = location;
45248
- if (IS_VIMEO.test(url)) {
45249
- // If this is a vimeo video, we need to add the query string
45250
- // parameter "dnt" so that analytics/tracking cookies aren't set.
45251
- // https://help.vimeo.com/hc/en-us/articles/12426260232977-Player-parameters-overview
45252
- if (url.indexOf("?") === -1) {
45253
- url += "?dnt=1";
45254
- } else {
45255
- url += "&dnt=1";
45256
- }
45257
- }
45258
- } else {
45259
- url = KA_EMBED.replace("{slug}", location);
45260
- let embedHostname = "https://www.khanacademy.org";
45261
- if (IS_KA_SITE.test(InitialRequestUrl.host)) {
45262
- embedHostname = InitialRequestUrl.origin;
45263
- }
45264
- url = url.replace("{host}", embedHostname);
45265
- }
45266
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, /*#__PURE__*/React__namespace.createElement(FixedToResponsive, {
45267
- width: DEFAULT_WIDTH,
45268
- height: DEFAULT_HEIGHT
45269
- // The key is here for the benefit of the editor, to ensure that
45270
- // any changes cause a re-rendering of the frame.
45271
- ,
45272
- key: location + this.props.alignment
45273
- }, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
45274
- style: a11y.srOnly
45275
- }, this.context.strings.videoWrapper), /*#__PURE__*/React__namespace.createElement("iframe", {
45276
- // TODO(joshuan): Consider not using iframes when we're
45277
- // loading this from webapp. This iframe is problematic
45278
- // for screenreaders.
45279
- className: "perseus-video-widget",
45280
- sandbox: "allow-same-origin allow-scripts",
45281
- width: DEFAULT_WIDTH,
45282
- height: DEFAULT_HEIGHT,
45283
- src: url,
45284
- allowFullScreen: true
45285
- })), /*#__PURE__*/React__namespace.createElement(VideoTranscriptLink, {
45286
- location: location
45287
- }));
45288
- }
45289
- }
45290
- var Video$1 = {
45291
- name: "video",
45292
- displayName: "Video",
45293
- defaultAlignment: "block",
45294
- supportedAlignments: ["block", "float-left", "float-right", "full-width"],
45295
- widget: Video
45296
- };
45297
-
45298
- /* eslint-disable import/no-named-as-default */
45299
- var extraWidgets = [Categorizer$1, CSProgram$1, Dropdown$1, Explanation$1, Definition$1, Grapher$1, GradedGroup$1, GradedGroupSet$1, Group$1, Iframe$1, Image$1, Interactive, InteractiveGraph$1, LabelImage$1, Matrix$1, Matcher$1, Measurer$1, Molecule$1, NumberLine$1, Orderer$1, Passage$1, PassageRef$1, PassageRefTarget$1, Plotter$1, PythonProgram$1, Sorter$1, Table$1, Unit, Video$1, DeprecatedStandin$1];
45300
-
45301
- /**
45302
- * This should be called by all clients, specifying whether extra widgets are
45303
- * needed via `loadExtraWidgets`. It is idempotent, so it's not a problem to
45304
- * call it multiple times.
45305
- *
45306
- * skipMathJax:
45307
- * If false/undefined, MathJax will be configured, and the
45308
- * promise will wait for MathJax to load (if it hasn't already).
45309
- */
45310
- const init = function (options) {
45311
- registerWidgets(basicWidgets);
45312
- registerWidgets(extraWidgets);
45313
- replaceDeprecatedWidgets();
45314
-
45315
- // Pass skipMathJax: true if MathJax is already loaded and configured.
45316
- const skipMathJax = options.skipMathJax;
45317
- if (skipMathJax) {
45318
- // @ts-expect-error - TS2322 - Type 'Promise<void>' is not assignable to type 'Promise<undefined>'.
45319
- return Promise.resolve();
45320
- }
45321
- return new Promise((resolve, reject) => {
45322
- MathJax.Hub.Config({
45323
- messageStyle: "none",
45324
- skipStartupTypeset: "none",
45325
- "HTML-CSS": {
45326
- availableFonts: ["TeX"],
45327
- imageFont: null,
45328
- scale: 100,
45329
- showMathMenu: false
45330
- }
45331
- });
45332
- MathJax.Hub.Configured();
45333
- MathJax.Hub.Queue(resolve);
45334
- });
45335
- };
45336
-
45337
- // This file is processed by a Rollup plugin (replace) to inject the production
45338
- const libName = "@khanacademy/perseus";
45339
- const libVersion = "26.1.0";
45340
- perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45341
-
45342
- const apiVersion = {
45343
- major: 12,
45344
- minor: 0
45345
- };
45346
- const itemDataVersion = {
45347
- major: 0,
45348
- minor: 1
45349
- };
45350
-
45351
- var allWidgets = [...basicWidgets, ...extraWidgets];
45352
-
45353
- registerWidgets(allWidgets);
45354
- const ItemVersion = getVersionVector();
45355
- ItemVersion["::renderer::"] = itemDataVersion;
45356
-
45357
- class ArticleRenderer extends React__namespace.Component {
45358
- static contextType = PerseusI18nContext;
45359
- _currentFocus;
45360
- sectionRenderers = [];
45361
- static defaultProps = {
45362
- apiOptions: ApiOptions.defaults,
45363
- useNewStyles: false,
45364
- linterContext: PerseusLinter__namespace.linterContextDefault
45365
- };
45366
- constructor(props) {
45367
- super(props);
45368
- }
45369
- componentDidMount() {
45370
- this._currentFocus = null;
45371
- }
45372
- shouldComponentUpdate(nextProps) {
45373
- return nextProps !== this.props;
45374
- }
45375
- _handleFocusChange = (newFocusPath, oldFocusPath) => {
45376
- // TODO(charlie): DRY this up--some of this logic is repeated in
45377
- // ItemRenderer.
45378
- if (newFocusPath) {
45379
- this._setCurrentFocus(newFocusPath);
45380
- } else {
45381
- this._onRendererBlur(oldFocusPath);
45382
- }
45383
- };
45384
- _setCurrentFocus = newFocusPath => {
45385
- const {
45386
- keypadElement,
45387
- apiOptions
45388
- } = this.props;
45389
- const {
45390
- isMobile
45391
- } = apiOptions;
45392
- const prevFocusPath = this._currentFocus;
45393
- this._currentFocus = newFocusPath;
45394
-
45395
- // Use the section prefix to extract the relevant Renderer's input
45396
- // paths, so as to check whether the focused path represents an
45397
- // input.
45398
- let didFocusInput = false;
45399
- let focusedInput;
45400
- if (this._currentFocus) {
45401
- const [sectionIndex, ...focusPath] = this._currentFocus;
45402
- const inputPaths = this.sectionRenderers[sectionIndex].getInputPaths();
45403
- didFocusInput = inputPaths.some(inputPath => {
45404
- return Util.inputPathsEqual(inputPath, focusPath);
45405
- });
45406
- focusedInput = this.sectionRenderers[sectionIndex].getDOMNodeForPath(focusPath);
45407
- }
45408
- const {
45409
- onFocusChange
45410
- } = this.props.apiOptions;
45411
- if (onFocusChange) {
45412
- // Wait for the keypad to mount before getting the height
45413
- setTimeout(() => {
45414
- const keypadDomNode = keypadElement?.getDOMNode();
45415
- const keypadHeight = keypadDomNode && didFocusInput ? keypadDomNode.getBoundingClientRect().height : 0;
45416
- onFocusChange(this._currentFocus, prevFocusPath, keypadHeight, didFocusInput ? focusedInput : null);
45417
- }, 0);
45418
- }
45419
- if (keypadElement && isMobile) {
45420
- if (didFocusInput) {
45421
- keypadElement.activate();
45422
- } else {
45423
- keypadElement.dismiss();
45424
- }
45425
- }
45426
- };
45427
- _onRendererBlur = blurPath => {
45428
- const blurringFocusPath = this._currentFocus;
45429
-
45430
- // Failsafe: abort if ID is different, because focus probably happened
45431
- // before blur.
45432
- if (!Util.inputPathsEqual(blurPath, blurringFocusPath)) {
45433
- return;
45434
- }
44539
+ // Failsafe: abort if ID is different, because focus probably happened
44540
+ // before blur.
44541
+ if (!Util.inputPathsEqual(blurPath, blurringFocusPath)) {
44542
+ return;
44543
+ }
45435
44544
 
45436
44545
  // Wait until after any new focus events fire this tick before declaring
45437
44546
  // that nothing is focused, since if there were a focus change across
@@ -46869,31 +45978,248 @@ function inferItemTreeShape(node) {
46869
45978
  if (typeof node === "object" && node.__type === "hint") {
46870
45979
  return shapes.hint;
46871
45980
  }
46872
- if (typeof node === "object") {
46873
- const valueShapes = {};
46874
- Object.keys(node).forEach(key => {
46875
- valueShapes[key] = inferItemTreeShape(node[key]);
46876
- });
46877
- return shapes.shape(valueShapes);
45981
+ if (typeof node === "object") {
45982
+ const valueShapes = {};
45983
+ Object.keys(node).forEach(key => {
45984
+ valueShapes[key] = inferItemTreeShape(node[key]);
45985
+ });
45986
+ return shapes.shape(valueShapes);
45987
+ }
45988
+ throw new perseusCore.PerseusError(`unexpected multi-item node ${JSON.stringify(node)}`, perseusCore.Errors.InvalidInput);
45989
+ }
45990
+
45991
+ /**
45992
+ * Convert the given Item to an ItemTree, by unwrapping the `_multi` key.
45993
+ */
45994
+ function itemToTree(item) {
45995
+ return item._multi;
45996
+ }
45997
+
45998
+ /**
45999
+ * Convert the given ItemTree to an Item, by wrapping it in the `_multi` key.
46000
+ */
46001
+ function treeToItem(node) {
46002
+ return {
46003
+ _multi: node
46004
+ };
46005
+ }
46006
+
46007
+ var isObject = function (obj) {
46008
+ return obj === Object(obj);
46009
+ };
46010
+ var merge = function () {
46011
+ var obj = {};
46012
+ for (var i = 0; i < arguments.length; i++) {
46013
+ var source = arguments[i];
46014
+ if (source) {
46015
+ for (var prop in source) {
46016
+ obj[prop] = source[prop];
46017
+ }
46018
+ }
46019
+ }
46020
+ return obj;
46021
+ };
46022
+ var clone = function (obj) {
46023
+ if (!isObject(obj)) {
46024
+ return obj;
46025
+ }
46026
+ return Array.isArray(obj) ? obj.slice() : merge(obj);
46027
+ };
46028
+
46029
+ /* TODO batch *all* mutations
46030
+ * idea: freeze / thaw implementations for all types
46031
+ * lens constructor thaws, freeze delegates to type's freeze
46032
+ */
46033
+
46034
+ // equivalents, without requiring it
46035
+ // find the implementation to use for a given object
46036
+ var dispatch = function (x) {
46037
+ if (Array.isArray(x)) {
46038
+ return arr;
46039
+ } else if (isObject(x)) {
46040
+ return obj;
46041
+ } else if (typeof x === "string") {
46042
+ return str;
46043
+ }
46044
+ };
46045
+
46046
+ // This is underscore with a different name
46047
+ var lens = function (obj) {
46048
+ if (obj instanceof lens) {
46049
+ return obj;
46050
+ }
46051
+ if (!(this instanceof lens)) {
46052
+ return new lens(obj);
46053
+ }
46054
+ var ops = dispatch(obj);
46055
+ this._wrapped = ops.thaw ? ops.thaw(obj) : obj;
46056
+ };
46057
+ lens.prototype.freeze = function () {
46058
+ var obj = this._wrapped;
46059
+ var ops = dispatch(obj);
46060
+ return ops.freeze ? ops.freeze(obj) : obj;
46061
+ };
46062
+ lens.prototype.zoom = function (lensArr) {
46063
+ if (this._zoomStack === undefined) {
46064
+ this._zoomStack = [];
46065
+ }
46066
+ this._zoomStack.push({
46067
+ zoom: lensArr,
46068
+ wrapped: this._wrapped
46069
+ });
46070
+ this._wrapped = lens(this._wrapped).get(lensArr);
46071
+ return this;
46072
+ };
46073
+ lens.prototype.deZoom = function () {
46074
+ var frame = this._zoomStack.pop();
46075
+ this._wrapped = lens(frame.wrapped).set(frame.zoom, this._wrapped).freeze();
46076
+ return this;
46077
+ };
46078
+ lens.prototype.get = function (lensArr) {
46079
+ var obj = this._wrapped;
46080
+ for (var i = 0; i < lensArr.length; i++) {
46081
+ obj = dispatch(obj).get(obj, lensArr[i]);
46082
+ }
46083
+ return obj;
46084
+ };
46085
+ lens.prototype.mod = function (lensArr, f) {
46086
+ var obj = this._wrapped;
46087
+ var newObj = clone(obj);
46088
+ var ops = dispatch(obj);
46089
+ if (lensArr.length === 0) {
46090
+ this._wrapped = f(this._wrapped);
46091
+ } else if (lensArr.length === 1) {
46092
+ this._wrapped = ops.mod(newObj, lensArr[0], f);
46093
+ } else {
46094
+ var monocle = lensArr[0];
46095
+ var shortLens = lensArr.slice(1);
46096
+
46097
+ // newObj = ops.mod(obj[monocle], shortLens, f);
46098
+
46099
+ newObj[monocle] = lens(obj[monocle]).mod(shortLens, f).freeze();
46100
+ this._wrapped = newObj;
46101
+ }
46102
+ return this;
46103
+ };
46104
+
46105
+ // TODO - move to individual files
46106
+ lens.prototype.merge = function (lensArr, props) {
46107
+ this._wrapped = lens(this._wrapped).mod(lensArr, function (oldProps) {
46108
+ return merge(oldProps, props);
46109
+ }).freeze();
46110
+ return this;
46111
+ };
46112
+
46113
+ // Lens must have length >= 1 or there would be nothing to return
46114
+ lens.prototype.del = function (lensArr) {
46115
+ var obj = this._wrapped;
46116
+ var ops = dispatch(obj);
46117
+ if (lensArr.length === 1) {
46118
+ this._wrapped = ops.del(obj, lensArr[0]);
46119
+ } else {
46120
+ var monocle = lensArr[0];
46121
+ var shortLens = lensArr.slice(1);
46122
+ var newObj = clone(obj);
46123
+ newObj[monocle] = lens(obj[monocle]).del(shortLens).freeze();
46124
+ this._wrapped = newObj;
46125
+ }
46126
+ return this;
46127
+ };
46128
+ lens.prototype.set = function (lensArr, set) {
46129
+ return this.mod(lensArr, function () {
46130
+ return set;
46131
+ });
46132
+ };
46133
+
46134
+ // ARRAY FUNCTIONS
46135
+
46136
+ const arr = {
46137
+ get: function (arr, monocle) {
46138
+ return arr[monocle];
46139
+ },
46140
+ set: function (arr, monocle, val) {
46141
+ var newArr = arr.splice();
46142
+ newArr[monocle] = val;
46143
+ return newArr;
46144
+ },
46145
+ mod: function (arr, monocle, f) {
46146
+ var newArr = arr.slice();
46147
+ newArr[monocle] = f(arr[monocle]);
46148
+ return newArr;
46149
+ },
46150
+ del: function (arr, monocle) {
46151
+ var newArr = arr.slice();
46152
+ newArr.splice(monocle, 1);
46153
+ return newArr;
46154
+ }
46155
+ };
46156
+
46157
+ /*
46158
+ // Lens must point to a member of an array. We'll insert into that array.
46159
+ lens.prototype.insertAt = function(lensArr, toInsert) {
46160
+ var obj = this._wrapped;
46161
+
46162
+ var arrLens = lensArr.slice(0, -1);
46163
+ var arr = lens(obj).get(arrLens).slice(); // slice to copy
46164
+
46165
+ var arrIdx = lensArr[lensArr.length-1];
46166
+ arr.splice(arrIdx, 0, toInsert);
46167
+ return lens(obj).set(arrLens, arr);
46168
+ };
46169
+
46170
+ lens.prototype.insertBefore = lens.prototype.insertAt;
46171
+ lens.prototype.insertAfter = function(lensArr, toInsert) {
46172
+ var newLens = lensArr.slice();
46173
+ newLens[newLens.length-1] += 1;
46174
+ return lens(this._wrapped).insertAt(newLens, toInsert);
46175
+ };
46176
+ */
46177
+
46178
+ // OBJECT FUNCTIONS
46179
+
46180
+ const obj = {
46181
+ get: function (obj, monocle) {
46182
+ return obj[monocle];
46183
+ },
46184
+ set: function (obj, monocle, val) {
46185
+ var newObj = clone(obj);
46186
+ newObj[monocle] = val;
46187
+ return newObj;
46188
+ },
46189
+ mod: function (obj, monocle, f) {
46190
+ var newObj = clone(obj);
46191
+ newObj[monocle] = f(obj[monocle]);
46192
+ return newObj;
46193
+ },
46194
+ del: function (obj, monocle) {
46195
+ var newObj = clone(obj);
46196
+ delete newObj[monocle];
46197
+ return newObj;
46878
46198
  }
46879
- throw new perseusCore.PerseusError(`unexpected multi-item node ${JSON.stringify(node)}`, perseusCore.Errors.InvalidInput);
46880
- }
46199
+ };
46881
46200
 
46882
- /**
46883
- * Convert the given Item to an ItemTree, by unwrapping the `_multi` key.
46884
- */
46885
- function itemToTree(item) {
46886
- return item._multi;
46887
- }
46201
+ // STRING FUNCTIONS
46888
46202
 
46889
- /**
46890
- * Convert the given ItemTree to an Item, by wrapping it in the `_multi` key.
46891
- */
46892
- function treeToItem(node) {
46893
- return {
46894
- _multi: node
46895
- };
46896
- }
46203
+ const str = {
46204
+ get: function (arr, monocle) {
46205
+ return arr[monocle];
46206
+ },
46207
+ set: function (arr, monocle, val) {
46208
+ var newArr = arr.splice();
46209
+ newArr[monocle] = val;
46210
+ return newArr;
46211
+ },
46212
+ mod: function (arr, monocle, f) {
46213
+ var newArr = arr.splice();
46214
+ newArr[monocle] = f(arr[monocle]);
46215
+ return newArr;
46216
+ },
46217
+ del: function (arr, monocle) {
46218
+ var newArr = arr.slice();
46219
+ newArr.splice(monocle);
46220
+ return newArr;
46221
+ }
46222
+ };
46897
46223
 
46898
46224
  // TODO(mdr)
46899
46225
  // TODO(mdr)
@@ -47035,467 +46361,833 @@ class MultiRenderer extends React__namespace.Component {
47035
46361
  }
47036
46362
 
47037
46363
  /**
47038
- * Construct a Renderer for the given HintNode, and keep track of the hint
47039
- * itself for future use, too.
46364
+ * Construct a Renderer for the given HintNode, and keep track of the hint
46365
+ * itself for future use, too.
46366
+ */
46367
+ _makeHintRendererData(hint) {
46368
+ // TODO(mdr): Once HintsRenderer supports inter-widget communication,
46369
+ // give it a ref. Until then, leave the ref null forever, to avoid
46370
+ // confusing the findWidgets functions.
46371
+ //
46372
+ // NOTE(davidflanagan): As a partial step toward inter-widget
46373
+ // communication we're going to pass a findExternalWidgets function
46374
+ // (using a dummy data object). This allows passage-ref widgets in
46375
+ // hints to use findWidget() to find the passage widgets they reference.
46376
+ // Note that this is one-way only, however. It does not allow
46377
+ // widgets in the question to find widgets in the hints, for example.
46378
+ const findExternalWidgets = criterion => this._findWidgets({}, criterion);
46379
+ return {
46380
+ hint,
46381
+ findExternalWidgets,
46382
+ // _annotateRendererArray() needs this
46383
+ ref: null,
46384
+ makeRenderer: () => /*#__PURE__*/React__namespace.createElement(HintsRenderer, _extends({}, this._getRendererProps(), {
46385
+ findExternalWidgets: findExternalWidgets,
46386
+ hints: [hint]
46387
+ }))
46388
+ };
46389
+ }
46390
+
46391
+ /**
46392
+ * Construct a tree of interconnected RendererDatas, corresponding to the
46393
+ * given item. Called in `_tryMakeRendererState`, in order to store this
46394
+ * tree in the component state.
46395
+ */
46396
+ _makeRendererDataTree(item, shape) {
46397
+ const itemTree = itemToTree(item);
46398
+ return this.rendererDataTreeMapper.mapTree(itemTree, shape);
46399
+ }
46400
+
46401
+ /**
46402
+ * Return all widgets that meet the given criterion, from all Renderers
46403
+ * except the Renderer that triggered this call.
46404
+ *
46405
+ * This function is provided to each Renderer's `findExternalWidgets` prop,
46406
+ * which enables widgets in different Renderers to discover each other and
46407
+ * communicate.
46408
+ */
46409
+ _findWidgets(callingData, filterCriterion) {
46410
+ const results = [];
46411
+ this._mapRenderers(data => {
46412
+ if (callingData !== data && data.ref) {
46413
+ results.push(...data.ref.findInternalWidgets(filterCriterion));
46414
+ }
46415
+ });
46416
+ return results;
46417
+ }
46418
+
46419
+ /**
46420
+ * Copy the renderer tree, apply the given transformation to the leaf nodes
46421
+ * and the optional given transformation to the array nodes, and return the
46422
+ * result.
46423
+ *
46424
+ * Used to provide structured data to the call site (the Renderer tree on
46425
+ * `render`, the Score tree on `getScores`, etc.), and to traverse the
46426
+ * renderer tree even when we disregard the output (like in
46427
+ * `_findWidgets`).
46428
+ */
46429
+ _mapRenderers(
46430
+ // eslint-disable-next-line no-restricted-syntax
46431
+ leafMapper
46432
+ // @ts-expect-error - TS2315 - Type 'Tree' is not generic.
46433
+ ) {
46434
+ const {
46435
+ rendererDataTree
46436
+ } = this.state;
46437
+ if (!rendererDataTree) {
46438
+ return null;
46439
+ }
46440
+ const mapper = buildMapper().setContentMapper(leafMapper).setHintMapper(leafMapper);
46441
+ return mapper.mapTree(rendererDataTree, this.props.shape);
46442
+ }
46443
+ _scoreFromRef(ref) {
46444
+ if (!ref) {
46445
+ return null;
46446
+ }
46447
+ const [guess, score] = ref.guessAndScore();
46448
+ let state;
46449
+ if (ref.getSerializedState) {
46450
+ state = ref.getSerializedState();
46451
+ }
46452
+ return Util.keScoreFromPerseusScore(score, guess, state);
46453
+ }
46454
+
46455
+ /**
46456
+ * Return a tree in the shape of the multi-item, with scores at each of
46457
+ * the content nodes and `null` at the other leaf nodes.
46458
+ */
46459
+ getScores() {
46460
+ return this._mapRenderers(data => this._scoreFromRef(data.ref));
46461
+ }
46462
+
46463
+ /**
46464
+ * Return a single composite score for all rendered content nodes.
46465
+ * The `guess` is a tree in the shape of the multi-item, with an individual
46466
+ * guess at each content node and `null` at the other leaf nodes.
46467
+ */
46468
+ score() {
46469
+ const scores = [];
46470
+ const state = [];
46471
+ const guess = this._mapRenderers(data => {
46472
+ if (!data.ref) {
46473
+ return null;
46474
+ }
46475
+ if (data.ref.getSerializedState) {
46476
+ state.push(data.ref.getSerializedState());
46477
+ }
46478
+ scores.push(data.ref.score());
46479
+ return data.ref?.getUserInput();
46480
+ });
46481
+ const combinedScore = scores.reduce(Util.combineScores);
46482
+ return Util.keScoreFromPerseusScore(combinedScore, guess, state);
46483
+ }
46484
+
46485
+ /**
46486
+ * Return a tree in the shape of the multi-item, with serialized state at
46487
+ * each of the content nodes and `null` at the other leaf nodes.
46488
+ *
46489
+ * If the lastSerializedState argument is supplied, this function will fill
46490
+ * in the state of not-currently-rendered content and hint nodes with the
46491
+ * values from the previous serialized state. If no lastSerializedState is
46492
+ * supplied, `null` will be returned for not-currently-rendered content and
46493
+ * hint nodes.
46494
+ */
46495
+ _getSerializedState(lastSerializedState) {
46496
+ return this._mapRenderers((data, _, path) => {
46497
+ if (data.ref) {
46498
+ return data.ref.getSerializedState();
46499
+ }
46500
+ if (lastSerializedState) {
46501
+ return lens(lastSerializedState).get(path);
46502
+ }
46503
+ return null;
46504
+ });
46505
+ }
46506
+
46507
+ /**
46508
+ * Given a tree in the shape of the multi-item, with serialized state at
46509
+ * each of the content nodes, restore each state to the corresponding
46510
+ * renderer if currently mounted.
46511
+ */
46512
+ restoreSerializedState(serializedState, callback) {
46513
+ // We want to call our async callback only once all of the childrens'
46514
+ // callbacks have run. We add one to this counter before we call out to
46515
+ // each renderer and decrement it when it runs our callback.
46516
+ let numCallbacks = 0;
46517
+ const countCallback = () => {
46518
+ numCallbacks--;
46519
+ if (callback && numCallbacks === 0) {
46520
+ callback();
46521
+ }
46522
+ };
46523
+ this._mapRenderers((data, _, path) => {
46524
+ if (!data.ref) {
46525
+ return;
46526
+ }
46527
+ const state = lens(serializedState).get(path);
46528
+ if (!state) {
46529
+ return;
46530
+ }
46531
+ numCallbacks++;
46532
+ data.ref?.restoreSerializedState(state, countCallback);
46533
+ });
46534
+ }
46535
+
46536
+ /**
46537
+ * Given an array of renderers, if it happens to be an array of *hint*
46538
+ * renderers, then attach a `firstN` method to the array, which allows the
46539
+ * layout to render the hints together in one HintsRenderer.
47040
46540
  */
47041
- _makeHintRendererData(hint) {
47042
- // TODO(mdr): Once HintsRenderer supports inter-widget communication,
47043
- // give it a ref. Until then, leave the ref null forever, to avoid
47044
- // confusing the findWidgets functions.
47045
- //
47046
- // NOTE(davidflanagan): As a partial step toward inter-widget
47047
- // communication we're going to pass a findExternalWidgets function
47048
- // (using a dummy data object). This allows passage-ref widgets in
47049
- // hints to use findWidget() to find the passage widgets they reference.
47050
- // Note that this is one-way only, however. It does not allow
47051
- // widgets in the question to find widgets in the hints, for example.
47052
- const findExternalWidgets = criterion => this._findWidgets({}, criterion);
47053
- return {
47054
- hint,
47055
- findExternalWidgets,
47056
- // _annotateRendererArray() needs this
47057
- ref: null,
47058
- makeRenderer: () => /*#__PURE__*/React__namespace.createElement(HintsRenderer, _extends({}, this._getRendererProps(), {
47059
- findExternalWidgets: findExternalWidgets,
47060
- hints: [hint]
47061
- }))
47062
- };
46541
+ _annotateRendererArray(renderers, rendererDatas, shape) {
46542
+ if (shape.elementShape.type === "hint") {
46543
+ // The shape says that these are HintRendererDatas, even though
46544
+ // it's not provable at compile time, so perform a cast.
46545
+ const hintRendererDatas = rendererDatas;
46546
+ renderers = [...renderers];
46547
+ renderers.firstN = n => /*#__PURE__*/React__namespace.createElement(HintsRenderer, _extends({}, this._getRendererProps(), {
46548
+ findExternalWidgets: hintRendererDatas[0] ? hintRendererDatas[0].findExternalWidgets : undefined,
46549
+ hints: hintRendererDatas.map(d => d.hint),
46550
+ hintsVisible: n
46551
+ }));
46552
+ }
46553
+ return renderers;
47063
46554
  }
47064
46555
 
47065
46556
  /**
47066
- * Construct a tree of interconnected RendererDatas, corresponding to the
47067
- * given item. Called in `_tryMakeRendererState`, in order to store this
47068
- * tree in the component state.
46557
+ * Return a tree in the shape of the multi-item, with a Renderer at each
46558
+ * content node and a HintRenderer at each hint node.
46559
+ *
46560
+ * This is generated by running each of the `makeRenderer` functions at the
46561
+ * leaf nodes.
47069
46562
  */
47070
- _makeRendererDataTree(item, shape) {
47071
- const itemTree = itemToTree(item);
47072
- return this.rendererDataTreeMapper.mapTree(itemTree, shape);
46563
+ _getRenderers() {
46564
+ return this.getRenderersMapper.mapTree(this.state.rendererDataTree, this.props.shape);
46565
+ }
46566
+ render() {
46567
+ if (this.state.renderError) {
46568
+ return /*#__PURE__*/React__namespace.createElement("div", {
46569
+ className: aphrodite.css(styles.error)
46570
+ }, this.context.strings.errorRendering({
46571
+ error: String(this.state.renderError)
46572
+ }));
46573
+ }
46574
+
46575
+ // Pass the renderer tree to the `children` function, which will
46576
+ // determine the actual content of this component.
46577
+ return /*#__PURE__*/React__namespace.createElement(DependenciesContext.Provider, {
46578
+ value: this.props.dependencies
46579
+ }, this.props.children({
46580
+ renderers: this._getRenderers()
46581
+ }));
46582
+ }
46583
+ }
46584
+ const styles = aphrodite.StyleSheet.create({
46585
+ error: {
46586
+ color: "red"
46587
+ }
46588
+ });
46589
+
46590
+ /**
46591
+ * Utility functions to build React PropTypes for multi-items and shapes.
46592
+ *
46593
+ * If you're writing new components, though, consider using the Item and Shape
46594
+ * types instead.
46595
+ */
46596
+
46597
+ /**
46598
+ * Return a PropType that accepts Items of the given shape, and rejects other
46599
+ * objects.
46600
+ *
46601
+ * Usage: `propTypes: {item: buildPropTypeForShape(myShape)}`
46602
+ */
46603
+ function buildPropTypeForShape(shape) {
46604
+ return PropTypes__default["default"].oneOfType([PropTypes__default["default"].shape({
46605
+ _multi: buildTreePropTypeForShape(shape)
46606
+ }), PropTypes__default["default"].oneOf([null, undefined])]);
46607
+ }
46608
+
46609
+ /**
46610
+ * Return a PropType that accepts ItemTrees of the given shape, and rejects
46611
+ * other objects.
46612
+ */
46613
+ function buildTreePropTypeForShape(shape) {
46614
+ if (shape.type === "content") {
46615
+ return PropTypes__default["default"].shape({
46616
+ // TODO(mdr): Remove #LegacyContentNode support.
46617
+ __type: PropTypes__default["default"].oneOf(["content", "item"]).isRequired,
46618
+ content: PropTypes__default["default"].string,
46619
+ images: PropTypes__default["default"].objectOf(PropTypes__default["default"].any),
46620
+ widgets: PropTypes__default["default"].objectOf(PropTypes__default["default"].any)
46621
+ });
46622
+ }
46623
+ if (shape.type === "hint") {
46624
+ return PropTypes__default["default"].shape({
46625
+ __type: PropTypes__default["default"].oneOf(["hint"]).isRequired,
46626
+ content: PropTypes__default["default"].string,
46627
+ images: PropTypes__default["default"].objectOf(PropTypes__default["default"].any),
46628
+ widgets: PropTypes__default["default"].objectOf(PropTypes__default["default"].any),
46629
+ replace: PropTypes__default["default"].bool
46630
+ });
46631
+ }
46632
+ if (shape.type === "tags") {
46633
+ return PropTypes__default["default"].arrayOf(PropTypes__default["default"].string.isRequired);
46634
+ }
46635
+ if (shape.type === "array") {
46636
+ const elementPropType = buildTreePropTypeForShape(shape.elementShape);
46637
+ return PropTypes__default["default"].arrayOf(elementPropType.isRequired);
46638
+ }
46639
+ if (shape.type === "object") {
46640
+ const valueShapes = shape.shape;
46641
+ const propTypeShape = {};
46642
+ Object.keys(valueShapes).forEach(key => {
46643
+ propTypeShape[key] = buildTreePropTypeForShape(valueShapes[key]).isRequired;
46644
+ });
46645
+ return PropTypes__default["default"].shape(propTypeShape);
46646
+ }
46647
+ throw new perseusCore.PerseusError(
46648
+ // @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
46649
+ `unexpected shape type ${shape.type}`, perseusCore.Errors.InvalidInput);
46650
+ }
46651
+
46652
+ /**
46653
+ * This library provides support for Perseus multi-items: structured Perseus
46654
+ * content that content creators can easily create, and that applications can
46655
+ * easily render into different parts of the layout.
46656
+ *
46657
+ * For more details about application and motivation, see:
46658
+ * https://sites.google.com/a/khanacademy.org/forge/for-developers/perseus-items-and-multi-items
46659
+ *
46660
+ * This file primarily exposes the `MultiRenderer` component, which performs
46661
+ * multi-rendering. To multi-render a question, pass in the content of the item
46662
+ * to the `MultiRenderer` component as a props. Then, pass in a function which
46663
+ * takes an object of renderers (in the same structure as the content), and
46664
+ * return a render tree. The `MultiRenderer` component will allow you to
46665
+ * combine scores, serialized state, etc. without having to manually call on
46666
+ * each of the functions. It also handles inter-widgets requests between the
46667
+ * different renderers.
46668
+ * For more details, see `multi-items/multi-renderer.jsx`.
46669
+ *
46670
+ * Example:
46671
+ *
46672
+ * item = {_multi: {
46673
+ * left: <content data>,
46674
+ * right: [<content data>, <content data>],
46675
+ * }}
46676
+ * shape = shapes.shape({
46677
+ * left: shapes.content,
46678
+ * right: shapes.arrayOf(shapes.content),
46679
+ * })
46680
+ *
46681
+ * <MultiRenderer item={item} shape={shape}>
46682
+ * {({renderers}) =>
46683
+ * <div>
46684
+ * <div id="left">{renderers.left}</div>
46685
+ * <ul id="right">
46686
+ * {renderers.right.map(r => <li>{r}</li>)}
46687
+ * </ul>
46688
+ * </div>
46689
+ * }
46690
+ * </MultiRenderer>
46691
+ *
46692
+ * This file also exposes `shapes`, which helps you construct a runtime type
46693
+ * declaration for your particular class of multi-item. This can then be used
46694
+ * to create a MultirendererEditor for your multi-item shape, and to validate
46695
+ * that a multi-item conforms to the shape via `buildPropTypeForShape`.
46696
+ * For more details, see `multi-items/shapes.js`.
46697
+ *
46698
+ * This file also exposes some utility functions for working with generic
46699
+ * multi-items, like `findContentNodesInItem`, `findHintNodesInItem`,
46700
+ * `inferItemShape`, and `buildEmptyItemForShape`.
46701
+ * For more details, see `multi-items/items.js`.
46702
+ */
46703
+ var MultiItems = {
46704
+ // Tools for rendering your multi-items
46705
+ MultiRenderer,
46706
+ // Tools for declaring your multi-item shapes
46707
+ shapes,
46708
+ buildPropTypeForShape,
46709
+ // Tools for generically manipulating multi-items
46710
+ buildEmptyItemForShape,
46711
+ findContentNodesInItem: findContentNodesInItem$2,
46712
+ findHintNodesInItem,
46713
+ inferItemShape: inferItemShape$2
46714
+ };
46715
+
46716
+ /**
46717
+ * Calculates whether a perseus item is renderable by a specific
46718
+ * perseus-item-data version.
46719
+ *
46720
+ * This is done inside of the perseus repo so that it can traverse
46721
+ * widget-specific data that might need to do a sub-traversal.
46722
+ * This supports widgets that contain renderers, such as the
46723
+ * group or sequence widgets.
46724
+ */
46725
+ const {
46726
+ findContentNodesInItem: findContentNodesInItem$1,
46727
+ inferItemShape: inferItemShape$1
46728
+ } = MultiItems;
46729
+ const isUpgradedWidgetInfoRenderableBy = function (widgetInfo, widgetRendererVersion) {
46730
+ if (widgetRendererVersion == null) {
46731
+ // If the widget does not exist in this version, this will
46732
+ // be null, and that version of perseus cannot render the
46733
+ // widget (it doesn't even know the widget exists!)
46734
+ return false;
46735
+ }
46736
+ const widgetVersion = widgetInfo.version || {
46737
+ major: 0,
46738
+ minor: 0
46739
+ };
46740
+ if (widgetRendererVersion.major > widgetVersion.major) {
46741
+ return true;
46742
+ }
46743
+ if (widgetRendererVersion.major < widgetVersion.major) {
46744
+ return false;
46745
+ }
46746
+ // If the major versions are the same, the minor version acts
46747
+ // like a tie-breaker.
46748
+ // For example, input-number 3.2 can render an input-number
46749
+ // 2.4, 3.0, or 3.2, but not an input number 3.3 or 4.0.
46750
+ return widgetRendererVersion.minor >= widgetVersion.minor;
46751
+ };
46752
+ const isRawWidgetInfoRenderableBy = function (widgetInfo, rendererContentVersion) {
46753
+ // Empty/non-existant widgets are always safe to render
46754
+ if (widgetInfo == null || widgetInfo.type == null) {
46755
+ return true;
47073
46756
  }
47074
46757
 
47075
- /**
47076
- * Return all widgets that meet the given criterion, from all Renderers
47077
- * except the Renderer that triggered this call.
47078
- *
47079
- * This function is provided to each Renderer's `findExternalWidgets` prop,
47080
- * which enables widgets in different Renderers to discover each other and
47081
- * communicate.
47082
- */
47083
- _findWidgets(callingData, filterCriterion) {
47084
- const results = [];
47085
- this._mapRenderers(data => {
47086
- if (callingData !== data && data.ref) {
47087
- results.push(...data.ref.findInternalWidgets(filterCriterion));
46758
+ // NOTE: This doesn't modify the widget info if the widget info
46759
+ // is at a later version than is supported.
46760
+ const upgradedWidgetInfo = upgradeWidgetInfoToLatestVersion(widgetInfo);
46761
+ return isUpgradedWidgetInfoRenderableBy(upgradedWidgetInfo, rendererContentVersion[upgradedWidgetInfo.type]);
46762
+ };
46763
+ const isRendererContentRenderableBy = function (rendererOptions, rendererContentVersion) {
46764
+ let isRenderable = true;
46765
+ traverse(rendererOptions, null, function (widgetInfo) {
46766
+ isRenderable = isRenderable && isRawWidgetInfoRenderableBy(widgetInfo, rendererContentVersion);
46767
+ });
46768
+ return isRenderable;
46769
+ };
46770
+ const isItemRenderableByVersion = function (itemData, rendererContentVersion) {
46771
+ if (itemData == null || rendererContentVersion == null) {
46772
+ throw new perseusCore.PerseusError("missing parameter to Perseus.isRenderable.item", perseusCore.Errors.InvalidInput);
46773
+ }
46774
+ if (itemData._multi) {
46775
+ const shape = inferItemShape$1(itemData);
46776
+ let isRenderable = true;
46777
+ findContentNodesInItem$1(itemData, shape, node => {
46778
+ const nodeIsRenderable = isRendererContentRenderableBy(node, rendererContentVersion);
46779
+ if (!nodeIsRenderable) {
46780
+ isRenderable = false;
47088
46781
  }
47089
46782
  });
47090
- return results;
46783
+ return isRenderable;
47091
46784
  }
46785
+ return isRendererContentRenderableBy(itemData.question, rendererContentVersion);
46786
+ };
47092
46787
 
47093
- /**
47094
- * Copy the renderer tree, apply the given transformation to the leaf nodes
47095
- * and the optional given transformation to the array nodes, and return the
47096
- * result.
47097
- *
47098
- * Used to provide structured data to the call site (the Renderer tree on
47099
- * `render`, the Score tree on `getScores`, etc.), and to traverse the
47100
- * renderer tree even when we disregard the output (like in
47101
- * `_findWidgets`).
47102
- */
47103
- _mapRenderers(
47104
- // eslint-disable-next-line no-restricted-syntax
47105
- leafMapper
47106
- // @ts-expect-error - TS2315 - Type 'Tree' is not generic.
47107
- ) {
47108
- const {
47109
- rendererDataTree
47110
- } = this.state;
47111
- if (!rendererDataTree) {
47112
- return null;
47113
- }
47114
- const mapper = buildMapper().setContentMapper(leafMapper).setHintMapper(leafMapper);
47115
- return mapper.mapTree(rendererDataTree, this.props.shape);
47116
- }
47117
- _scoreFromRef(ref) {
47118
- if (!ref) {
47119
- return null;
47120
- }
47121
- const [guess, score] = ref.guessAndScore();
47122
- let state;
47123
- if (ref.getSerializedState) {
47124
- state = ref.getSerializedState();
46788
+ /**
46789
+ * Identifies whether or not a given perseus item requires the use of a mouse
46790
+ * or screen, based on the widgets it contains.
46791
+ */
46792
+ const {
46793
+ findContentNodesInItem,
46794
+ inferItemShape
46795
+ } = MultiItems;
46796
+
46797
+ // Iterate over a single Perseus renderer, mutating `widgets` by appending
46798
+ // violating widget types discovered in this item.
46799
+ function traverseRenderer(itemData, widgets) {
46800
+ traverse(itemData, null, function (info) {
46801
+ if (info.type && !isAccessible(info)) {
46802
+ widgets.push(info.type);
47125
46803
  }
47126
- return Util.keScoreFromPerseusScore(score, guess, state);
47127
- }
46804
+ });
46805
+ }
47128
46806
 
47129
- /**
47130
- * Return a tree in the shape of the multi-item, with scores at each of
47131
- * the content nodes and `null` at the other leaf nodes.
47132
- */
47133
- getScores() {
47134
- return this._mapRenderers(data => this._scoreFromRef(data.ref));
46807
+ // Returns a list of widgets that cause a given perseus item to require
46808
+ // the use of a screen or mouse.
46809
+ //
46810
+ // For now we'll just check the `accessible` field on each of the widgets
46811
+ // in the item data, but in the future we may specify accessibility on
46812
+ // each widget with higher granularity.
46813
+ function violatingWidgets(itemData) {
46814
+ // TODO(jordan): Hints as well
46815
+ const widgets = [];
46816
+ if (itemData._multi) {
46817
+ const shape = inferItemShape(itemData);
46818
+ findContentNodesInItem(itemData, shape, content => traverseRenderer(content, widgets));
46819
+ } else {
46820
+ traverseRenderer(itemData.question, widgets);
47135
46821
  }
47136
46822
 
47137
- /**
47138
- * Return a single composite score for all rendered content nodes.
47139
- * The `guess` is a tree in the shape of the multi-item, with an individual
47140
- * guess at each content node and `null` at the other leaf nodes.
47141
- */
47142
- score() {
47143
- const scores = [];
47144
- const state = [];
47145
- const guess = this._mapRenderers(data => {
47146
- if (!data.ref) {
47147
- return null;
47148
- }
47149
- if (data.ref.getSerializedState) {
47150
- state.push(data.ref.getSerializedState());
47151
- }
47152
- scores.push(data.ref.score());
47153
- return data.ref?.getUserInput();
47154
- });
47155
- const combinedScore = scores.reduce(Util.combineScores);
47156
- return Util.keScoreFromPerseusScore(combinedScore, guess, state);
47157
- }
46823
+ // Uniquify the list of widgets (by type)
46824
+ return ___default["default"].uniq(widgets);
46825
+ }
47158
46826
 
47159
- /**
47160
- * Return a tree in the shape of the multi-item, with serialized state at
47161
- * each of the content nodes and `null` at the other leaf nodes.
47162
- *
47163
- * If the lastSerializedState argument is supplied, this function will fill
47164
- * in the state of not-currently-rendered content and hint nodes with the
47165
- * values from the previous serialized state. If no lastSerializedState is
47166
- * supplied, `null` will be returned for not-currently-rendered content and
47167
- * hint nodes.
47168
- */
47169
- _getSerializedState(lastSerializedState) {
47170
- return this._mapRenderers((data, _, path) => {
47171
- if (data.ref) {
47172
- return data.ref.getSerializedState();
47173
- }
47174
- if (lastSerializedState) {
47175
- return lens(lastSerializedState).get(path);
47176
- }
47177
- return null;
47178
- });
47179
- }
46827
+ /* eslint-disable eqeqeq, @typescript-eslint/no-unused-vars */
47180
46828
 
47181
- /**
47182
- * Given a tree in the shape of the multi-item, with serialized state at
47183
- * each of the content nodes, restore each state to the corresponding
47184
- * renderer if currently mounted.
47185
- */
47186
- restoreSerializedState(serializedState, callback) {
47187
- // We want to call our async callback only once all of the childrens'
47188
- // callbacks have run. We add one to this counter before we call out to
47189
- // each renderer and decrement it when it runs our callback.
47190
- let numCallbacks = 0;
47191
- const countCallback = () => {
47192
- numCallbacks--;
47193
- if (callback && numCallbacks === 0) {
47194
- callback();
46829
+ /**
46830
+ * Format a floating point for display using the specified
46831
+ * number of significant figures and least significant decimal.
46832
+ * Scientific notation may used by this method if this
46833
+ * object is very small, very large, has many significant
46834
+ * figures, or the number of significant figures would be
46835
+ * ambiguous in the output if scientific notation were not
46836
+ * used.
46837
+ *
46838
+ * @param f A floating point number that should be displayed
46839
+ * @param sigFigs desired number of significant figures (integer).
46840
+ * @param sigDecs the least significant decimal place (integer).
46841
+ * @param scientific true iff scientific notation should always be used.
46842
+ * @return a string of this object formatted correctly.
46843
+ */
46844
+ function displaySigFigs(f, sigFigs, sigDecs, scientific) {
46845
+ const s = "" + f;
46846
+ let order = parseOrder(s);
46847
+ let mantissa = parseMantissa(s);
46848
+ let positive = parseSign(s);
46849
+ let add;
46850
+ let decAdd;
46851
+ let sigAdd;
46852
+ let zeroScientific = false;
46853
+ if (f == 0 || mantissa == "" || mantissa == "0") {
46854
+ mantissa = "";
46855
+ for (let i = 0; i < sigFigs; i++) {
46856
+ mantissa += "0";
46857
+ }
46858
+ order = sigFigs + sigDecs;
46859
+ if (sigDecs < 0 && -sigDecs >= sigFigs) {
46860
+ zeroScientific = true;
46861
+ }
46862
+ } else {
46863
+ decAdd = order - mantissa.length - sigDecs;
46864
+ sigAdd = sigFigs - mantissa.length;
46865
+ add = Math.min(sigAdd, decAdd);
46866
+ if (add < 0) {
46867
+ let rounded = round(mantissa, -add);
46868
+ if (rounded.length > mantissa.length + add) {
46869
+ order++;
46870
+ if (decAdd > sigAdd) {
46871
+ rounded = round(rounded, 1);
46872
+ }
47195
46873
  }
47196
- };
47197
- this._mapRenderers((data, _, path) => {
47198
- if (!data.ref) {
47199
- return;
46874
+ mantissa = rounded;
46875
+ } else if (add > 0) {
46876
+ for (let i = 0; i < add; i++) {
46877
+ mantissa += "0";
47200
46878
  }
47201
- const state = lens(serializedState).get(path);
47202
- if (!state) {
47203
- return;
46879
+ }
46880
+ if (mantissa == "" || mantissa == "0") {
46881
+ mantissa = "0";
46882
+ positive = true;
46883
+ order = 1 + sigDecs;
46884
+ if (order != 0) {
46885
+ zeroScientific = true;
47204
46886
  }
47205
- numCallbacks++;
47206
- data.ref?.restoreSerializedState(state, countCallback);
47207
- });
47208
- }
47209
-
47210
- /**
47211
- * Given an array of renderers, if it happens to be an array of *hint*
47212
- * renderers, then attach a `firstN` method to the array, which allows the
47213
- * layout to render the hints together in one HintsRenderer.
47214
- */
47215
- _annotateRendererArray(renderers, rendererDatas, shape) {
47216
- if (shape.elementShape.type === "hint") {
47217
- // The shape says that these are HintRendererDatas, even though
47218
- // it's not provable at compile time, so perform a cast.
47219
- const hintRendererDatas = rendererDatas;
47220
- renderers = [...renderers];
47221
- renderers.firstN = n => /*#__PURE__*/React__namespace.createElement(HintsRenderer, _extends({}, this._getRendererProps(), {
47222
- findExternalWidgets: hintRendererDatas[0] ? hintRendererDatas[0].findExternalWidgets : undefined,
47223
- hints: hintRendererDatas.map(d => d.hint),
47224
- hintsVisible: n
47225
- }));
47226
46887
  }
47227
- return renderers;
47228
46888
  }
47229
-
47230
- /**
47231
- * Return a tree in the shape of the multi-item, with a Renderer at each
47232
- * content node and a HintRenderer at each hint node.
47233
- *
47234
- * This is generated by running each of the `makeRenderer` functions at the
47235
- * leaf nodes.
47236
- */
47237
- _getRenderers() {
47238
- return this.getRenderersMapper.mapTree(this.state.rendererDataTree, this.props.shape);
46889
+ const useScientific = scientific || mantissa.length > 20 || order > 4 || order < -2 || order - mantissa.length > 0 && trailingZeros(mantissa) > 0 || zeroScientific;
46890
+ let returnVal = "";
46891
+ if (!positive) {
46892
+ returnVal += "-";
47239
46893
  }
47240
- render() {
47241
- if (this.state.renderError) {
47242
- return /*#__PURE__*/React__namespace.createElement("div", {
47243
- className: aphrodite.css(styles.error)
47244
- }, this.context.strings.errorRendering({
47245
- error: String(this.state.renderError)
47246
- }));
46894
+ if (useScientific) {
46895
+ returnVal += mantissa.charAt(0);
46896
+ if (mantissa.length > 1) {
46897
+ returnVal += "." + mantissa.substring(1, mantissa.length);
47247
46898
  }
47248
-
47249
- // Pass the renderer tree to the `children` function, which will
47250
- // determine the actual content of this component.
47251
- return /*#__PURE__*/React__namespace.createElement(DependenciesContext.Provider, {
47252
- value: this.props.dependencies
47253
- }, this.props.children({
47254
- renderers: this._getRenderers()
47255
- }));
46899
+ if (order - 1 != 0) {
46900
+ returnVal += " x 10^" + (order - 1);
46901
+ }
46902
+ } else {
46903
+ let wholePart = "";
46904
+ let fractPart = "";
46905
+ let needDot = true;
46906
+ if (order > 0) {
46907
+ if (mantissa.length > order) {
46908
+ wholePart = mantissa.substring(0, order);
46909
+ fractPart = mantissa.substring(order, mantissa.length);
46910
+ } else {
46911
+ wholePart = mantissa;
46912
+ needDot = trailingZeros(mantissa) != 0;
46913
+ for (let i = 0; i < order - mantissa.length; i++) {
46914
+ wholePart += "0";
46915
+ }
46916
+ }
46917
+ } else {
46918
+ for (let i = 0; i < -order; i++) {
46919
+ fractPart += "0";
46920
+ }
46921
+ fractPart += mantissa;
46922
+ }
46923
+ returnVal += (wholePart == "" ? "0" : wholePart) + (needDot ? "." : "") + fractPart;
47256
46924
  }
46925
+ return returnVal;
47257
46926
  }
47258
- const styles = aphrodite.StyleSheet.create({
47259
- error: {
47260
- color: "red"
47261
- }
47262
- });
47263
46927
 
47264
46928
  /**
47265
- * Utility functions to build React PropTypes for multi-items and shapes.
46929
+ * Count the significant trailing zeros on this object.
47266
46930
  *
47267
- * If you're writing new components, though, consider using the Item and Shape
47268
- * types instead.
46931
+ * @return the number of trailing zeros
47269
46932
  */
46933
+ function trailingZeros(mantissa) {
46934
+ let zeros = 0;
46935
+ for (let i = mantissa.length - 1; i >= 0; i--) {
46936
+ const c = mantissa.charAt(i);
46937
+ if (c == "0") {
46938
+ zeros++;
46939
+ } else {
46940
+ return zeros;
46941
+ }
46942
+ }
46943
+ return zeros;
46944
+ }
47270
46945
 
47271
46946
  /**
47272
- * Return a PropType that accepts Items of the given shape, and rejects other
47273
- * objects.
46947
+ * Parse a string representation of a floating point
46948
+ * and pull out the sign.
47274
46949
  *
47275
- * Usage: `propTypes: {item: buildPropTypeForShape(myShape)}`
46950
+ * @param s the string representation of a floating point.
46951
+ * @return true iff this is a positive number
47276
46952
  */
47277
- function buildPropTypeForShape(shape) {
47278
- return PropTypes__default["default"].oneOfType([PropTypes__default["default"].shape({
47279
- _multi: buildTreePropTypeForShape(shape)
47280
- }), PropTypes__default["default"].oneOf([null, undefined])]);
46953
+ function parseSign(s) {
46954
+ let beginning = true;
46955
+ let seenDot = false;
46956
+ let seenSomething = false;
46957
+ let zeros = "";
46958
+ let all = "";
46959
+ let pos = true;
46960
+ for (let i = 0; i < s.length; i++) {
46961
+ const c = s.charAt(i);
46962
+ if (c >= "1" && c <= "9") {
46963
+ all += zeros + c;
46964
+ zeros = "";
46965
+ seenSomething = true;
46966
+ beginning = false;
46967
+ } else if (c == "0") {
46968
+ if (seenDot) {
46969
+ if (seenSomething) {
46970
+ all += zeros + c;
46971
+ zeros = "";
46972
+ }
46973
+ } else {
46974
+ if (seenSomething) {
46975
+ zeros += c;
46976
+ }
46977
+ }
46978
+ beginning = false;
46979
+ } else if (!seenDot && c == ".") {
46980
+ all += zeros;
46981
+ zeros = "";
46982
+ seenDot = true;
46983
+ beginning = false;
46984
+ } else if (c == "e" || c == "E" && i + 1 < s.length) {
46985
+ parseInt(s.substring(i + 1, s.length));
46986
+ i = s.length;
46987
+ } else if (beginning && (c == "+" || c == "-")) {
46988
+ if (c == "-") {
46989
+ pos = !pos;
46990
+ }
46991
+ }
46992
+ }
46993
+ if (all == "") {
46994
+ return true;
46995
+ }
46996
+ return pos;
47281
46997
  }
47282
46998
 
47283
46999
  /**
47284
- * Return a PropType that accepts ItemTrees of the given shape, and rejects
47285
- * other objects.
47000
+ * Parse a string representation of a floating point
47001
+ * and pull out the mantissa.
47002
+ *
47003
+ * @param s the string representation of a floating point.
47004
+ * @return the mantissa of this number.
47286
47005
  */
47287
- function buildTreePropTypeForShape(shape) {
47288
- if (shape.type === "content") {
47289
- return PropTypes__default["default"].shape({
47290
- // TODO(mdr): Remove #LegacyContentNode support.
47291
- __type: PropTypes__default["default"].oneOf(["content", "item"]).isRequired,
47292
- content: PropTypes__default["default"].string,
47293
- images: PropTypes__default["default"].objectOf(PropTypes__default["default"].any),
47294
- widgets: PropTypes__default["default"].objectOf(PropTypes__default["default"].any)
47295
- });
47296
- }
47297
- if (shape.type === "hint") {
47298
- return PropTypes__default["default"].shape({
47299
- __type: PropTypes__default["default"].oneOf(["hint"]).isRequired,
47300
- content: PropTypes__default["default"].string,
47301
- images: PropTypes__default["default"].objectOf(PropTypes__default["default"].any),
47302
- widgets: PropTypes__default["default"].objectOf(PropTypes__default["default"].any),
47303
- replace: PropTypes__default["default"].bool
47304
- });
47006
+ function parseMantissa(s) {
47007
+ let seenDot = false;
47008
+ let seenSomething = false;
47009
+ let zeros = "";
47010
+ let leadZeros = "";
47011
+ let all = "";
47012
+ for (let i = 0; i < s.length; i++) {
47013
+ const c = s.charAt(i);
47014
+ if (c >= "1" && c <= "9") {
47015
+ all += zeros + c;
47016
+ zeros = "";
47017
+ seenSomething = true;
47018
+ } else if (c == "0") {
47019
+ if (seenDot) {
47020
+ if (seenSomething) {
47021
+ all += zeros + c;
47022
+ zeros = "";
47023
+ } else {
47024
+ leadZeros += c;
47025
+ }
47026
+ } else {
47027
+ if (seenSomething) {
47028
+ leadZeros += c;
47029
+ zeros += c;
47030
+ } else {
47031
+ leadZeros += c;
47032
+ }
47033
+ }
47034
+ } else if (!seenDot && c == ".") {
47035
+ all += zeros;
47036
+ zeros = "";
47037
+ seenDot = true;
47038
+ } else if (c == "e" || c == "E" && i + 1 < s.length) {
47039
+ parseInt(s.substring(i + 1, s.length));
47040
+ i = s.length;
47041
+ } else ;
47305
47042
  }
47306
- if (shape.type === "tags") {
47307
- return PropTypes__default["default"].arrayOf(PropTypes__default["default"].string.isRequired);
47043
+ if (all == "") {
47044
+ return leadZeros;
47308
47045
  }
47309
- if (shape.type === "array") {
47310
- const elementPropType = buildTreePropTypeForShape(shape.elementShape);
47311
- return PropTypes__default["default"].arrayOf(elementPropType.isRequired);
47046
+ return all;
47047
+ }
47048
+
47049
+ /**
47050
+ * Parse a string representation of a floating point
47051
+ * and pull out the exponent.
47052
+ *
47053
+ * @param s the string representation of a floating point.
47054
+ * @return (integer) the number after the e.
47055
+ */
47056
+ function parseOrder(s) {
47057
+ let seenDot = false;
47058
+ let seenSomething = false;
47059
+ let zeros = "";
47060
+ let all = "";
47061
+ let decPlaces = 0;
47062
+ let totalDecs = 0;
47063
+ for (let i = 0; i < s.length; i++) {
47064
+ const c = s.charAt(i);
47065
+ if (c >= "1" && c <= "9") {
47066
+ all += zeros + c;
47067
+ zeros = "";
47068
+ seenSomething = true;
47069
+ if (!seenDot) {
47070
+ totalDecs++;
47071
+ decPlaces++;
47072
+ }
47073
+ } else if (c == "0") {
47074
+ if (seenDot) {
47075
+ if (seenSomething) {
47076
+ all += zeros + c;
47077
+ zeros = "";
47078
+ } else {
47079
+ decPlaces--;
47080
+ }
47081
+ } else {
47082
+ totalDecs++;
47083
+ if (seenSomething) {
47084
+ decPlaces++;
47085
+ zeros += c;
47086
+ }
47087
+ }
47088
+ } else if (!seenDot && c == ".") {
47089
+ all += zeros;
47090
+ zeros = "";
47091
+ seenDot = true;
47092
+ } else if (c == "e" || c == "E" && i + 1 < s.length) {
47093
+ const raised = parseInt(s.substring(i + 1, s.length));
47094
+ decPlaces += raised;
47095
+ totalDecs += raised;
47096
+ i = s.length;
47097
+ } else ;
47312
47098
  }
47313
- if (shape.type === "object") {
47314
- const valueShapes = shape.shape;
47315
- const propTypeShape = {};
47316
- Object.keys(valueShapes).forEach(key => {
47317
- propTypeShape[key] = buildTreePropTypeForShape(valueShapes[key]).isRequired;
47318
- });
47319
- return PropTypes__default["default"].shape(propTypeShape);
47099
+ if (all == "") {
47100
+ return totalDecs;
47320
47101
  }
47321
- throw new perseusCore.PerseusError(
47322
- // @ts-expect-error - TS2339 - Property 'type' does not exist on type 'never'.
47323
- `unexpected shape type ${shape.type}`, perseusCore.Errors.InvalidInput);
47102
+ return decPlaces;
47324
47103
  }
47325
47104
 
47326
47105
  /**
47327
- * This library provides support for Perseus multi-items: structured Perseus
47328
- * content that content creators can easily create, and that applications can
47329
- * easily render into different parts of the layout.
47330
- *
47331
- * For more details about application and motivation, see:
47332
- * https://sites.google.com/a/khanacademy.org/forge/for-developers/perseus-items-and-multi-items
47333
- *
47334
- * This file primarily exposes the `MultiRenderer` component, which performs
47335
- * multi-rendering. To multi-render a question, pass in the content of the item
47336
- * to the `MultiRenderer` component as a props. Then, pass in a function which
47337
- * takes an object of renderers (in the same structure as the content), and
47338
- * return a render tree. The `MultiRenderer` component will allow you to
47339
- * combine scores, serialized state, etc. without having to manually call on
47340
- * each of the functions. It also handles inter-widgets requests between the
47341
- * different renderers.
47342
- * For more details, see `multi-items/multi-renderer.jsx`.
47343
- *
47344
- * Example:
47345
- *
47346
- * item = {_multi: {
47347
- * left: <content data>,
47348
- * right: [<content data>, <content data>],
47349
- * }}
47350
- * shape = shapes.shape({
47351
- * left: shapes.content,
47352
- * right: shapes.arrayOf(shapes.content),
47353
- * })
47354
- *
47355
- * <MultiRenderer item={item} shape={shape}>
47356
- * {({renderers}) =>
47357
- * <div>
47358
- * <div id="left">{renderers.left}</div>
47359
- * <ul id="right">
47360
- * {renderers.right.map(r => <li>{r}</li>)}
47361
- * </ul>
47362
- * </div>
47363
- * }
47364
- * </MultiRenderer>
47365
- *
47366
- * This file also exposes `shapes`, which helps you construct a runtime type
47367
- * declaration for your particular class of multi-item. This can then be used
47368
- * to create a MultirendererEditor for your multi-item shape, and to validate
47369
- * that a multi-item conforms to the shape via `buildPropTypeForShape`.
47370
- * For more details, see `multi-items/shapes.js`.
47371
- *
47372
- * This file also exposes some utility functions for working with generic
47373
- * multi-items, like `findContentNodesInItem`, `findHintNodesInItem`,
47374
- * `inferItemShape`, and `buildEmptyItemForShape`.
47375
- * For more details, see `multi-items/items.js`.
47376
- */
47377
- var MultiItems = {
47378
- // Tools for rendering your multi-items
47379
- MultiRenderer,
47380
- // Tools for declaring your multi-item shapes
47381
- shapes,
47382
- buildPropTypeForShape,
47383
- // Tools for generically manipulating multi-items
47384
- buildEmptyItemForShape,
47385
- findContentNodesInItem: findContentNodesInItem$2,
47386
- findHintNodesInItem,
47387
- inferItemShape: inferItemShape$2
47388
- };
47389
-
47390
- /**
47391
- * Calculates whether a perseus item is renderable by a specific
47392
- * perseus-item-data version.
47106
+ * Remove the specified number of digits from string by
47107
+ * rounding. Proper rounding rules for scientific purposes
47108
+ * are followed.
47109
+ * This method may cause an extra significant figure
47110
+ * to be added to the number. For example, if 999999
47111
+ * were rounded, A one would carry over and become
47112
+ * a significant figure. Those who call this method
47113
+ * should check for this and call round again if needed.
47393
47114
  *
47394
- * This is done inside of the perseus repo so that it can traverse
47395
- * widget-specific data that might need to do a sub-traversal.
47396
- * This supports widgets that contain renderers, such as the
47397
- * group or sequence widgets.
47115
+ * @param mantissa A string representing an whole number of arbitrary length.
47116
+ * @param digits A number of digits to remove
47117
+ * @return A string represted the rounded version of mantissa
47398
47118
  */
47399
- const {
47400
- findContentNodesInItem: findContentNodesInItem$1,
47401
- inferItemShape: inferItemShape$1
47402
- } = MultiItems;
47403
- const isUpgradedWidgetInfoRenderableBy = function (widgetInfo, widgetRendererVersion) {
47404
- if (widgetRendererVersion == null) {
47405
- // If the widget does not exist in this version, this will
47406
- // be null, and that version of perseus cannot render the
47407
- // widget (it doesn't even know the widget exists!)
47408
- return false;
47409
- }
47410
- const widgetVersion = widgetInfo.version || {
47411
- major: 0,
47412
- minor: 0
47413
- };
47414
- if (widgetRendererVersion.major > widgetVersion.major) {
47415
- return true;
47416
- }
47417
- if (widgetRendererVersion.major < widgetVersion.major) {
47418
- return false;
47419
- }
47420
- // If the major versions are the same, the minor version acts
47421
- // like a tie-breaker.
47422
- // For example, input-number 3.2 can render an input-number
47423
- // 2.4, 3.0, or 3.2, but not an input number 3.3 or 4.0.
47424
- return widgetRendererVersion.minor >= widgetVersion.minor;
47425
- };
47426
- const isRawWidgetInfoRenderableBy = function (widgetInfo, rendererContentVersion) {
47427
- // Empty/non-existant widgets are always safe to render
47428
- if (widgetInfo == null || widgetInfo.type == null) {
47429
- return true;
47119
+ function round(mantissa, digits) {
47120
+ const last = mantissa.length - digits - 1;
47121
+ if (last < 0) {
47122
+ return "";
47430
47123
  }
47431
-
47432
- // NOTE: This doesn't modify the widget info if the widget info
47433
- // is at a later version than is supported.
47434
- const upgradedWidgetInfo = upgradeWidgetInfoToLatestVersion(widgetInfo);
47435
- return isUpgradedWidgetInfoRenderableBy(upgradedWidgetInfo, rendererContentVersion[upgradedWidgetInfo.type]);
47436
- };
47437
- const isRendererContentRenderableBy = function (rendererOptions, rendererContentVersion) {
47438
- let isRenderable = true;
47439
- traverse(rendererOptions, null, function (widgetInfo) {
47440
- isRenderable = isRenderable && isRawWidgetInfoRenderableBy(widgetInfo, rendererContentVersion);
47441
- });
47442
- return isRenderable;
47443
- };
47444
- const isItemRenderableByVersion = function (itemData, rendererContentVersion) {
47445
- if (itemData == null || rendererContentVersion == null) {
47446
- throw new perseusCore.PerseusError("missing parameter to Perseus.isRenderable.item", perseusCore.Errors.InvalidInput);
47124
+ if (last >= mantissa.length - 1) {
47125
+ return mantissa;
47447
47126
  }
47448
- if (itemData._multi) {
47449
- const shape = inferItemShape$1(itemData);
47450
- let isRenderable = true;
47451
- findContentNodesInItem$1(itemData, shape, node => {
47452
- const nodeIsRenderable = isRendererContentRenderableBy(node, rendererContentVersion);
47453
- if (!nodeIsRenderable) {
47454
- isRenderable = false;
47127
+ const nextToLast = mantissa.charAt(last + 1);
47128
+ const lastChar = mantissa.charAt(last);
47129
+ let roundUp = false;
47130
+ if (nextToLast > "5") {
47131
+ roundUp = true;
47132
+ } else if (nextToLast == "5") {
47133
+ for (let j = last + 2; j < mantissa.length; j++) {
47134
+ if (mantissa.charAt(j) != "0") {
47135
+ roundUp = true;
47455
47136
  }
47456
- });
47457
- return isRenderable;
47137
+ }
47138
+ // @ts-expect-error - TS2362 - The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
47139
+ if (lastChar % 2 == 1) {
47140
+ roundUp = true;
47141
+ }
47458
47142
  }
47459
- return isRendererContentRenderableBy(itemData.question, rendererContentVersion);
47460
- };
47461
-
47462
- /**
47463
- * Identifies whether or not a given perseus item requires the use of a mouse
47464
- * or screen, based on the widgets it contains.
47465
- */
47466
- const {
47467
- findContentNodesInItem,
47468
- inferItemShape
47469
- } = MultiItems;
47470
-
47471
- // Iterate over a single Perseus renderer, mutating `widgets` by appending
47472
- // violating widget types discovered in this item.
47473
- function traverseRenderer(itemData, widgets) {
47474
- traverse(itemData, null, function (info) {
47475
- if (info.type && !isAccessible(info)) {
47476
- widgets.push(info.type);
47143
+ let result = "";
47144
+ for (let i = last; i >= 0; i--) {
47145
+ const c = mantissa.charAt(i);
47146
+ if (roundUp) {
47147
+ let nextChar;
47148
+ if (c == "9") {
47149
+ nextChar = "0";
47150
+ } else {
47151
+ switch (c) {
47152
+ case "0":
47153
+ nextChar = "1";
47154
+ break;
47155
+ case "1":
47156
+ nextChar = "2";
47157
+ break;
47158
+ case "2":
47159
+ nextChar = "3";
47160
+ break;
47161
+ case "3":
47162
+ nextChar = "4";
47163
+ break;
47164
+ case "4":
47165
+ nextChar = "5";
47166
+ break;
47167
+ case "5":
47168
+ nextChar = "6";
47169
+ break;
47170
+ case "6":
47171
+ nextChar = "7";
47172
+ break;
47173
+ case "7":
47174
+ nextChar = "8";
47175
+ break;
47176
+ case "8":
47177
+ nextChar = "9";
47178
+ break;
47179
+ }
47180
+ roundUp = false;
47181
+ }
47182
+ result = nextChar + result;
47183
+ } else {
47184
+ result = c + result;
47477
47185
  }
47478
- });
47479
- }
47480
-
47481
- // Returns a list of widgets that cause a given perseus item to require
47482
- // the use of a screen or mouse.
47483
- //
47484
- // For now we'll just check the `accessible` field on each of the widgets
47485
- // in the item data, but in the future we may specify accessibility on
47486
- // each widget with higher granularity.
47487
- function violatingWidgets(itemData) {
47488
- // TODO(jordan): Hints as well
47489
- const widgets = [];
47490
- if (itemData._multi) {
47491
- const shape = inferItemShape(itemData);
47492
- findContentNodesInItem(itemData, shape, content => traverseRenderer(content, widgets));
47493
- } else {
47494
- traverseRenderer(itemData.question, widgets);
47495
47186
  }
47496
-
47497
- // Uniquify the list of widgets (by type)
47498
- return ___default["default"].uniq(widgets);
47187
+ if (roundUp) {
47188
+ result = "1" + result;
47189
+ }
47190
+ return result;
47499
47191
  }
47500
47192
 
47501
47193
  /**
@@ -48219,9 +47911,6 @@ function generateTestInteractiveGraphWidget() {
48219
47911
  markings: "graph",
48220
47912
  labels: ["x", "y"],
48221
47913
  showProtractor: false,
48222
- showRuler: false,
48223
- rulerLabel: "mm",
48224
- rulerTicks: 2,
48225
47914
  range: [[-10, 10], [-10, 10]],
48226
47915
  graph: {
48227
47916
  type: "angle"
@@ -48333,7 +48022,6 @@ const widgetNameToEnum = {
48333
48022
  simulator: "SIMULATOR",
48334
48023
  sorter: "SORTER",
48335
48024
  table: "TABLE",
48336
- "unit-input": "UNIT",
48337
48025
  video: "VIDEO"
48338
48026
  };
48339
48027
  function convertWidgetNameToEnum(name) {
@@ -48415,6 +48103,8 @@ exports.getAnswerFromUserInput = getAnswerFromUserInput;
48415
48103
  exports.getAnswersFromWidgets = getAnswersFromWidgets;
48416
48104
  exports.getCorrectAnswerForWidgetId = getCorrectAnswerForWidgetId;
48417
48105
  exports.getLineCoords = getLineCoords;
48106
+ exports.getLinearSystemCoords = getLinearSystemCoords;
48107
+ exports.getSegmentCoords = getSegmentCoords;
48418
48108
  exports.getValidWidgetIds = getValidWidgetIds;
48419
48109
  exports.getWidgetFromWidgetMap = getWidgetFromWidgetMap;
48420
48110
  exports.getWidgetIdsFromContent = getWidgetIdsFromContent;