@khanacademy/perseus 48.0.0 → 48.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -81,6 +81,8 @@ export { addWidget, QUESTION_WIDGETS } from "./util/snowman-utils";
81
81
  export { getCircleCoords, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getSegmentCoords, getSinusoidCoords, getQuadraticCoords, getAngleCoords, } from "./widgets/interactive-graphs/reducer/initialize-graph-state";
82
82
  export { getClockwiseAngle } from "./widgets/interactive-graphs/math";
83
83
  export { makeSafeUrl } from "./widgets/phet-simulation";
84
+ export { parseDataFromJSONP } from "./util/graphie-utils";
85
+ export type { GraphieData, GraphieLabel, GraphieRange, } from "./util/graphie-utils";
84
86
  /**
85
87
  * Mixins
86
88
  */
package/dist/index.js CHANGED
@@ -3494,6 +3494,163 @@ const Util = {
3494
3494
  deepClone
3495
3495
  };
3496
3496
 
3497
+ // For offline exercises in the mobile app, we download the graphie data
3498
+ // (svgs and localized data files) and serve them from the local file
3499
+ // system (with file://). We replace urls that start with `web+graphie`
3500
+ // in the perseus json with this `file+graphie` prefix to indicate that
3501
+ // they should have the `file://` protocol instead of `https://`.
3502
+ const svgLocalLabelsRegex = /^file\+graphie:/;
3503
+ const hashRegex = /\/([^/]+)$/;
3504
+
3505
+ /**
3506
+ * A Graphie axis range.
3507
+ */
3508
+
3509
+ /**
3510
+ * A Graphie label.
3511
+ */
3512
+
3513
+ /**
3514
+ * The Graphie data (labels, etc.)
3515
+ */
3516
+
3517
+ function getLocale() {
3518
+ const {
3519
+ JIPT,
3520
+ kaLocale
3521
+ } = getDependencies();
3522
+ return JIPT.useJIPT ? "en-pt" : kaLocale;
3523
+ }
3524
+ function shouldUseLocalizedData() {
3525
+ return getLocale() !== "en";
3526
+ }
3527
+
3528
+ // A regex to split at the last / of a URL, separating the base part from the
3529
+ // hash. This is used to create the localized label data URLs.
3530
+ const splitHashRegex = /\/(?=[^/]+$)/;
3531
+ function getLocalizedDataUrl(url) {
3532
+ // For local (cached) graphie images, they are already localized.
3533
+ if (svgLocalLabelsRegex.test(url)) {
3534
+ return Util.getDataUrl(url);
3535
+ }
3536
+ const [base, hash] = Util.getBaseUrl(url).split(splitHashRegex);
3537
+ return `${base}/${getLocale()}/${hash}-data.json`;
3538
+ }
3539
+
3540
+ // Get the hash from the url, which is just the filename
3541
+ function getUrlHash(url) {
3542
+ const match = url.match(hashRegex);
3543
+ if (match == null) {
3544
+ throw new perseusCore.PerseusError("not a valid URL", perseusCore.Errors.InvalidInput);
3545
+ }
3546
+ return match && match[1];
3547
+ }
3548
+
3549
+ /**
3550
+ * Parse JSONP for a web+graphie://... hash to extract Graphie data, or throw
3551
+ * an error if invalid JSONP.
3552
+ */
3553
+ function parseDataFromJSONP(graphieHash, graphieJSONP, errorCallback) {
3554
+ // The JSONP is expected to be in the form of `svgDataHASH(...)` or `svgOtherDataHASH(...)`
3555
+ const match = graphieJSONP.match(new RegExp(`^(?:svgData|svgOtherData)${graphieHash}\\((.+)\\);$`));
3556
+
3557
+ // It is also possible that the JSONP is simply a JSON object,
3558
+ // in which case we can parse it directly.
3559
+ const jsonToParse = match ? match[1] : graphieJSONP;
3560
+
3561
+ // Try to parse the JSONP, and if it fails, call the error callback
3562
+ try {
3563
+ return JSON.parse(jsonToParse);
3564
+ } catch (error) {
3565
+ errorCallback(error);
3566
+ return null;
3567
+ }
3568
+ }
3569
+ // The global cache of label data. Its format is:
3570
+ // {
3571
+ // hash (e.g. "c21435944d2cf0c8f39d9059cb35836aa701d04a"): {
3572
+ // loaded: a boolean of whether the data has been loaded or not
3573
+ // dataCallbacks: a list of callbacks to call with the data when the data
3574
+ // is loaded
3575
+ // data: the other data for this hash
3576
+ // },
3577
+ // ...
3578
+ // }
3579
+ const labelDataCache = {};
3580
+ function loadGraphie(url, onDataLoaded) {
3581
+ const hash = getUrlHash(url);
3582
+
3583
+ // We can't make multiple jsonp calls to the same file because their
3584
+ // callbacks will collide with each other. Instead, we cache the data
3585
+ // and only make the jsonp calls once.
3586
+ const entry = labelDataCache[hash];
3587
+ if (entry != null) {
3588
+ if (entry.loaded) {
3589
+ const {
3590
+ data,
3591
+ localized
3592
+ } = entry;
3593
+ onDataLoaded(data, localized);
3594
+ } else {
3595
+ entry.dataCallbacks.push(onDataLoaded);
3596
+ }
3597
+ } else {
3598
+ const cacheData = {
3599
+ loaded: false,
3600
+ dataCallbacks: [onDataLoaded],
3601
+ localized: shouldUseLocalizedData()
3602
+ };
3603
+ labelDataCache[hash] = cacheData;
3604
+ const retrieveData = async (url, errorCallback) => {
3605
+ const response = await fetch(url);
3606
+ if (!response?.ok) {
3607
+ errorCallback();
3608
+ return;
3609
+ }
3610
+ const jsonp = await response.text();
3611
+ const data = parseDataFromJSONP(hash, jsonp, error => {
3612
+ Log.error("Failed to parse JSONP for svg-image", perseusCore.Errors.Service, {
3613
+ cause: error,
3614
+ loggedMetadata: {
3615
+ dataUrl: Util.getDataUrl(url),
3616
+ jsonp
3617
+ }
3618
+ });
3619
+ });
3620
+
3621
+ // If the data is null, then we failed to parse the JSONP
3622
+ if (!data) {
3623
+ return;
3624
+ }
3625
+ const newCacheEntry = labelDataCache[hash] = {
3626
+ ...labelDataCache[hash],
3627
+ loaded: true,
3628
+ data
3629
+ };
3630
+ newCacheEntry.dataCallbacks.forEach(callback => {
3631
+ callback(newCacheEntry.data, cacheData.localized);
3632
+ });
3633
+ };
3634
+ const dataLoadErrorHandler = (x, status, error) => {
3635
+ Log.error("Data load failed for svg-image", perseusCore.Errors.Service, {
3636
+ cause: error,
3637
+ loggedMetadata: {
3638
+ dataUrl: Util.getDataUrl(url),
3639
+ status
3640
+ }
3641
+ });
3642
+ };
3643
+ if (shouldUseLocalizedData()) {
3644
+ cacheData.localized = false;
3645
+ // If there is isn't any localized data, fall back to
3646
+ // the original, unlocalized data
3647
+ retrieveData(getLocalizedDataUrl(url), dataLoadErrorHandler);
3648
+ } else {
3649
+ retrieveData(Util.getDataUrl(url), dataLoadErrorHandler);
3650
+ }
3651
+ }
3652
+ }
3653
+
3497
3654
  // Derived from the MIT-licensed:
