@khanacademy/perseus 18.0.1 → 19.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/es/index.css +209 -128
  3. package/dist/es/index.js +1026 -3516
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +209 -128
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +917 -3409
  8. package/dist/index.js.map +1 -1
  9. package/dist/mixins/changeable.d.ts +1 -1
  10. package/dist/perseus-api.d.ts +1 -0
  11. package/dist/perseus-types.d.ts +2 -109
  12. package/dist/renderer.d.ts +2 -4
  13. package/dist/server-item-renderer.d.ts +1 -0
  14. package/dist/strings.js +0 -29
  15. package/dist/types.d.ts +4 -4
  16. package/dist/util/geometry.d.ts +2 -0
  17. package/dist/util/graphie.d.ts +107 -1
  18. package/dist/util/math.d.ts +2 -0
  19. package/dist/util/parse-perseus-json.d.ts +3 -3
  20. package/dist/widgets/expression.d.ts +8 -2
  21. package/dist/widgets/label-image.d.ts +2 -0
  22. package/dist/widgets.d.ts +1 -1
  23. package/package.json +3 -4
  24. package/src/__testdata__/renderer.testdata.ts +152 -0
  25. package/src/__tests__/renderer.test.tsx +17 -0
  26. package/src/__tests__/widgets.test.ts +0 -1
  27. package/src/components/math-input.tsx +4 -6
  28. package/src/extra-widgets.ts +0 -2
  29. package/src/index.ts +8 -4
  30. package/src/mixins/changeable.ts +1 -1
  31. package/src/perseus-types.ts +1 -154
  32. package/src/renderer.tsx +14 -8
  33. package/src/styles/perseus-renderer.less +0 -1
  34. package/src/types.ts +9 -5
  35. package/src/util/geometry.test.ts +75 -0
  36. package/src/util/geometry.ts +19 -0
  37. package/src/util/graphie.test.ts +1291 -0
  38. package/src/util/graphie.ts +832 -784
  39. package/src/util/interactive.ts +23 -51
  40. package/src/util/math.test.ts +11 -0
  41. package/src/util/math.ts +8 -0
  42. package/src/util/parse-perseus-json.test.ts +3 -3
  43. package/src/util/parse-perseus-json.ts +4 -4
  44. package/src/util/register-all-widgets-for-testing.ts +1 -3
  45. package/src/widgets/__tests__/__snapshots__/input-number.test.ts.snap +4 -2
  46. package/src/widgets/__tests__/__snapshots__/numeric-input.test.ts.snap +4 -2
  47. package/src/widgets/__tests__/expression-mobile.test.tsx +8 -5
  48. package/src/widgets/__tests__/renderQuestion.tsx +11 -3
  49. package/src/widgets/expression.tsx +1 -23
  50. package/src/widgets/label-image.tsx +0 -1
  51. package/src/widgets.ts +1 -9
  52. package/tsconfig-build.tsbuildinfo +1 -1
  53. package/dist/widgets/transformer.d.ts +0 -94
  54. package/src/styles/widgets/transformer.less +0 -29
  55. package/src/widgets/__stories__/transformer.stories.tsx +0 -14
  56. package/src/widgets/__testdata__/transformer.testdata.ts +0 -107
  57. package/src/widgets/transformer.tsx +0 -2936
@@ -31,6 +31,7 @@ import {Errors} from "../logging/log";
31
31
  import {PerseusError} from "../perseus-error";
32
32
 
33
33
  import KhanColors from "./colors";
34
+ import {clockwise, reverseVector} from "./geometry";
34
35
  import GraphUtils, {polar} from "./graphie";
35
36
  import KhanMath from "./math";
36
37
 
@@ -40,38 +41,6 @@ export type MouseHandler = (position: Coord) => void;
40
41
 
41
42
  const {getCanUse3dTransform} = InteractiveUtil;
42
43
 
