@khanacademy/perseus 17.8.0 → 18.0.1

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 (56) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/es/index.css +0 -4
  3. package/dist/es/index.js +1422 -1021
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +0 -4
  6. package/dist/index.d.ts +4 -2
  7. package/dist/index.js +1321 -954
  8. package/dist/index.js.map +1 -1
  9. package/dist/perseus-types.d.ts +4 -3
  10. package/dist/renderer.d.ts +6 -6
  11. package/dist/server-item-renderer.d.ts +2 -4
  12. package/dist/strings.js +1 -1
  13. package/dist/types.d.ts +19 -2
  14. package/dist/util/drawing-transform.d.ts +21 -0
  15. package/dist/util/extract-perseus-data.d.ts +26 -0
  16. package/dist/util/graph-bounds.d.ts +9 -0
  17. package/dist/util/graphie.d.ts +2 -0
  18. package/dist/util/interval.d.ts +1 -0
  19. package/dist/util/parse-perseus-json.d.ts +10 -0
  20. package/dist/util/tex.d.ts +1 -3
  21. package/dist/widgets/deprecated-standin/index.d.ts +4 -4
  22. package/dist/widgets/passage.d.ts +3 -3
  23. package/dist/widgets.d.ts +16 -6
  24. package/package.json +9 -9
  25. package/src/__tests__/extract-perseus-data.test.ts +1027 -0
  26. package/src/index.ts +6 -1
  27. package/src/init.ts +1 -1
  28. package/src/perseus-types.ts +4 -3
  29. package/src/renderer.tsx +12 -9
  30. package/src/server-item-renderer.tsx +2 -4
  31. package/src/styles/widgets/image.less +0 -3
  32. package/src/types.ts +19 -16
  33. package/src/util/drawing-transform.test.ts +67 -0
  34. package/src/util/drawing-transform.ts +102 -0
  35. package/src/util/extract-perseus-data.ts +539 -0
  36. package/src/util/graph-bounds.ts +23 -0
  37. package/src/util/graphie.ts +1167 -1258
  38. package/src/util/interactive.ts +2 -2
  39. package/src/util/interval.ts +4 -0
  40. package/src/util/parse-perseus-json.test.ts +10 -0
  41. package/src/util/parse-perseus-json.ts +13 -0
  42. package/src/util/tex.ts +11 -84
  43. package/src/widgets/__stories__/image.stories.tsx +12 -26
  44. package/src/widgets/__tests__/__snapshots__/grapher.test.ts.snap +72 -288
  45. package/src/widgets/__tests__/__snapshots__/interaction.test.ts.snap +5 -20
  46. package/src/widgets/__tests__/__snapshots__/interactive-graph.test.ts.snap +32 -128
  47. package/src/widgets/__tests__/__snapshots__/number-line.test.ts.snap +22 -88
  48. package/src/widgets/__tests__/__snapshots__/plotter.test.tsx.snap +12 -48
  49. package/src/widgets/deprecated-standin/__stories__/deprecated-standin.stories.tsx +8 -12
  50. package/src/widgets/deprecated-standin/__tests__/__snapshots__/deprecated-standin.test.ts.snap +6 -7
  51. package/src/widgets/deprecated-standin/index.tsx +8 -7
  52. package/src/widgets/interactive-graph.tsx +3 -2
  53. package/src/widgets/label-image/answer-pill.tsx +6 -1
  54. package/src/widgets/passage.tsx +5 -7
  55. package/src/widgets.ts +34 -10
  56. package/tsconfig-build.tsbuildinfo +1 -1
package/src/index.ts CHANGED
@@ -80,6 +80,11 @@ export {default as KhanColors} from "./util/colors";
80
80
  export {default as preprocessTex} from "./util/katex-preprocess";
81
81
  export {registerAllWidgetsForTesting} from "./util/register-all-widgets-for-testing";
82
82
  export * as SizingUtils from "./util/sizing-utils";
83
+ export {
84
+ getAnswersFromWidgets,
85
+ injectWidgets,
86
+ } from "./util/extract-perseus-data";
87
+ export {default as parsePerseusJSON} from "./util/parse-perseus-json";
83
88
 