3498
3655
 
3499
3656
  /*jshint browser:true, node:true */
@@ -8925,6 +9082,9 @@ class Graphie$1 {
8925
9082
  $__default["default"](el).css("position", "relative");
8926
9083
  this.raphael = Raphael$1(el);
8927
9084
 
9085
+ // Hide the Raphael canvas from screen readers
9086
+ $__default["default"](el).attr("aria-hidden", "true");
9087
+
8928
9088
  // For a sometimes-reproducible IE8 bug; doesn't affect SVG browsers at all
8929
9089
  $__default["default"](el).children("div").css("position", "absolute");
8930
9090
  }
@@ -17002,106 +17162,11 @@ class ImageLoader extends React__namespace.Component {
17002
17162
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
17003
17163
  // Minimum image width to make an image appear as zoomable.
17004
17164
  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
17165
  function isImageProbablyPhotograph(imageUrl) {
17070
17166
  // TODO(david): Do an inventory to refine this heuristic. For example, what
17071
17167
  // % of .png images are illustrations?
17072
17168
  return /\.(jpg|jpeg)$/i.test(imageUrl);
17073
17169
  }
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
17170
  function defaultPreloader(dimensions) {
17106
17171
  return /*#__PURE__*/React__namespace.createElement("span", {
17107
17172
  style: {
@@ -17188,90 +17253,22 @@ class SvgImage extends React__namespace.Component {
17188
17253
  return Util.isLabeledSVG(this.props.src) ? state.imageLoaded && state.dataLoaded : state.imageLoaded;
17189
17254
  }
17190
17255
  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
17228
- });
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
- });
17256
+ loadGraphie(this.props.src, (data, localized) => {
17257
+ if (this._isMounted && data.labels && data.range) {
17258
+ const labelsRendered = {};
17259
+ data.labels.forEach(label => {
17260
+ labelsRendered[label.content] = false;
17246
17261
  });
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
- });
17262
+ this.setState({
17263
+ dataLoaded: true,
17264
+ labelDataIsLocalized: localized,
17265
+ labelsRendered,
17266
+ labels: data.labels,
17267
+ range: data.range
17256
17268
  });
17257
17269
  }
17258
- }
17270
+ });
17259
17271
  }
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
17272
  sizeProvided() {
17276
17273
  return this.props.width != null && this.props.height != null;
17277
17274
  }
@@ -45530,7 +45527,7 @@ const init = function () {
45530
45527
 
45531
45528
  // This file is processed by a Rollup plugin (replace) to inject the production
45532
45529
  const libName = "@khanacademy/perseus";
45533
- const libVersion = "48.0.0";
45530
+ const libVersion = "48.1.0";
45534
45531
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45535
45532
 
45536
45533
  const apiVersion = {
@@ -49196,6 +49193,7 @@ exports.lockedFigureFillStyles = lockedFigureFillStyles;
49196
49193
  exports.makeSafeUrl = makeSafeUrl;
49197
49194
  exports.mathOnlyParser = mathOnlyParser;
49198
49195
  exports.mediaQueries = mediaQueries;
49196
+ exports.parseDataFromJSONP = parseDataFromJSONP;
49199
49197
  exports.parsePerseusItem = parsePerseusItem;
49200
49198
  exports.plotterPlotTypes = plotterPlotTypes;
49201
49199
  exports.preprocessTex = preprocessTex;