43
- function sum(array: any) {
44
- return _.reduce(
45
- array,
46
- function (memo, arg) {
47
- return memo + arg;
48
- },
49
- 0,
50
- );
51
- }
52
-
53
- function clockwise(points: any) {
54
- const segments = _.zip(points, points.slice(1).concat(points.slice(0, 1)));
55
- const areas = _.map(segments, function (segment) {
56
- const p1 = segment[0];
57
- const p2 = segment[1];
58
- return (p2[0] - p1[0]) * (p2[1] + p1[1]);
59
- });
60
- return sum(areas) > 0;
61
- }
62
-
63
- /* vector-add multiple [x, y] coords/vectors */
64
- function addPoints(...points) {
65
- const zipped = _.zip(...points);
66
- return _.map(zipped, sum);
67
- }
68
-
69
- function reverseVector(vector: Array<number>) {
70
- return _.map(vector, function (coord) {
71
- return coord * -1;
72
- });
73
- }
74
-
75
44
  function scaledDistanceFromAngle(angle: number) {
76
45
  const a = 3.51470560176242 * 20;
77
46
  const b = 0.5687298702748785 * 20;
@@ -79,14 +48,14 @@ function scaledDistanceFromAngle(angle: number) {
79
48
  return (a - b) * Math.exp(c * angle) + b;
80
49
  }
81
50
 
82
- function scaledPolarRad(radius: number, radians: number) {
51
+ function scaledPolarRad(radius: number, radians: number): Coord {
83
52
  return [
84
53
  radius * Math.cos(radians),
85
54
  radius * Math.sin(radians) * -1, // SVG flips y axis
86
55
  ];
87
56
  }
88
57
 
89
- function scaledPolarDeg(radius: number, degrees) {
58
+ function scaledPolarDeg(radius: number, degrees): Coord {
90
59
  const radians = (degrees * Math.PI) / 180;
91
60
  return scaledPolarRad(radius, radians);
92
61
  }
@@ -245,7 +214,7 @@ _.extend(GraphUtils.Graphie.prototype, {
245
214
  /**
246
215
  * Get mouse coordinates in graph coordinates
247
216
  */
248
- getMouseCoord: function (event) {
217
+ getMouseCoord: function (event): Coord {
249
218
  return this.unscalePoint(this.getMousePx(event));
250
219
  },
251
220
 
@@ -291,11 +260,14 @@ _.extend(GraphUtils.Graphie.prototype, {
291
260
  const temp: Array<never> = [];
292
261
 
293
262
  if (Math.abs(angle - 90) < 1e-9 && options.showRightAngleMarker) {
294
- const v1 = addPoints(sVertex, scaledPolarDeg(sRadius, startAngle));
295
- const v2 = addPoints(sVertex, scaledPolarDeg(sRadius, endAngle));
263
+ const v1 = kvector.add(
264
+ sVertex,
265
+ scaledPolarDeg(sRadius, startAngle),
266
+ );
267
+ const v2 = kvector.add(sVertex, scaledPolarDeg(sRadius, endAngle));
296
268
 
297
269
  sRadius *= Math.SQRT2;
298
- const v3 = addPoints(sVertex, scaledPolarDeg(sRadius, halfAngle));
270
+ const v3 = kvector.add(sVertex, scaledPolarDeg(sRadius, halfAngle));
299
271
 
300
272
  _.each([v1, v2], function (v) {
301
273
  // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
@@ -329,7 +301,7 @@ _.extend(GraphUtils.Graphie.prototype, {
329
301
  }
330
302
 
331
303
  const sOffset = scaledPolarDeg(sRadius + 15, halfAngle);
332
- const sPosition = addPoints(sVertex, sOffset);
304
+ const sPosition = kvector.add(sVertex, sOffset);
333
305
  const position = graphie.unscalePoint(sPosition);
334
306
 
335
307
  // Reuse label if possible
@@ -395,8 +367,8 @@ _.extend(GraphUtils.Graphie.prototype, {
395
367
  );
396
368
 
397
369
  const sPath = [
398
- addPoints(sMidpoint, sOffsetVector, sHeightVector),
399
- addPoints(
370
+ kvector.add(sMidpoint, sOffsetVector, sHeightVector),
371
+ kvector.add(
400
372
  sMidpoint,
401
373
  sOffsetVector,
402
374
  reverseVector(sHeightVector),
@@ -436,7 +408,7 @@ _.extend(GraphUtils.Graphie.prototype, {
436
408
  sOffsetVector = reverseVector(sOffsetVector);
437
409
  }
438
410
 
439
- const sEnd = addPoints(sMidpoint, sOffsetVector);
411
+ const sEnd = kvector.add(sMidpoint, sOffsetVector);
440
412
 
441
413
  // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
442
414
  temp.push(graphie.scaledPath([sStart, sEnd], style));
@@ -457,7 +429,7 @@ _.extend(GraphUtils.Graphie.prototype, {
457
429
 
458
430
  const sOffset = 20;
459
431
  const sOffsetVector = scaledPolarRad(sOffset, perpendicularAngle);
460
- const sPosition = addPoints(sMidpoint, sOffsetVector);
432
+ const sPosition = kvector.add(sMidpoint, sOffsetVector);
461
433
  const position = graphie.unscalePoint(sPosition);
462
434
 
463
435
  // Reuse label if possible
@@ -522,7 +494,7 @@ _.extend(GraphUtils.Graphie.prototype, {
522
494
 
523
495
  const sRadius = 10 + scaledDistanceFromAngle(360 - angle);
524
496
  const sOffsetVector = scaledPolarDeg(sRadius, halfAngle);
525
- const sPosition = addPoints(sVertex, sOffsetVector);
497
+ const sPosition = kvector.add(sVertex, sOffsetVector);
526
498
  const position = graphie.unscalePoint(sPosition);
527
499
 
528
500
  // Reuse label if possible
@@ -3225,7 +3197,7 @@ _.extend(GraphUtils.Graphie.prototype, {
3225
3197
  return new Ruler(this, options || {});
3226
3198
  },
3227
3199
 
3228
- addPoints: addPoints,
3200
+ addPoints: kvector.add,
3229
3201
  });
3230
3202
 
3231
3203
  function Protractor(graph: any, center: any) {
@@ -3679,7 +3651,7 @@ function Ruler(graphie: any, options: any) {
3679
3651
  });
3680
3652
 
3681
3653
  const mouseTarget = graphie.mouselayer.path(
3682
- GraphUtils.svgPath([
3654
+ graphie.svgPath([
3683
3655
  leftBottom,
3684
3656
  [left, top],
3685
3657
  rightTop,
@@ -3970,16 +3942,16 @@ _.extend(MovableAngle.prototype, {
3970
3942
  const points = this.points;
3971
3943
 
3972
3944
  // Drag the vertex to move the entire angle
3973
- points[1].onMove = function (x, y: any) {
3945
+ points[1].onMove = function (x: number, y: number) {
3974
3946
  const oldVertex = points[1].coord;
3975
- const newVertex = [x, y];
3976
- const delta = addPoints(newVertex, reverseVector(oldVertex));
3947
+ const newVertex: Coord = [x, y];
3948
+ const delta = kvector.add(newVertex, reverseVector(oldVertex));
3977
3949
 
3978
3950
  let valid = true;
3979
3951
  const newPoints: Record<string, any> = {};
3980
3952
  _.each([0, 2], function (i) {
3981
3953
  const oldPoint = points[i].coord;
3982
- let newPoint = addPoints(oldPoint, delta);
3954
+ let newPoint = kvector.add(oldPoint, delta);
3983
3955
 
3984
3956
  let angle = GraphUtils.findAngle(newVertex, newPoint);
3985
3957
  angle *= Math.PI / 180;
@@ -4024,7 +3996,7 @@ _.extend(MovableAngle.prototype, {
4024
3996
  Math.round((angle - snapOffset) / snap) * snap +
4025
3997
  snapOffset;
4026
3998
  const distance = GraphUtils.getDistance(newPoint, vertex);
4027
- return addPoints(vertex, polar(distance, angle));
3999
+ return kvector.add(vertex, polar(distance, angle));
4028
4000
  }
4029
4001
  return true;
4030
4002
  };
@@ -0,0 +1,11 @@
1
+ import {sum} from "./math";
2
+
3
+ describe("sum", () => {
4
+ it("returns 0 given an empty array", () => {
5
+ expect(sum([])).toBe(0);
6
+ });
7
+
8
+ it("adds the numbers in the array", () => {
9
+ expect(sum([1, 2, 5])).toBe(8);
10
+ });
11
+ });
package/src/util/math.ts CHANGED
@@ -283,4 +283,12 @@ const KhanMath = {
283
283
  },
284
284
  } as const;
285
285
 
286
+ export function sum(array: number[]): number {
287
+ return array.reduce(add, 0);
288
+ }
289
+
290
+ export function add(a: number, b: number): number {
291
+ return a + b;
292
+ }
293
+
286
294
  export default KhanMath;
@@ -1,8 +1,8 @@
1
- import parsePerseusJSON from "./parse-perseus-json";
1
+ import {parsePerseusItem} from "./parse-perseus-json";
2
2
 
3
- describe("parsePerseusJSON", () => {
3
+ describe("parsePerseusItem", () => {
4
4
  it("should parse JSON", () => {
5
- const result = parsePerseusJSON(
5
+ const result = parsePerseusItem(
6
6
  `{ "question": { "content": "idk why I'm testing this" }}`,
7
7
  );
8
8
  expect(result.question.content).toBe("idk why I'm testing this");
@@ -1,13 +1,13 @@
1
1
  import type {PerseusItem} from "../perseus-types";
2
2
 
3
3
  /**
4
- * Helper to parse Perseus JSON
4
+ * Helper to parse PerseusItem JSON
5
5
  * Why not just use JSON.parse? We want:
6
6
  * - To make sure types are correct
7
7
  * - To give us a central place to validate/transform output if needed
8
- * @param {string} input - the stringified Perseus JSON
8
+ * @param {string} json - the stringified PerseusItem JSON
9
9
  * @returns {PerseusItem} the parsed PerseusItem object
10
10
  */
11
- export default function parsePerseusJSON(input: string): PerseusItem {
12
- return JSON.parse(input);
11
+ export function parsePerseusItem(json: string): PerseusItem {
12
+ return JSON.parse(json);
13
13
  }
@@ -9,7 +9,5 @@ import * as Widgets from "../widgets";
9
9
  export const registerAllWidgetsForTesting = () => {
10
10
  Widgets.registerWidgets(allWidgets);
11
11
 
12
- // When it's time to replace the transformer widget
13
- // uncomment this line
14
- // Widgets.replaceWidget("transformer", "deprecated-standin");
12
+ Widgets.replaceWidget("transformer", "deprecated-standin");
15
13
  };
@@ -181,11 +181,13 @@ exports[`rendering supports mobile rendering: mobile render 1`] = `
181
181
  <span
182
182
  class="mq-textarea"
183
183
  >
184
- <span />
184
+ <span
185
+ aria-label="Math Input:"
186
+ />
185
187
  </span>
186
188
  <span
189
+ aria-hidden="true"
187
190
  class="mq-root-block mq-empty"
188
- mathquill-block-id="1"
189
191
  style="padding: 10px 11px 8px 11px; font-size: 18pt;"
190
192
  />
191
193
  </div>
@@ -43,11 +43,13 @@ exports[`Numeric input widget styles differently on mobile: mobile render 1`] =
43
43
  <span
44
44
  class="mq-textarea"
45
45
  >
46
- <span />
46
+ <span
47
+ aria-label="Math Input:"
48
+ />
47
49
  </span>
48
50
  <span
51
+ aria-hidden="true"
49
52
  class="mq-root-block mq-empty"
50
- mathquill-block-id="1"
51
53
  style="padding: 10px 11px 8px 11px; font-size: 18pt;"
52
54
  />
53
55
  </div>
@@ -3,6 +3,7 @@ import {
3
3
  KeypadContext,
4
4
  StatefulKeypadContextProvider,
5
5
  MobileKeypad,
6
+ mathQuillInstance,
6
7
  } from "@khanacademy/math-input";
7
8
  import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
8
9
  import {
@@ -13,7 +14,6 @@ import {
13
14
  within,
14
15
  } from "@testing-library/react";
15
16
  import userEvent from "@testing-library/user-event";
16
- import MathQuill from "mathquill";
17
17
  import * as React from "react";
18
18
 
19
19
  import {
@@ -26,7 +26,7 @@ import {registerWidget} from "../../widgets";
26
26
  import {expressionItem2} from "../__testdata__/expression.testdata";
27
27
  import ExpressionExport from "../expression";
28
28
 
29
- const MQ = MathQuill.getInterface(2);
29
+ const MQ = mathQuillInstance;
30
30
 
31
31
  function RendererWithContext({item}) {
32
32
  return (
@@ -351,10 +351,13 @@ describe("expression mobile", () => {
351
351
 
352
352
  // MathQuill is problematic,
353
353
  // this is how to get the value of the input directly from MQ
354
- const mathquillInstance =
354
+ const mathquillInstance = MQ(
355
355
  // eslint-disable-next-line testing-library/no-node-access
356
- MQ(document.getElementsByClassName("mq-editable-field")[0]);
356
+ document.getElementsByClassName(
357
+ "mq-editable-field",
358
+ )[0] as HTMLElement,
359
+ );
357
360
 
358
- expect(mathquillInstance.latex()).toBe("x=\\sin\\left(9\\right)");
361
+ expect(mathquillInstance?.latex()).toBe("x=\\sin\\left(9\\right)");
359
362
  });
360
363
  });
@@ -2,9 +2,16 @@ import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
2
2
  import {render} from "@testing-library/react";
3
3
  import * as React from "react";
4
4
 
5
- // eslint-disable-next-line import/no-relative-packages
6
- import {testDependenciesV2} from "../../../../../testing/test-dependencies";
7
- import {DependenciesContext, useDependencies} from "../../dependencies";
5
+ import {
6
+ testDependenciesV2,
7
+ testDependencies,
8
+ // eslint-disable-next-line import/no-relative-packages
9
+ } from "../../../../../testing/test-dependencies";
10
+ import {
11
+ DependenciesContext,
12
+ useDependencies,
13
+ setDependencies,
14
+ } from "../../dependencies";
8
15
  import * as Perseus from "../../index";
9
16
  import {registerAllWidgetsForTesting} from "../../util/register-all-widgets-for-testing";
10
17
 
@@ -27,6 +34,7 @@ export const renderQuestion = (
27
34
  ) => void;
28
35
  unmount: RenderResult["unmount"];
29
36
  } => {
37
+ setDependencies(testDependencies);
30
38
  registerAllWidgetsForTesting();
31
39
 
32
40
  let renderer: Perseus.Renderer | null = null;
@@ -35,28 +35,6 @@ type InputPath = ReadonlyArray<string>;
35
35
  const ERROR_TITLE = i18n._("Oops!");
36
36
  const ERROR_MESSAGE = i18n._("Sorry, I don't understand that!");
37
37
 
38
- const insertBraces = (value) => {
39
- // HACK(alex): Make sure that all LaTeX super/subscripts are wrapped
40
- // in curly braces to avoid the mismatch between KAS and LaTeX sup/sub
41
- // parsing.
42
- //
43
- // What exactly is this mismatch? Due to its heritage of parsing plain
44
- // text math from <OldExpression />, KAS parses "x^12" as x^(12).
45
- // This is both generally what the user expects to happen, and is
46
- // consistent with other computer algebra systems. It is NOT
47
- // consistent with LaTeX however, where x^12 is equivalent to x^{1}2.
48
- //
49
- // Since the only LaTeX we parse comes from MathQuill, this wouldn't
50
- // be a problem if MathQuill just always gave us the latter version
51
- // (with explicit braces). However, instead it always gives the former.
52
- // This behavior is baked in pretty deep; my naive attempts at changing
53
- // it triggered all sorts of confusing errors. So instead we just make
54
- // sure to add in any missing braces before grading MathQuill input.
55
- //
56
- // TODO(alex): Properly hack MathQuill to always use explicit braces.
57
- return value.replace(/([_^])([^{])/g, "$1{$2}");
58
- };
59
-
60
38
  const anglicizeOperators = (tex: string): string => {
61
39
  // sen is used instead of sin in some languages, e.g. Portuguese.
62
40
  // To ensure that answers in various languages are graded correctly, we
@@ -65,7 +43,7 @@ const anglicizeOperators = (tex: string): string => {
65
43
  };
66
44
 
67
45
  const normalizeTex = (tex: string): string => {
68
- return anglicizeOperators(insertBraces(tex));
46
+ return anglicizeOperators(tex);
69
47
  };
70
48
 
71
49
  const deriveKeypadVersion = (apiOptions: APIOptions) => {
@@ -401,7 +401,6 @@ export class LabelImage extends React.Component<
401
401
 
402
402
  // Update Perseus widget state with user selected answers without
403
403
  // triggering interaction events for listeners.
404
- // @ts-expect-error - TS2345 - Argument of type 'null' is not assignable to parameter of type '(() => unknown) | undefined'.
405
404
  onChange({markers: updatedMarkers}, null, true);
406
405
  }
407
406
 
package/src/widgets.ts CHANGED
@@ -48,7 +48,7 @@ export const registerWidgets = (widgets: ReadonlyArray<WidgetExports>) => {
48
48
  * @param name - the widget that you are trying to replace
49
49
  * @param replacementName - the name of the widget that takes its place
50
50
  *
51
- * e.g. replaceWidget("transformer", "always-correct") will make it so the
51
+ * e.g. replaceWidget("transformer", "deprecated-standin") will make it so the
52
52
  * transformer widget is replaced by the always correct widget
53
53
  */
54
54
  export const replaceWidget = (name: string, replacementName: string) => {
@@ -61,10 +61,6 @@ export const replaceWidget = (name: string, replacementName: string) => {
61
61
  }
62
62
 
63
63
  registerWidget(name, substituteWidget);
64
-
65
- if (Log) {
66
- Log.log(`INFO: Replacing widget ${name} with ${replacementName}`);
67
- }
68
64
  };
69
65
 
70
66
  export const registerEditors = (editorsToRegister: ReadonlyArray<Editor>) => {
@@ -97,10 +93,6 @@ export const replaceEditor = (name: string, replacementName: string) => {
97
93
  }
98
94
 
99
95
  editors[name] = substituteEditor;
100
-
101
- if (Log) {
102
- Log.log(`INFO: Replacing editor ${name} with ${replacementName}`);
103
- }
104
96
  };
105
97
 
106
98
  export const getWidget = (