84
89
  /**
85
90
  * Mixins
@@ -116,7 +121,6 @@ export type {
116
121
  VideoKind,
117
122
  WidgetDict,
118
123
  WidgetExports,
119
- WidgetInfo,
120
124
  } from "./types";
121
125
  export type {ParsedValue} from "./util";
122
126
  export type {
@@ -130,6 +134,7 @@ export type {
130
134
  PerseusPlotterWidgetOptions,
131
135
  PerseusRadioWidgetOptions,
132
136
  PerseusRenderer,
137
+ PerseusWidget,
133
138
  } from "./perseus-types";
134
139
  export type {Coord} from "./interactive2/types";
135
140
  export type {MarkerType} from "./widgets/label-image/types";
package/src/init.ts CHANGED
@@ -24,7 +24,7 @@ const init = function (options: PerseusOptions): Promise<undefined> {
24
24
  // This section replaces widgets
25
25
  // When it's time to replace the transformer widget
26
26
  // uncomment this line
27
- // Widgets.replaceWidget("transformer", "always-correct");
27
+ // Widgets.replaceWidget("transformer", "deprecated-standin");
28
28
 
29
29
  // Pass skipMathJax: true if MathJax is already loaded and configured.
30
30
  const skipMathJax = options.skipMathJax;
@@ -97,7 +97,7 @@ type Widget<Type extends string, Options> = {
97
97
  // NOTE: perseus_data.go says this is required even though it isn't necessary.
98
98
  alignment?: string;
99
99
  // Options specific to the type field of the widget. See Perseus*WidgetOptions for more details
100
- options: Options | null | undefined;
100
+ options: Options;
101
101
  // Only used by interactive child widgets (line, point, etc) to identify the components
102
102
  // NOTE: perseus_data.go says this is required even though it isn't necessary.
103
103
  key?: number;
@@ -704,13 +704,14 @@ export type PerseusMatcherWidgetOptions = {
704
704
  padding: boolean;
705
705
  };
706
706
 
707
+ export type PerseusMatrixWidgetAnswers = ReadonlyArray<ReadonlyArray<number>>;
707
708
  export type PerseusMatrixWidgetOptions = {
708
709
  // Translatable Text; Shown before the matrix
709
710
  prefix: string;
710
711
  // Translatable Text; Shown after the matrix
711
712
  suffix: string;
712
713
  // A data matrix representing the "correct" answers to be entered into the matrix
713
- answers: ReadonlyArray<ReadonlyArray<number>>;
714
+ answers: PerseusMatrixWidgetAnswers;
714
715
  // The coordinate location of the cursor position at start. default: [0, 0]
715
716
  cursorPosition: ReadonlyArray<number>;
716
717
  // The coordinate size of the matrix. Only supports 2-dimensional matrix. default: [3, 3]
@@ -821,7 +822,7 @@ export type PerseusNumberLineWidgetOptions = {
821
822
  // The correct relative value. default: "eq". options: "eq", "lt", "gt", "le", "ge"
822
823
  correctRel: string | null | undefined;
823
824
  // This is the correct answer. The answer is validated (as right or wrong) by using only the end position of the point and the relation (=, &lt;, &gt;, ≤, ≥).
824
- correctX: number | null | undefined;
825
+ correctX: number;
825
826
  // This controls the initial position of the point along the number line
826
827
  initialX: number | null | undefined;
827
828
  // Show tooltips
package/src/renderer.tsx CHANGED
@@ -30,14 +30,17 @@ import WidgetContainer from "./widget-container";
30
30
  import * as Widgets from "./widgets";
31
31
 
32
32
  import type {DependenciesContext} from "./dependencies";
33
- import type {PerseusRenderer, PerseusWidgetOptions} from "./perseus-types";
33
+ import type {
34
+ PerseusRenderer,
35
+ PerseusWidget,
36
+ PerseusWidgetOptions,
37
+ } from "./perseus-types";
34
38
  import type {
35
39
  APIOptions,
36
40
  APIOptionsWithDefaults,
37
41
  FilterCriterion,
38
42
  FocusPath,
39
43
  PerseusScore,
40
- WidgetInfo,
41
44
  WidgetProps,
42
45
  } from "./types";
43
46
  import type {LinterContextProps} from "@khanacademy/perseus-linter";
@@ -179,7 +182,7 @@ type Props = Partial<React.ContextType<typeof DependenciesContext>> & {
179
182
  type State = {
180
183
  translationLintErrors: ReadonlyArray<string>;
181
184
  widgetInfo: Readonly<{
182
- [id: string]: WidgetInfo | null | undefined;
185
+ [id: string]: PerseusWidget | null | undefined;
183
186
  }>;
184
187
  widgetProps: Readonly<{
185
188
  [id: string]: any | null | undefined;
@@ -461,7 +464,7 @@ class Renderer extends React.Component<Props, State> {
461
464
 
462
465
  // @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
463
466
  _getAllWidgetsInfo: (props: Props) => {
464
- [key: string]: WidgetInfo;
467
+ [key: string]: PerseusWidget;
465
468
  } = (props: Props) => {
466
469
  // @ts-expect-error - TS2345 - Argument of type '{ [key: string]: PerseusWidget; }' is not assignable to parameter of type 'Partial<Record<string, CategorizerWidget>>'.
467
470
  return mapObject(props.widgets, (widgetInfo, widgetId) => {
@@ -483,7 +486,7 @@ class Renderer extends React.Component<Props, State> {
483
486
 
484
487
  _getAllWidgetsStartProps: (
485
488
  allWidgetInfo: {
486
- [key: string]: WidgetInfo;
489
+ [key: string]: PerseusWidget;
487
490
  },
488
491
  props: Props,
489
492
  ) => any = (allWidgetInfo, props) => {
@@ -519,9 +522,9 @@ class Renderer extends React.Component<Props, State> {
519
522
  };
520
523
  };
521
524
 
522
- _getWidgetInfo: (widgetId: string) => WidgetInfo = (
525
+ _getWidgetInfo: (widgetId: string) => PerseusWidget = (
523
526
  widgetId: string,
524
- ): WidgetInfo => {
527
+ ): PerseusWidget => {
525
528
  return (
526
529
  this.state.widgetInfo[widgetId] ||
527
530
  this._getDefaultWidgetInfo(widgetId)
@@ -816,14 +819,14 @@ class Renderer extends React.Component<Props, State> {
816
819
  const widgetId = filterCriterion;
817
820
  filterFunc = (
818
821
  id: string,
819
- widgetInfo: WidgetInfo,
822
+ widgetInfo: PerseusWidget,
820
823
  widget?: Widget | null,
821
824
  ) => id === widgetId;
822
825
  } else {
823
826
  const widgetType = filterCriterion;
824
827
  filterFunc = (
825
828
  id: string,
826
- widgetInfo: WidgetInfo,
829
+ widgetInfo: PerseusWidget,
827
830
  widget?: Widget | null,
828
831
  ) => {
829
832
  return widgetInfo.type === widgetType;
@@ -21,6 +21,7 @@ import {ApiOptions} from "./perseus-api";
21
21
  import Renderer from "./renderer";
22
22
  import Util from "./util";
23
23
 
24
+ import type {PerseusItem} from "./perseus-types";
24
25
  import type {APIOptions, FocusPath, PerseusDependenciesV2} from "./types";
25
26
  import type {KeypadAPI} from "@khanacademy/math-input";
26
27
  import type {
@@ -36,10 +37,7 @@ const {mapObject} = Objective;
36
37
  type OwnProps = {
37
38
  apiOptions: APIOptions;
38
39
  hintsVisible?: number;
39
- item: {
40
- hints: ReadonlyArray<any>;
41
- question: any;
42
- };
40
+ item: PerseusItem;
43
41
  linterContext: LinterContextProps;
44
42
  problemNum?: number;
45
43
  reviewMode?: boolean;
@@ -1,7 +1,4 @@
1
1
  .perseus-image-widget {
2
- margin-left: auto;
3
- margin-right: auto;
4
-
5
2
  .perseus-image-title {
6
3
  text-align: center;
7
4
  }
package/src/types.ts CHANGED
@@ -415,12 +415,15 @@ export type WidgetExports<
415
415
  // the `validate` method.
416
416
  getWidget?: () => T;
417
417
  accessible?: boolean | ((props: any) => boolean);
418
+ /** Supresses widget from showing up in the dropdown in the content editor */
418
419
  hidden?: boolean;
