@khanacademy/perseus 48.0.0 → 48.2.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
@@ -368,6 +368,8 @@ const mockStrings = {
368
368
  addPoint: "Add Point",
369
369
  removePoint: "Remove Point",
370
370
  graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
371
+ selectAnAnswer: "Select an answer",
372
+ // The following strings are used for interactive graph SR descriptions.
371
373
  closePolygon: "Close shape",
372
374
  openPolygon: "Re-open shape",
373
375
  srPointAtCoordinates: _ref27 => {
@@ -385,7 +387,41 @@ const mockStrings = {
385
387
  return `Interactive elements: ${elements}`;
386
388
  },
387
389
  srNoInteractiveElements: "No interactive elements",
388
- selectAnAnswer: "Select an answer"
390
+ circleGraphAriaLabel: "A circle on a coordinate plane.",
391
+ circleShapeAriaLabel: _ref29 => {
392
+ let {
393
+ centerX,
394
+ centerY
395
+ } = _ref29;
396
+ return `Circle. The center point is at ${centerX} comma ${centerY}.`;
397
+ },
398
+ circleRadiusPointAriaLabel: _ref30 => {
399
+ let {
400
+ radiusPointX,
401
+ radiusPointY
402
+ } = _ref30;
403
+ return `Radius point at ${radiusPointX} comma ${radiusPointY}.`;
404
+ },
405
+ circleRadiusDescription: _ref31 => {
406
+ let {
407
+ radius
408
+ } = _ref31;
409
+ return `Circle radius is ${radius}.`;
410
+ },
411
+ circleOuterPointsDescription: _ref32 => {
412
+ let {
413
+ point1X,
414
+ point1Y,
415
+ point2X,
416
+ point2Y,
417
+ point3X,
418
+ point3Y,
419
+ point4X,
420
+ point4Y
421
+ } = _ref32;
422
+ return `Points on the circle at ${point1X} comma ${point1Y}, ${point2X} comma ${point2Y}, ${point3X} comma ${point3Y}, ${point4X} comma ${point4Y}.`;
423
+ }
424
+ // The above strings are used for interactive graph SR descriptions.
389
425
  };
390
426
 
391
427
  /**
@@ -3494,6 +3530,163 @@ const Util = {
3494
3530
  deepClone
3495
3531
  };
3496
3532
 
3533
+ // For offline exercises in the mobile app, we download the graphie data
3534
+ // (svgs and localized data files) and serve them from the local file
3535
+ // system (with file://). We replace urls that start with `web+graphie`
3536
+ // in the perseus json with this `file+graphie` prefix to indicate that
3537
+ // they should have the `file://` protocol instead of `https://`.
3538
+ const svgLocalLabelsRegex = /^file\+graphie:/;
3539
+ const hashRegex = /\/([^/]+)$/;
3540
+
3541
+ /**
3542
+ * A Graphie axis range.
3543
+ */
3544
+
3545
+ /**
3546
+ * A Graphie label.
3547
+ */
3548
+
3549
+ /**
3550
+ * The Graphie data (labels, etc.)
3551
+ */
3552
+
3553
+ function getLocale() {
3554
+ const {
3555
+ JIPT,
3556
+ kaLocale
3557
+ } = getDependencies();
3558
+ return JIPT.useJIPT ? "en-pt" : kaLocale;
3559
+ }
3560
+ function shouldUseLocalizedData() {
3561
+ return getLocale() !== "en";
3562
+ }
3563
+
3564
+ // A regex to split at the last / of a URL, separating the base part from the
3565
+ // hash. This is used to create the localized label data URLs.
3566
+ const splitHashRegex = /\/(?=[^/]+$)/;
3567
+ function getLocalizedDataUrl(url) {
3568
+ // For local (cached) graphie images, they are already localized.
3569
+ if (svgLocalLabelsRegex.test(url)) {
3570
+ return Util.getDataUrl(url);
3571
+ }
3572
+ const [base, hash] = Util.getBaseUrl(url).split(splitHashRegex);
3573
+ return `${base}/${getLocale()}/${hash}-data.json`;
3574
+ }
3575
+
3576
+ // Get the hash from the url, which is just the filename
3577
+ function getUrlHash(url) {
3578
+ const match = url.match(hashRegex);
3579
+ if (match == null) {
3580
+ throw new perseusCore.PerseusError("not a valid URL", perseusCore.Errors.InvalidInput);
3581
+ }
3582
+ return match && match[1];
3583
+ }
3584
+
3585
+ /**
3586
+ * Parse JSONP for a web+graphie://... hash to extract Graphie data, or throw
3587
+ * an error if invalid JSONP.
3588
+ */
3589
+ function parseDataFromJSONP(graphieHash, graphieJSONP, errorCallback) {
3590
+ // The JSONP is expected to be in the form of `svgDataHASH(...)` or `svgOtherDataHASH(...)`
3591
+ const match = graphieJSONP.match(new RegExp(`^(?:svgData|svgOtherData)${graphieHash}\\((.+)\\);$`));
3592
+
3593
+ // It is also possible that the JSONP is simply a JSON object,
3594
+ // in which case we can parse it directly.
3595
+ const jsonToParse = match ? match[1] : graphieJSONP;
3596
+
3597
+ // Try to parse the JSONP, and if it fails, call the error callback
3598
+ try {
3599
+ return JSON.parse(jsonToParse);
3600
+ } catch (error) {
3601
+ errorCallback(error);
3602
+ return null;
3603
+ }
3604
+ }
3605
+ // The global cache of label data. Its format is:
3606
+ // {
3607
+ // hash (e.g. "c21435944d2cf0c8f39d9059cb35836aa701d04a"): {
3608
+ // loaded: a boolean of whether the data has been loaded or not
3609
+ // dataCallbacks: a list of callbacks to call with the data when the data
3610
+ // is loaded
3611
+ // data: the other data for this hash
3612
+ // },
3613
+ // ...
3614
+ // }
3615
+ const labelDataCache = {};
3616
+ function loadGraphie(url, onDataLoaded) {
3617
+ const hash = getUrlHash(url);
3618
+
3619
+ // We can't make multiple jsonp calls to the same file because their
3620
+ // callbacks will collide with each other. Instead, we cache the data
3621
+ // and only make the jsonp calls once.
3622
+ const entry = labelDataCache[hash];
3623
+ if (entry != null) {
3624
+ if (entry.loaded) {
3625
+ const {
3626
+ data,
3627
+ localized
3628
+ } = entry;
3629
+ onDataLoaded(data, localized);
3630
+ } else {
3631
+ entry.dataCallbacks.push(onDataLoaded);
3632
+ }
3633
+ } else {
3634
+ const cacheData = {
3635
+ loaded: false,
3636
+ dataCallbacks: [onDataLoaded],
3637
+ localized: shouldUseLocalizedData()
3638
+ };
3639
+ labelDataCache[hash] = cacheData;
3640
+ const retrieveData = async (url, errorCallback) => {
3641
+ const response = await fetch(url);
3642
+ if (!response?.ok) {
3643
+ errorCallback();
3644
+ return;
3645
+ }
3646
+ const jsonp = await response.text();
3647
+ const data = parseDataFromJSONP(hash, jsonp, error => {
3648
+ Log.error("Failed to parse JSONP for svg-image", perseusCore.Errors.Service, {
3649
+ cause: error,
3650
+ loggedMetadata: {
3651
+ dataUrl: Util.getDataUrl(url),
3652
+ jsonp
3653
+ }
3654
+ });
3655
+ });
3656
+
3657
+ // If the data is null, then we failed to parse the JSONP
3658
+ if (!data) {
3659
+ return;
3660
+ }
3661
+ const newCacheEntry = labelDataCache[hash] = {
3662
+ ...labelDataCache[hash],
3663
+ loaded: true,
3664
+ data
3665
+ };
3666
+ newCacheEntry.dataCallbacks.forEach(callback => {
3667
+ callback(newCacheEntry.data, cacheData.localized);
3668
+ });
3669
+ };
3670
+ const dataLoadErrorHandler = (x, status, error) => {
3671
+ Log.error("Data load failed for svg-image", perseusCore.Errors.Service, {
3672
+ cause: error,
3673
+ loggedMetadata: {
3674
+ dataUrl: Util.getDataUrl(url),
3675
+ status
3676
+ }
3677
+ });
3678
+ };
3679
+ if (shouldUseLocalizedData()) {
3680
+ cacheData.localized = false;
3681
+ // If there is isn't any localized data, use the error callback to try to
3682
+ // load the non-localized data. If that fails too, then we will throw an error.
3683
+ retrieveData(getLocalizedDataUrl(url), () => retrieveData(Util.getDataUrl(url), dataLoadErrorHandler));
3684
+ } else {
3685
+ retrieveData(Util.getDataUrl(url), dataLoadErrorHandler);
3686
+ }
3687
+ }
3688
+ }
3689
+
3497
3690
  // Derived from the MIT-licensed:
3498
3691
 
3499
3692
  /*jshint browser:true, node:true */
@@ -8925,6 +9118,9 @@ class Graphie$1 {
8925
9118
  $__default["default"](el).css("position", "relative");
8926
9119
  this.raphael = Raphael$1(el);
8927
9120
 
9121
+ // Hide the Raphael canvas from screen readers
9122
+ $__default["default"](el).attr("aria-hidden", "true");
9123
+
8928
9124
  // For a sometimes-reproducible IE8 bug; doesn't affect SVG browsers at all
8929
9125
  $__default["default"](el).children("div").css("position", "absolute");
8930
9126
  }
@@ -17002,106 +17198,11 @@ class ImageLoader extends React__namespace.Component {
17002
17198
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
17003
17199
  // Minimum image width to make an image appear as zoomable.
17004
17200
  const ZOOMABLE_THRESHOLD = 700;
17005
-
17006
- // The global cache of label data. Its format is:
17007
- // {
17008
- // hash (e.g. "c21435944d2cf0c8f39d9059cb35836aa701d04a"): {
17009
- // loaded: a boolean of whether the data has been loaded or not
17010
- // dataCallbacks: a list of callbacks to call with the data when the data
17011
- // is loaded
17012
- // data: the other data for this hash
17013
- // },
17014
- // ...
17015
- // }
17016
- const labelDataCache = {};
17017
-
17018
- // Write our own JSONP handler because all the other ones don't do things we
17019
- // need.
17020
- const doJSONP = function (url, options) {
17021
- options = {
17022
- callbackName: "callback",
17023
- success: $__default["default"].noop,
17024
- error: $__default["default"].noop,
17025
- ...options
17026
- };
17027
-
17028
- // Create the script
17029
- const script = document.createElement("script");
17030
- script.setAttribute("async", "");
17031
- script.setAttribute("src", url);
17032
-
17033
- // A cleanup function to run when we're done.
17034
- function cleanup() {
17035
- document.head?.removeChild(script);
17036
- delete window[options.callbackName];
17037
- }
17038
-
17039
- // Add the global callback.
17040
- // @ts-expect-error - TS2740 - Type '() => void' is missing the following properties from type 'Window': clientInformation, closed, customElements, devicePixelRatio, and 206 more.
17041
- window[options.callbackName] = function () {
17042
- cleanup();
17043
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
17044
- args[_key] = arguments[_key];
17045
- }
17046
- options.success.apply(null, args);
17047
- };
17048
-
17049
- // Add the error handler.
17050
- script.addEventListener("error", function () {
17051
- cleanup();
17052
- for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
17053
- args[_key2] = arguments[_key2];
17054
- }
17055
- options.error.apply(null, args);
17056
- });
17057
-
17058
- // Insert the script to start the download.
17059
- document.head?.appendChild(script);
17060
- };
17061
-
17062
- // For offline exercises in the mobile app, we download the graphie data
17063
- // (svgs and localized data files) and serve them from the local file
17064
- // system (with file://). We replace urls that start with `web+graphie`
17065
- // in the perseus json with this `file+graphie` prefix to indicate that
17066
- // they should have the `file://` protocol instead of `https://`.
17067
- const svgLocalLabelsRegex = /^file\+graphie:/;
17068
- const hashRegex = /\/([^/]+)$/;
17069
17201
  function isImageProbablyPhotograph(imageUrl) {
17070
17202
  // TODO(david): Do an inventory to refine this heuristic. For example, what
17071
17203
  // % of .png images are illustrations?
17072
17204
  return /\.(jpg|jpeg)$/i.test(imageUrl);
17073
17205
  }
