@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
@@ -4,6 +4,7 @@ import type {
4
4
  InputNumberWidget,
5
5
  PerseusRenderer,
6
6
  } from "../perseus-types";
7
+ import type {RenderProps} from "../widgets/radio/widget";
7
8
 
8
9
  export const dropdownWidget: DropdownWidget = {
9
10
  type: "dropdown",
@@ -120,3 +121,154 @@ export const mockedItem: PerseusRenderer = {
120
121
  },
121
122
  },
122
123
  } as PerseusRenderer;
124
+
125
+ // Note that if this item is used, you _must_ first register the MockWidget
126
+ export const mockedRandomItem: PerseusRenderer = {
127
+ content: "Mock widgets ==> [[\u2603 radio 1]] [[\u2603 radio 2]]",
128
+ images: {},
129
+ widgets: {
130
+ "radio 1": {
131
+ graded: true,
132
+ version: {major: 0, minor: 0},
133
+ static: false,
134
+ numCorrect: 1,
135
+ hasNoneOfTheAbove: false,
136
+ multipleSelect: false,
137
+ countChoices: false,
138
+ deselectEnabled: false,
139
+ type: "radio",
140
+ options: {
141
+ static: false,
142
+ countChoices: false,
143
+ deselectEnabled: false,
144
+ displayCount: null,
145
+ hasNoneOfTheAbove: false,
146
+ multipleSelect: false,
147
+ randomize: true,
148
+ choices: [
149
+ {
150
+ content: "Content 1",
151
+ correct: true,
152
+ },
153
+ {
154
+ content: "Content 2",
155
+ correct: false,
156
+ },
157
+ {
158
+ content: "Content 3",
159
+ correct: false,
160
+ },
161
+ {
162
+ content: "Content 4",
163
+ correct: false,
164
+ },
165
+ ],
166
+ },
167
+ alignment: "default",
168
+ },
169
+ "radio 2": {
170
+ graded: true,
171
+ version: {major: 0, minor: 0},
172
+ static: false,
173
+ numCorrect: 1,
174
+ hasNoneOfTheAbove: false,
175
+ multipleSelect: false,
176
+ countChoices: false,
177
+ deselectEnabled: false,
178
+ type: "radio",
179
+ options: {
180
+ static: false,
181
+ countChoices: false,
182
+ deselectEnabled: false,
183
+ displayCount: null,
184
+ hasNoneOfTheAbove: false,
185
+ multipleSelect: false,
186
+ randomize: true,
187
+ choices: [
188
+ {
189
+ content: "Content 1",
190
+ correct: true,
191
+ },
192
+ {
193
+ content: "Content 2",
194
+ correct: false,
195
+ },
196
+ {
197
+ content: "Content 3",
198
+ correct: false,
199
+ },
200
+ {
201
+ content: "Content 4",
202
+ correct: false,
203
+ },
204
+ ],
205
+ },
206
+ alignment: "default",
207
+ },
208
+ },
209
+ } as PerseusRenderer;
210
+
211
+ export const mockedShuffledRadioProps: {[key in string]: RenderProps} = {
212
+ "radio 1": {
213
+ numCorrect: 1,
214
+ countChoices: false,
215
+ deselectEnabled: false,
216
+ hasNoneOfTheAbove: false,
217
+ multipleSelect: false,
218
+ choices: [
219
+ {
220
+ content: "Content 4",
221
+ correct: false,
222
+ originalIndex: 3,
223
+ },
224
+ {
225
+ content: "Content 2",
226
+ correct: false,
227
+ originalIndex: 1,
228
+ },
229
+ {
230
+ content: "Content 1",
231
+ correct: true,
232
+ originalIndex: 0,
233
+ },
234
+
235
+ {
236
+ content: "Content 3",
237
+ correct: false,
238
+ originalIndex: 2,
239
+ },
240
+ ],
241
+ selectedChoices: [false, false, true, false],
242
+ },
243
+ "radio 2": {
244
+ numCorrect: 1,
245
+ countChoices: false,
246
+ deselectEnabled: false,
247
+ hasNoneOfTheAbove: false,
248
+ multipleSelect: false,
249
+ choices: [
250
+ {
251
+ content: "Content 4",
252
+ correct: false,
253
+ originalIndex: 3,
254
+ },
255
+ {
256
+ content: "Content 2",
257
+ correct: false,
258
+ originalIndex: 1,
259
+ },
260
+ {
261
+ content: "Content 1",
262
+ correct: true,
263
+ originalIndex: 0,
264
+ },
265
+
266
+ {
267
+ content: "Content 3",
268
+ correct: false,
269
+ originalIndex: 2,
270
+ },
271
+ ],
272
+ selectedChoices: [false, false, true, false],
273
+ },
274
+ };
@@ -14,6 +14,8 @@ import {
14
14
  question1,
15
15
  question2,
16
16
  mockedItem,
17
+ mockedRandomItem,
18
+ mockedShuffledRadioProps,
17
19
  } from "../__testdata__/renderer.testdata";