419
- // The widget version. Any time the _major_ version changes, the widget
420
- // should provide a new entry in the propUpgrades map to migrate from the
421
- // older version to the current (new) version. Minor version changes must
422
- // be backwards compatible with previous minor versions widget options.
423
- // This key defaults to `{major: 0, minor: 0}` if not provided.
420
+ /**
421
+ The widget version. Any time the _major_ version changes, the widget
422
+ should provide a new entry in the propUpgrades map to migrate from the
423
+ older version to the current (new) version. Minor version changes must
424
+ be backwards compatible with previous minor versions widget options.
425
+ This key defaults to `{major: 0, minor: 0}` if not provided.
426
+ */
424
427
  version?: Version;
425
428
  supportedAlignments?: ReadonlyArray<Alignment>;
426
429
  defaultAlignment?: Alignment;
@@ -430,17 +433,19 @@ export type WidgetExports<
430
433
 
431
434
  traverseChildWidgets?: any; // (Props, traverseRenderer) => NewProps,
432
435
 
433
- // transforms the widget options to the props used to render the widget
436
+ /** transforms the widget options to the props used to render the widget */
434
437
  transform?: WidgetTransform;
435
- // transforms the widget options to the props used to render the widget for
436
- // static renders
438
+ /** transforms the widget options to the props used to render the widget for
439
+ static renders */
437
440
  staticTransform?: WidgetTransform; // this is a function of some sort,