17074
- function getLocale() {
17075
- const {
17076
- JIPT,
17077
- kaLocale
17078
- } = getDependencies();
17079
- return JIPT.useJIPT ? "en-pt" : kaLocale;
17080
- }
17081
- function shouldUseLocalizedData() {
17082
- return getLocale() !== "en";
17083
- }
17084
-
17085
- // A regex to split at the last / of a URL, separating the base part from the
17086
- // hash. This is used to create the localized label data URLs.
17087
- const splitHashRegex = /\/(?=[^/]+$)/;
17088
- function getLocalizedDataUrl(url) {
17089
- // For local (cached) graphie images, they are already localized.
17090
- if (svgLocalLabelsRegex.test(url)) {
17091
- return Util.getDataUrl(url);
17092
- }
17093
- const [base, hash] = Util.getBaseUrl(url).split(splitHashRegex);
17094
- return `${base}/${getLocale()}/${hash}-data.json`;
17095
- }
17096
-
17097
- // Get the hash from the url, which is just the filename
17098
- function getUrlHash(url) {
17099
- const match = url.match(hashRegex);
17100
- if (match == null) {
17101
- throw new perseusCore.PerseusError("not a valid URL", perseusCore.Errors.InvalidInput);
17102
- }
17103
- return match && match[1];
17104
- }
17105
17206
  function defaultPreloader(dimensions) {
17106
17207
  return /*#__PURE__*/React__namespace.createElement("span", {
17107
17208
  style: {
@@ -17188,90 +17289,22 @@ class SvgImage extends React__namespace.Component {
17188
17289
  return Util.isLabeledSVG(this.props.src) ? state.imageLoaded && state.dataLoaded : state.imageLoaded;
17189
17290
  }
17190
17291
  loadResources() {
17191
- const hash = getUrlHash(this.props.src);
17192
-
17193
- // We can't make multiple jsonp calls to the same file because their
17194
- // callbacks will collide with each other. Instead, we cache the data
17195
- // and only make the jsonp calls once.
17196
- if (labelDataCache[hash]) {
17197
- if (labelDataCache[hash].loaded) {
17198
- const {
17199
- data,
17200
- localized
17201
- } = labelDataCache[hash];
17202
- this.onDataLoaded(data, localized);
17203
- } else {
17204
- labelDataCache[hash].dataCallbacks.push(this.onDataLoaded);
17205
- }
17206
- } else {
17207
- const cacheData = {
17208
- loaded: false,
17209
- dataCallbacks: [this.onDataLoaded],
17210
- data: null,
17211
- localized: shouldUseLocalizedData()
17212
- };
17213
- labelDataCache[hash] = cacheData;
17214
- const retrieveData = (url, errorCallback) => {
17215
- doJSONP(url, {
17216
- callbackName: "svgData" + hash,
17217
- success: data => {
17218
- // @ts-expect-error - TS2540 - Cannot assign to 'data' because it is a read-only property.
17219
- cacheData.data = data;
17220
- // @ts-expect-error - TS2540 - Cannot assign to 'loaded' because it is a read-only property.
17221
- cacheData.loaded = true;
17222
- ___default["default"].each(cacheData.dataCallbacks, callback => {
17223
- // @ts-expect-error - TS2345 - Argument of type 'null' is not assignable to parameter of type '{ labels: readonly any[]; range: readonly any[]; }'.
17224
- callback(cacheData.data, cacheData.localized);
17225
- });
17226
- },
17227
- error: errorCallback
17292
+ loadGraphie(this.props.src, (data, localized) => {
17293
+ if (this._isMounted && data.labels && data.range) {
17294
+ const labelsRendered = {};
17295
+ data.labels.forEach(label => {
17296
+ labelsRendered[label.content] = false;
17228
17297
  });
17229
- };
17230
- if (shouldUseLocalizedData()) {
17231
- retrieveData(getLocalizedDataUrl(this.props.src), (x, status, error) => {
17232
- // @ts-expect-error - TS2540 - Cannot assign to 'localized' because it is a read-only property.
17233
- cacheData.localized = false;
17234
-
17235
- // If there is isn't any localized data, fall back to
17236
- // the original, unlocalized data
17237
- retrieveData(Util.getDataUrl(this.props.src), (x, status, error) => {
17238
- Log.error("Data load failed for svg-image", perseusCore.Errors.Service, {
17239
- cause: error,
17240
- loggedMetadata: {
17241
- dataUrl: Util.getDataUrl(this.props.src),
17242
- status
17243
- }
17244
- });
17245
- });
17246
- });
17247
- } else {
17248
- retrieveData(Util.getDataUrl(this.props.src), (x, status, error) => {
17249
- Log.error("Data load failed for svg-image", perseusCore.Errors.Service, {
17250
- cause: error,
17251
- loggedMetadata: {
17252
- dataUrl: Util.getDataUrl(this.props.src),
17253
- status
17254
- }
17255
- });
17298
+ this.setState({
17299
+ dataLoaded: true,
17300
+ labelDataIsLocalized: localized,
17301
+ labelsRendered,
17302
+ labels: data.labels,
17303
+ range: data.range
17256
17304
  });
17257
17305
  }
17258
- }
17306
+ });
17259
17307
  }
17260
- onDataLoaded = (data, localized) => {
17261
- if (this._isMounted && data.labels && data.range) {
17262
- const labelsRendered = data.labels.reduce((dict, label) => ({
17263
- ...dict,
17264
- [label.content]: false
17265
- }), {});
17266
- this.setState({
17267
- dataLoaded: true,
17268
- labelDataIsLocalized: localized,
17269
- labelsRendered,
17270
- labels: data.labels,
17271
- range: data.range
17272
- });
17273
- }
17274
- };
17275
17308
  sizeProvided() {
17276
17309
  return this.props.width != null && this.props.height != null;
17277
17310
  }
@@ -34464,21 +34497,96 @@ function CircleGraph(props) {
34464
34497
  center,
34465
34498
  radiusPoint
34466
34499
  } = graphState;
34467
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(MovableCircle, {
34500
+ const {
34501
+ strings
34502
+ } = usePerseusI18n();
34503
+ const [radiusPointAriaLive, setRadiusPointAriaLive] = React__namespace.useState("off");
34504
+ const radius = getRadius(graphState);
34505
+ const id = React__namespace.useId();
34506
+ const circleId = id + "-circle";
34507
+ const radiusId = id + "-radius";
34508
+ const outerPointsId = id + "-outer-points";
34509
+
34510
+ // Aria label strings
34511
+ const circleGraphAriaLabel = strings.circleGraphAriaLabel;
34512
+ const circleShapeAriaLabel = strings.circleShapeAriaLabel({
34513
+ centerX: center[0],
34514
+ centerY: center[1]
34515
+ });
34516
+ const circleRadiusPointAriaLabel = strings.circleRadiusPointAriaLabel({
34517
+ radiusPointX: radiusPoint[0],
34518
+ radiusPointY: radiusPoint[1]
34519
+ });
34520
+ const circleRadiusDescription = strings.circleRadiusDescription({
34521
+ radius
34522
+ });
34523
+ const circleOuterPointsDescription = strings.circleOuterPointsDescription({
34524
+ point1X: center[0] + radius,
34525
+ point1Y: center[1],
34526
+ point2X: center[0],
34527
+ point2Y: center[1] + radius,
34528
+ point3X: center[0] - radius,
34529
+ point3Y: center[1],
34530
+ point4X: center[0],
34531
+ point4Y: center[1] - radius
34532
+ });
34533
+ return /*#__PURE__*/React__namespace.createElement("g", {
34534
+ // Outer circle minimal description
34535
+ "aria-label": circleGraphAriaLabel,
34536
+ "aria-describedby": `${circleId} ${radiusId} ${outerPointsId}`
34537
+ }, /*#__PURE__*/React__namespace.createElement(MovableCircle, {
34538
+ id: circleId
34539
+ // Focusable circle aria label reads with every update
34540
+ // because of the aria-live property in the circle <g>.
34541
+ ,
34542
+ ariaLabel: circleShapeAriaLabel
34543
+ // Aria-describedby describes additional info on focus.
34544
+ ,
34545
+ ariaDescribedBy: `${radiusId} ${outerPointsId}`,
34468
34546
  center: center,
34469
- radius: getRadius(graphState),
34470
- onMove: c => dispatch(actions.circle.moveCenter(c))
34471
- }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34547
+ radius: radius,
34548
+ onMove: c => {
34549
+ setRadiusPointAriaLive("off");
34550
+ dispatch(actions.circle.moveCenter(c));
34551
+ }
34552
+ }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1
34553
+ // Radius point aria label reads with every update.
34554
+ , {
34555
+ ariaLabel: `${circleRadiusPointAriaLabel} ${circleRadiusDescription}`
34556
+ // Aria-describedby describes additional info on focus.
34557
+ ,
34558
+ ariaDescribedBy: `${outerPointsId}`
34559
+ // The radius point's aria-live property is set to "off" when
34560
+ // the circle is moved, so that it doesn't override the circle's
34561
+ // aria-live (since the point is moved along with the circle).
34562
+ // When the radius point is moved, the aria-live is set to
34563
+ // "polite" so that the radius is read out.
34564
+ ,
34565
+ ariaLive: radiusPointAriaLive,
34472
34566
  point: radiusPoint,
34473
34567
  sequenceNumber: 1,
34474
34568
  cursor: "ew-resize",
34475
34569
  onMove: newRadiusPoint => {
34570
+ setRadiusPointAriaLive("polite");
34476
34571
  dispatch(actions.circle.moveRadiusPoint(newRadiusPoint));
34477
34572
  }
34478
- }));
34573
+ }), /*#__PURE__*/React__namespace.createElement("g", {
34574
+ id: radiusId,
34575
+ style: {
34576
+ display: "hidden"
34577
+ }
34578
+ }, circleRadiusDescription), /*#__PURE__*/React__namespace.createElement("g", {
34579
+ id: outerPointsId,
34580
+ style: {
34581
+ display: "hidden"
34582
+ }
34583
+ }, circleOuterPointsDescription));
34479
34584
  }
34480
34585
  function MovableCircle(props) {
34481
34586
  const {
34587
+ id,
34588
+ ariaLabel,
34589
+ ariaDescribedBy,
34482
34590
  center,
34483
34591
  radius,
34484
34592
  onMove
@@ -34499,7 +34607,11 @@ function MovableCircle(props) {
34499
34607
  const [centerPx] = useTransformVectorsToPixels(center);
34500
34608
  const [radiiPx] = useTransformDimensionsToPixels([radius, radius]);
34501
34609
  return /*#__PURE__*/React__namespace.createElement("g", {
34610
+ "aria-label": ariaLabel,
34611
+ "aria-describedby": ariaDescribedBy,
34612
+ "aria-live": "polite",
34502
34613
  ref: draggableRef,
34614
+ role: "button",
34503
34615
  tabIndex: disableKeyboardInteraction ? -1 : 0,
34504
34616
  className: `movable-circle ${dragging ? "movable-circle--dragging" : ""}`
34505
34617
  }, /*#__PURE__*/React__namespace.createElement("ellipse", {
@@ -34509,6 +34621,7 @@ function MovableCircle(props) {
34509
34621
  rx: radiiPx[X] + 3,
34510
34622
  ry: radiiPx[Y] + 3
34511
34623
  }), /*#__PURE__*/React__namespace.createElement("ellipse", {
34624
+ id: id,
34512
34625
  className: "circle",
34513
34626
  cx: centerPx[X],
34514
34627
  cy: centerPx[Y],
@@ -45530,7 +45643,7 @@ const init = function () {
45530
45643
 
45531
45644
  // This file is processed by a Rollup plugin (replace) to inject the production
45532
45645
  const libName = "@khanacademy/perseus";
45533
- const libVersion = "48.0.0";
45646
+ const libVersion = "48.2.0";
45534
45647
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45535
45648
 
45536
45649
  const apiVersion = {
@@ -49196,6 +49309,7 @@ exports.lockedFigureFillStyles = lockedFigureFillStyles;
49196
49309
  exports.makeSafeUrl = makeSafeUrl;
49197
49310
  exports.mathOnlyParser = mathOnlyParser;
49198
49311
  exports.mediaQueries = mediaQueries;
49312
+ exports.parseDataFromJSONP = parseDataFromJSONP;
49199
49313
  exports.parsePerseusItem = parsePerseusItem;
49200
49314
  exports.plotterPlotTypes = plotterPlotTypes;
49201
49315
  exports.preprocessTex = preprocessTex;