18
20
  import * as Dependencies from "../dependencies";
19
21
  import {Errors} from "../logging/log";
@@ -29,6 +31,7 @@ import type {
29
31
  PerseusImageWidgetOptions,
30
32
  PerseusInputNumberWidgetOptions,
31
33
  } from "../perseus-types";
34
+ import type {APIOptions} from "../types";
32
35
 
33
36
  // NOTE(jeremy): We can't use an automatic mock for the translation linter,
34
37
  // because one of it's "instance" methods is created using `debounce` and Jest
@@ -287,6 +290,20 @@ describe("renderer", () => {
287
290
  /^less than or equal to$/,
288
291
  );
289
292
  });
293
+
294
+ it("should call the onWidgetStartProps callback if provided in apiOptions", () => {
295
+ // Arrange
296
+ const onWidgetStartProps = jest.fn();
297
+ const apiOptions: APIOptions = {onWidgetStartProps};
298
+
299
+ // Act
300
+ renderQuestion(mockedRandomItem, apiOptions);
301
+
302
+ // Assert
303
+ expect(onWidgetStartProps).toHaveBeenCalledWith(
304
+ mockedShuffledRadioProps,
305
+ );
306
+ });
290
307
  });
291
308
 
292
309
  describe("rendering", () => {
@@ -40,7 +40,6 @@ describe("Widget API support", () => {
40
40
  "simulator",
41
41
  "sorter",
42
42
  "table",
43
- "transformer",
44
43
  "unit-input",
45
44
  "video",
46
45
  ])("%s widget should provide static validate function", (widgetType) => {
@@ -157,7 +157,7 @@ class MathInput extends React.Component<Props, State> {
157
157
  (baseConfig) => ({
158
158
  ...baseConfig,
159
159
  handlers: {
160
- edit: (mathField) => {
160
+ edit: (mathField: MathFieldInterface) => {
161
161
  // This handler is guaranteed to be called on change, but
162
162
  // unlike React it sometimes generates false positives.
163
163
  // One of these is on initialization (with an empty string
@@ -184,11 +184,9 @@ class MathInput extends React.Component<Props, State> {
184
184
  // directly, instead of waiting for `.latex()` to be
185
185
  // called in `componentDidUpdate()`.
186
186
  const left =
187
- mathField.__controller.cursor[
188
- mathQuillInstance.L
189
- ];
187
+ mathField.cursor()[mathQuillInstance.L];
190
188
  if (left && left.ctrlSeq === "\\cdot ") {
191
- mathField.__controller.backspace();
189
+ mathField.controller().backspace();
192
190
  mathField.cmd("\\times");
193
191
  }
194
192
  } else {
@@ -214,7 +212,7 @@ class MathInput extends React.Component<Props, State> {
214
212
  $(this.__mathFieldWrapperRef).submit();
215
213
  }
216
214
  },
217
- upOutOf: (mathField) => {
215
+ upOutOf: (mathField: MathFieldInterface) => {
218
216
  // This handler is called when the user presses the up
219
217
  // arrow key, but there is nowhere in the expression to go
220
218
  // up to (no numerator or exponent). For ease of use,
@@ -33,7 +33,6 @@ import Sequence from "./widgets/sequence";
33
33
  import Simulator from "./widgets/simulator";
34
34
  import Sorter from "./widgets/sorter";
35
35
  import Table from "./widgets/table";
36
- import Transformer from "./widgets/transformer";
37
36
  import Unit from "./widgets/unit";
38
37
  import Video from "./widgets/video";
39
38
 
@@ -70,7 +69,6 @@ export default [
70
69
  Simulator,
71
70
  Sorter,
72
71
  Table,
73
- Transformer,
74
72
  Unit,
75
73
  Video,
76
74
  DeprecatedStandin,
package/src/index.ts CHANGED
@@ -31,7 +31,6 @@ export {default as Radio} from "./widgets/radio";
31
31
  export {default as BaseRadio} from "./widgets/radio/base-radio";
32
32
  export {default as Categorizer} from "./widgets/categorizer";
33
33
  export {default as InteractiveGraphWidget} from "./widgets/interactive-graph";
34
- export {default as TransformerWidget} from "./widgets/transformer";
35
34
  export {default as MatrixWidget} from "./widgets/matrix";
36
35
  export {default as TableWidget} from "./widgets/table";
37
36
  export {default as PlotterWidget} from "./widgets/plotter";
@@ -84,7 +83,7 @@ export {
84
83
  getAnswersFromWidgets,
85
84
  injectWidgets,
86
85
  } from "./util/extract-perseus-data";
87
- export {default as parsePerseusJSON} from "./util/parse-perseus-json";
86
+ export {parsePerseusItem} from "./util/parse-perseus-json";
88
87
 
89
88
  /**
90
89
  * Mixins
@@ -124,11 +123,16 @@ export type {
124
123
  } from "./types";
125
124
  export type {ParsedValue} from "./util";
126
125
  export type {
127
- InputNumberWidget,
128
- MathFormat,
129
126
  PerseusAnswerArea,
130
127
  PerseusExpressionWidgetOptions,
128
+ // Utility types
129
+ Range,
130
+ Size,
131
+ MathFormat,
132
+ InputNumberWidget, // TODO(jeremy): remove?
133
+ // Widget configuration types
131
134
  PerseusImageBackground,
135
+ PerseusInputNumberWidgetOptions,
132
136
  PerseusInteractiveGraphWidgetOptions,
133
137
  PerseusItem,
134
138
  PerseusPlotterWidgetOptions,
@@ -127,7 +127,7 @@ export type ChangeableProps = {
127
127
  values: {
128
128
  [key: string]: any;
129
129
  },
130
- callback?: () => unknown | null | undefined,
130
+ callback?: (() => unknown) | null | undefined,
131
131
  silent?: boolean,
132
132
  ) => unknown;
133
133
  };
@@ -90,7 +90,7 @@ type Widget<Type extends string, Options> = {
90
90
  // Whether this widget is displayed with the values and is immutable. For display only
91
91
  // NOTE: perseus_data.go says this is required even though it isn't necessary.
92
92
  static?: boolean;
93
- // Whether a widget is scored. Usually true except for IFrame widgets (deprecated) and Transformer widgets
93
+ // Whether a widget is scored. Usually true except for IFrame widgets (deprecated)
94
94
  // Default: true
95
95
  graded?: boolean;
96
96
  // The HTML alignment of the widget. "default" or "block"
@@ -164,9 +164,6 @@ export type SimulatorWidget = Widget<'simulator', PerseusSimulatorWidgetOptions>
164
164
  export type SorterWidget = Widget<'sorter', PerseusSorterWidgetOptions>;
165
165
  // prettier-ignore
166
166
  export type TableWidget = Widget<'table', PerseusTableWidgetOptions>;
167
- // prettier-ignore
168
- export type TransformerWidget = Widget<'transformer', PerseusTransformerWidgetOptions>;
169
-
170
167
  // prettier-ignore
171
168
  export type ExampleGraphieWidget = Widget<'example-graphie-widget', PerseusExampleGraphieWidgetOptions>;
172
169
  // prettier-ignore
@@ -228,7 +225,6 @@ export type PerseusWidget =
228
225
  | SimulatorWidget
229
226
  | SorterWidget
230
227
  | TableWidget
231
- | TransformerWidget
232
228
  | UnitInputWidget
233
229
  | VideoWidget
234
230
  | AutoCorrectWidget;
@@ -456,7 +452,6 @@ export type PerseusGrapherWidgetOptions = {
456
452
  // Two points on the same slope in the tangent wave line.
457
453
  coords: [Coord, Coord];
458
454
  };
459
- // TODO(jeremy): Unify with PerseusTransformerWidgetOptions's .graph
460
455
  graph: {
461
456
  backgroundImage: {
462
457
  bottom?: number;
@@ -990,115 +985,6 @@ export type PerseusTableWidgetOptions = {
990
985
  answers: ReadonlyArray<ReadonlyArray<string>>;
991
986
  };
992
987
 
993
- export type PerseusTransformerWidgetOptions = {
994
- correct: {
995
- shape: {
996
- type: ReadonlyArray<
997
- | "polygon-5"
998
- | "polygon-3"
999
- | "polygon-4"
1000
- | "lineSegment"
1001
- | "polygon-6"
1002
- | "angle"
1003
- | "line"
1004
- | "circle"
1005
- >;
1006
- coords: ReadonlyArray<Coord>;
1007
- options?: ReadonlyArray<{
1008
- reflex?: boolean;
1009
- }>;
1010
- };
1011
- transformations: ReadonlyArray<PerseusTransformerTransformation>;
1012
- };
1013
- drawSolutionShape: boolean;
1014
- gradeEmpty: boolean;
1015
- // TODO(jeremy): Unify with PerseusGrapherWidgetOptions's .graph
1016
- graph: {
1017
- backgroundImage: {
1018
- bottom?: number;
1019
- height?: number;
1020
- left?: number;
1021
- scale?: number;
1022
- url?: string;
1023
- width?: number;
1024
- };
1025
- gridStep?: [number, number];
1026
- labels?: ["x", "y"];
1027
- markings: "none" | "graph" | "grid";
1028
- range: [[number, number], [number, number]];
1029
- rulerLabel?: string;
1030
- rulerTicks?: number;
1031
- showProtractor: boolean;
1032
- showRuler?: boolean;
1033
- snapStep?: [number, number];
1034
- step: [number, number];
1035
- };
1036
- graphMode: "dynamic" | "interactive" | "static";
1037
- listMode: "interactive" | "dynamic" | "static";
1038
- starting: {
1039
- shape:
1040
- | {
1041
- type: ReadonlyArray<
1042
- | "polygon-5"
1043
- | "polygon-3"
1044
- | "polygon-4"
1045
- | "lineSegment"
1046
- | "polygon-6"
1047
- >;
1048
- coords: ReadonlyArray<Coord>;
1049
- options?: ReadonlyArray<Empty>;
1050
- }
1051
- | {
1052
- type: "polygon-3";
1053
- coords: [Coord, Coord, Coord];
1054
- }
1055
- | {
1056
- type: [
1057
- | "polygon-3"
1058
- | "lineSegment"
1059
- | "polygon-4"
1060
- | "polygon-5"
1061
- | "polygon-6"
1062
- | "line"
1063
- | "circle",
1064
- ];
1065
- coords: ReadonlyArray<Coord>;
1066
- options: [Empty];
1067
- }
1068
- | {
1069
- type: ["polygon-3"];
1070
- coords: [Coord, Coord, Coord];
1071
- options?: [Empty];
1072
- }
1073
- | {
1074
- type: ["angle"];
1075
- coords: [Coord, Coord, Coord];
1076
- options: [
1077
- {
1078
- reflex: boolean;
1079
- },
1080
- ];
1081
- }
1082
- | {
1083
- type: ["line" | "lineSegment"];
1084
- coords: [Coord, Coord];
1085
- options?: [Empty];
1086
- }
1087
- | {
1088
- type: "polygon";
1089
- coords: ReadonlyArray<Coord>;
1090
- };
1091
- transformations: ReadonlyArray<PerseusTransformerTransformation>;
1092
- };
1093
- tools: {
1094
- dilation: DilationTool;
1095
- reflection: ReflectionTool;
1096
- rotation: RotationTool;
1097
- translation: TranslationTool;
1098
- };
1099
- version: number;
1100
- };
1101
-
1102
988
  export type DilationTransformation = {
1103
989
  type: "dilation";
1104
990
  center: Coord;
@@ -1131,44 +1017,6 @@ export type TranslationTransformation = {
1131
1017
  contraints: Empty;
1132
1018
  };
1133
1019
 
1134
- export type PerseusTransformerTransformation =
1135
- | DilationTransformation
1136
- | ReflectionTransformation
1137
- | RotationTransformation
1138
- | TranslationTransformation;
1139
-
1140
- type DilationTool = {
1141
- constraints: PerseusTransformerToolConstraints;
1142
- coord: Coord;
1143
- enabled: boolean;
1144
- required?: boolean;
1145
- };
1146
-
1147
- type ReflectionTool = {
1148
- constraints: PerseusTransformerToolConstraints;
1149
- coords: [Coord, Coord];
1150
- enabled: boolean;
1151
- required?: boolean;
1152
- };
1153
-
1154
- type RotationTool = {
1155
- constraints: PerseusTransformerToolConstraints;
1156
- coord: Coord;
1157
- enabled: boolean;
1158
- required?: boolean;
1159
- };
1160
-
1161
- type TranslationTool = {
1162
- constraints: Empty;
1163
- enabled: boolean;
1164
- required?: boolean;
1165
- };
1166
-
1167
- export type PerseusTransformerToolConstraints = {
1168
- // Whether we should prevent the student from repositioning the tool
1169
- fixed: boolean;
1170
- };
1171
-
1172
1020
  export type PerseusInteractionWidgetOptions = {
1173
1021
  // The definition of the graph
1174
1022
  graph: PerseusInteractionGraph;
@@ -1514,6 +1362,5 @@ export type PerseusWidgetOptions =
1514
1362
  | PerseusSimulatorWidgetOptions
1515
1363
  | PerseusSorterWidgetOptions
1516
1364
  | PerseusTableWidgetOptions
1517
- | PerseusTransformerWidgetOptions
1518
1365
  | PerseusUnitInputWidgetOptions
1519
1366
  | PerseusVideoWidgetOptions;
package/src/renderer.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
2
2
  /* eslint-disable react/no-unsafe */
3
3
  import * as PerseusLinter from "@khanacademy/perseus-linter";
4
+ import {entries} from "@khanacademy/wonder-stuff-core";
4
5
  import classNames from "classnames";
5
6
  import $ from "jquery";
6
7
  import * as React from "react";
@@ -41,6 +42,7 @@ import type {
41
42
  FilterCriterion,
42
43
  FocusPath,
43
44
  PerseusScore,
45
+ PerseusWidgetMap,
44
46
  WidgetProps,
45
47
  } from "./types";
46
48
  import type {LinterContextProps} from "@khanacademy/perseus-linter";
@@ -485,18 +487,22 @@ class Renderer extends React.Component<Props, State> {
485
487
  };
486
488
 
487
489
  _getAllWidgetsStartProps: (
488
- allWidgetInfo: {
489
- [key: string]: PerseusWidget;
490
- },
490
+ allWidgetInfo: PerseusWidgetMap,
491
491
  props: Props,
492
- ) => any = (allWidgetInfo, props) => {
493
- // @ts-expect-error - TS2345 - Argument of type '{ [key: string]: PerseusWidget; }' is not assignable to parameter of type 'Partial<Record<string, CategorizerWidget>>'.
494
- return mapObject(allWidgetInfo, (widgetInfo) => {
495
- return Widgets.getRendererPropsForWidgetInfo(
492
+ ) => PerseusWidgetMap = (allWidgetInfo, props) => {
493
+ const {apiOptions, problemNum} = props;
494
+ const widgetsStartProps: PerseusWidgetMap = {};
495
+ entries(allWidgetInfo).forEach(([key, widgetInfo]) => {
496
+ widgetsStartProps[key] = Widgets.getRendererPropsForWidgetInfo(
496
497
  widgetInfo,
497
- props.problemNum,
498
+ problemNum,
498
499
  );
499
500
  });
501
+
502
+ // Call the onWidgetStartProps callback if it exists
503
+ apiOptions?.onWidgetStartProps?.(widgetsStartProps);
504
+
505
+ return widgetsStartProps;
500
506
  };
501
507
 
502
508
  // This is only used in _getWidgetInfo as a fallback if widgetId
@@ -448,7 +448,6 @@
448
448
  @import "./widgets/sequence.less";
449
449
  @import "./widgets/sortable.less";
450
450
  @import "./widgets/table.less";
451
- @import "./widgets/transformer.less";
452
451
  @import "./widgets/unit.less";
453
452
 
454
453
  .perseus-widget-container {
package/src/types.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type {SerializedHighlightSet} from "./components/highlighting/types";
2
1
  import type {ILogger} from "./logging/log";
3
2
  import type {Item} from "./multi-items/item-types";
4
3
  import type {
@@ -92,10 +91,6 @@ export type ChangeHandler = (
92
91
  currentValue?: string;
93
92
  // perseus-all-package/widgets/dropdown.jsx
94
93
  selected?: number;
95
- // perseus-all-package/widgets/transformer.jsx
96
- tools?: ReadonlyArray<any>;
97
- transformations?: ReadonlyArray<any>;
98
- highlights?: SerializedHighlightSet;
99
94
  // perseus-all-package/widgets/grapher.jsx
100
95
  plot?: any;
101
96
  },
@@ -228,6 +223,11 @@ export type APIOptions = Readonly<{
228
223
  // keystroke caused text typed in the text area to appear in it
229
224
  // only after a good few seconds.
230
225
  editorChangeDelay?: number;
226
+ // This is a callback function that returns all of the Widget props
227
+ // after they have been transformed by the widget's transform function.
228
+ // This is useful for when we need to know how a widget has shuffled its
229
+ // the available choices.
230
+ onWidgetStartProps?: (widgets: PerseusWidgetMap) => PerseusWidgetMap;
231
231
  }>;
232
232
 
233
233
  type TeXProps = {
@@ -453,6 +453,10 @@ export type WidgetExports<
453
453
  widget: T;
454
454
  }>;
455
455
 
456
+ export type PerseusWidgetMap = {
457
+ [key: string]: PerseusWidget;
458
+ };
459
+
456
460
  export type FilterCriterion =
457
461
  | string
458
462
  | ((
@@ -0,0 +1,75 @@
1
+ import {clockwise, reverseVector} from "./geometry";
2
+
3
+ describe("reverseVector", () => {
4
+ it("flips the sign of zero", () => {
5
+ // This is a characterization test for legacy behavior. It might not
6
+ // actually be desired behavior.
7
+ expect(reverseVector([0, 0])).toEqual([-0, -0]);
8
+ });
9
+
10
+ it("flips the sign of each component", () => {
11
+ expect(reverseVector([1, -5])).toEqual([-1, 5]);
12
+ });
13
+ });
14
+
15
+ describe("clockwise", () => {
16
+ it("returns false given no points", () => {
17
+ expect(clockwise([])).toBe(false);
18
+ });
19
+
20
+ it("returns false given a single point at the origin", () => {
21
+ expect(clockwise([[0, 0]])).toBe(false);
22
+ });
23
+
24
+ it("returns false given a single point not at the origin", () => {
25
+ expect(clockwise([[7, 7]])).toBe(false);
26
+ });
27
+
28
+ it("returns false given two identical points", () => {
29
+ expect(
30
+ clockwise([
31
+ [7, 7],
32
+ [7, 7],
33
+ ]),
34
+ ).toBe(false);
35
+ });
36
+
37
+ it("returns false given two different points", () => {
38
+ expect(
39
+ clockwise([
40
+ [8, 8],
41
+ [7, 7],
42
+ ]),
43
+ ).toBe(false);
44
+ });
45
+
46
+ it("returns true given a clockwise triangle in the first quadrant", () => {
47
+ expect(
48
+ clockwise([
49
+ [0, 1],
50
+ [1, 0],
51
+ [0, 0],
52
+ ]),
53
+ ).toBe(true);
54
+ });
55
+
56
+ it("returns false given a counter-clockwise triangle in the first quadrant", () => {
57
+ expect(
58
+ clockwise([
59
+ [1, 0],
60
+ [0, 1],
61
+ [0, 0],
62
+ ]),
63
+ ).toBe(false);
64
+ });
65
+
66
+ it("returns true given a clockwise triangle in the second quadrant", () => {
67
+ expect(
68
+ clockwise([
69
+ [0, 1],
70
+ [0, 0],
71
+ [-1, 0],
72
+ ]),
73
+ ).toBe(true);
74
+ });
75
+ });