438
441
 
439
- // A map of major version numbers (as a string, eg "1") to a function that
440
- // migrates from the _previous_ major version.
441
- // Example:
442
- // propUpgrades: {'1': (options) => ({...options})}
443
- // would migrate from major version 0 to 1.
442
+ /**
443
+ A map of major version numbers (as a string, eg "1") to a function that
444
+ migrates from the _previous_ major version.
445
+ Example:
446
+ propUpgrades: {'1': (options) => ({...options})}
447
+ would migrate from major version 0 to 1.
448
+ */
444
449
  propUpgrades?: {
445
450
  [key: string]: (arg1: any) => any;
446
451
  }; // OldProps => NewProps,
@@ -448,13 +453,11 @@ export type WidgetExports<
448
453
  widget: T;
449
454
  }>;
450
455
 
451
- export type WidgetInfo = PerseusWidget;
452
-
453
456
  export type FilterCriterion =
454
457
  | string
455
458
  | ((
456
459
  id: string,
457
- widgetInfo: WidgetInfo,
460
+ widgetInfo: PerseusWidget,
458
461
  widget?: Widget | null | undefined,
459
462
  ) => boolean);
460
463
 
@@ -0,0 +1,67 @@
1
+ import {DrawingTransform} from "./drawing-transform";
2
+ import {GraphBounds} from "./graph-bounds";
3
+
4
+ import type {Coord} from "../interactive2/types";
5
+
6
+ describe("DrawingTransform", () => {
7
+ describe("with bounds of -10 to 10, and 5px per unit", () => {
8
+ let transform: DrawingTransform;
9
+ beforeEach(() => {
10
+ const bounds = new GraphBounds([-10, 10], [-10, 10]);
11
+ // Each unit in both dimensions is 5 pixels, so the whole graph
12
+ // is 100px by 100px
13
+ const scale: [number, number] = [5, 5];
14
+ transform = new DrawingTransform({setSize() {}}, scale, bounds);
15
+ });
16
+
17
+ const testPoints: [string, Coord, Coord][] = [
18
+ ["lower left corner", [-10, -10], [0, 100]],
19
+ ["upper left corner", [-10, 10], [0, 0]],
20
+ ["lower right corner", [10, -10], [100, 100]],
21
+ ["upper right corner", [10, 10], [100, 0]],
22
+ ["origin", [0, 0], [50, 50]],
23
+ ];
24
+
25
+ it.each(testPoints)(
26
+ "transforms the point at the %s from math coords to canvas coords",
27
+ (_, cartesian, canvas) => {
28
+ expect(transform.scalePoint(cartesian)).toEqual(canvas);
29
+ },
30
+ );
31
+
32
+ it.each(testPoints)(
33
+ "transforms the point at the %s from canvas coords to math coords",
34
+ (_, cartesian, canvas) => {
35
+ expect(transform.unscalePoint(canvas)).toEqual(cartesian);
36
+ },
37
+ );
38
+ });
39
+
40
+ it("updates the size of the Raphael canvas when setScale is called with a number", () => {
41
+ const raphael = {setSize: jest.fn()};
42
+ const scale: Coord = [40, 40];
43
+ const bounds = new GraphBounds([-10, 10], [-10, 10]);
44
+ const transform = new DrawingTransform(raphael, scale, bounds);
45
+ expect(raphael.setSize).toHaveBeenCalledWith(800, 800);
46
+
47
+ // Act
48
+ transform.setScale(5);
49
+
50
+ // Assert
51
+ expect(raphael.setSize).toHaveBeenCalledWith(100, 100);
52
+ });
53
+
54
+ it("updates the size of the Raphael canvas when setScale is called with dimensions", () => {
55
+ const raphael = {setSize: jest.fn()};
56
+ const scale: Coord = [40, 40];
57
+ const bounds = new GraphBounds([-5, 5], [-5, 5]);
58
+ const transform = new DrawingTransform(raphael, scale, bounds);
59
+ expect(raphael.setSize).toHaveBeenCalledWith(400, 400);
60
+
61
+ // Act
62
+ transform.setScale([3, 7]);
63
+
64
+ // Assert
65
+ expect(raphael.setSize).toHaveBeenCalledWith(30, 70);
66
+ });
67
+ });
@@ -0,0 +1,102 @@
1
+ import type {GraphBounds} from "./graph-bounds";
2
+ import type {Coord} from "../interactive2/types";
3
+
4
+ interface Raphael {
5
+ setSize(width: number, height: number);
6
+ }
7
+
8
+ // DrawingTransform is responsible for transforming from "math coordinates"
9
+ // (on the Cartesian plane) to "canvas coordinates" in pixels.
10
+ export class DrawingTransform {
11
+ raphael: Raphael;
12
+
13
+ // Scale is measured in pixels per unit on the Cartesian plane.
14
+ xScale: number;
15
+ yScale: number;
16
+ bounds: GraphBounds;
17
+
18
+ constructor(
19
+ raphael: Raphael,
20
+ initialScale: [number, number],
21
+ bounds: GraphBounds,
22
+ ) {
23
+ this.raphael = raphael;
24
+ this.bounds = bounds;
25
+ this.xScale = initialScale[0];
26
+ this.yScale = initialScale[1];
27
+ this.setScale(initialScale);
28
+ }
29
+
30
+ // TODO(benchristel): rename these methods to transform* instead of scale*.
31
+ // They're doing more than scaling! Some of them also translate.
32
+ scaleVector = (point: number | Coord) => {
33
+ if (typeof point === "number") {
34
+ return this.scaleVector([point, point]);
35
+ }
36
+
37
+ const x = point[0];
38
+ const y = point[1];
39
+ return [x * this.xScale, y * this.yScale];
40
+ };
41
+
42
+ scalePoint = (point: number | Coord): Coord => {
43
+ if (typeof point === "number") {
44
+ return this.scalePoint([point, point]);
45
+ }
46
+
47
+ const x = point[0];
48
+ const y = point[1];
49
+ return [
50
+ (x - this.bounds.xMin) * this.xScale,
51
+ (this.bounds.yMax - y) * this.yScale,
52
+ ];
53
+ };
54
+
55
+ unscalePoint = (point: number | Coord) => {
56
+ if (typeof point === "number") {
57
+ return this.unscalePoint([point, point]);
58
+ }
59
+
60
+ const x = point[0];
61
+ const y = point[1];
62
+ return [
63
+ x / this.xScale + this.bounds.xMin,
64
+ this.bounds.yMax - y / this.yScale,
65
+ ];
66
+ };
67
+
68
+ unscaleVector = (point: number | Coord) => {
69
+ if (typeof point === "number") {
70
+ return this.unscaleVector([point, point]);
71
+ }
72
+
73
+ return [point[0] / this.xScale, point[1] / this.yScale];
74
+ };
75
+
76
+ setScale = (scale: number | Coord) => {
77
+ if (typeof scale === "number") {
78
+ scale = [scale, scale];
79
+ }
80
+
81
+ this.xScale = scale[0];
82
+ this.yScale = scale[1];
83
+
84
+ // Update the canvas size
85
+ this.raphael.setSize(...this.canvasDimensions());
86
+ };
87
+
88
+ canvasDimensions = (): Coord => {
89
+ return [
90
+ this.bounds.width() * this.xScale,
91
+ this.bounds.height() * this.yScale,
92
+ ];
93
+ };
94
+
95
+ pixelsPerUnitX = (): number => {
96
+ return this.xScale;
97
+ };
98
+
99
+ pixelsPerUnitY = (): number => {
100
+ return this.yScale;
101
+ };
102
+ }