@khanacademy/perseus 49.1.7 → 49.2.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 (30) hide show
  1. package/dist/components/math-input.d.ts +2 -1
  2. package/dist/es/index.css +6 -0
  3. package/dist/es/index.js +277 -147
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/es/strings.js +16 -12
  6. package/dist/es/strings.js.map +1 -1
  7. package/dist/index.css +6 -0
  8. package/dist/index.js +277 -138
  9. package/dist/index.js.map +1 -1
  10. package/dist/perseus-types.d.ts +12 -9
  11. package/dist/server-item-renderer.d.ts +1 -1
  12. package/dist/strings.d.ts +6 -5
  13. package/dist/strings.js +16 -12
  14. package/dist/strings.js.map +1 -1
  15. package/dist/types.d.ts +2 -5
  16. package/dist/util/parse-perseus-json/general-purpose-parsers/convert.d.ts +2 -0
  17. package/dist/util/parse-perseus-json/general-purpose-parsers/discriminated-union.d.ts +21 -0
  18. package/dist/util/parse-perseus-json/general-purpose-parsers/discriminated-union.typetest.d.ts +1 -0
  19. package/dist/util/parse-perseus-json/perseus-parsers/versioned-widget-options.d.ts +36 -0
  20. package/dist/widgets/expression/expression.d.ts +3 -3
  21. package/dist/widgets/interactive-graphs/graph-locked-labels-layer.d.ts +1 -3
  22. package/dist/widgets/interactive-graphs/graph-locked-layer.d.ts +0 -2
  23. package/dist/widgets/interactive-graphs/locked-figures/locked-ellipse.d.ts +1 -5
  24. package/dist/widgets/interactive-graphs/locked-figures/locked-function.d.ts +1 -5
  25. package/dist/widgets/interactive-graphs/locked-figures/locked-line.d.ts +0 -2
  26. package/dist/widgets/interactive-graphs/locked-figures/locked-point.d.ts +1 -5
  27. package/dist/widgets/interactive-graphs/locked-figures/locked-polygon.d.ts +1 -5
  28. package/dist/widgets/interactive-graphs/locked-figures/locked-vector.d.ts +1 -5
  29. package/dist/widgets/passage-ref/passage-ref.d.ts +7 -3
  30. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -487,12 +487,12 @@ const mockStrings = {
487
487
  angleMeasure,
488
488
  vertexX,
489
489
  vertexY,
490
- isX,
491
- isY,
492
- tsX,
493
- tsY
490
+ startingSideX,
491
+ startingSideY,
492
+ endingSideX,
493
+ endingSideY
494
494
  } = _ref40;
495
- return `The angle measure is ${angleMeasure} degrees with a vertex at ${vertexX} comma ${vertexY}, a point on the initial side at ${isX} comma ${isY} and a point on the terminal side at ${tsX} comma ${tsY}.`;
495
+ return `The angle measure is ${angleMeasure} degrees with a vertex at ${vertexX} comma ${vertexY}, a point on the starting side at ${startingSideX} comma ${startingSideY} and a point on the ending side at ${endingSideX} comma ${endingSideY}.`;
496
496
  }
497
497
  // The above strings are used for interactive graph SR descriptions.
498
498
  };
@@ -897,6 +897,9 @@ const mapButtonSets = buttonSets => {
897
897
  case "trig":
898
898
  keypadButtonSets.trigonometry = true;
899
899
  break;
900
+ case "scientific":
901
+ keypadButtonSets.scientific = true;
902
+ break;
900
903
  }
901
904
  });
902
905
  return keypadButtonSets;
@@ -3971,7 +3974,9 @@ ZoomServiceClass.prototype._scrollHandler = function (e) {
3971
3974
  }
3972
3975
  };
3973
3976
  ZoomServiceClass.prototype._keyHandler = function (e) {
3974
- if (e.keyCode === 27) {
3977
+ // 27: Esc, 13: Enter, 32: Space
3978
+ const keyCodes = [27, 13, 32];
3979
+ if (keyCodes.includes(e.keyCode)) {
3975
3980
  this._activeZoomClose();
3976
3981
  }
3977
3982
  };
@@ -4060,6 +4065,7 @@ Zoom.prototype.zoomImage = function () {
4060
4065
  }.bind(this);
4061
4066
  img.src = this._targetImage.src;
4062
4067
  img.alt = this._targetImage.alt;
4068
+ img.tabIndex = 0;
4063
4069
  this.$zoomedImage = $zoomedImage;
4064
4070
  };
4065
4071
  Zoom.prototype._zoomOriginal = function () {
@@ -17242,6 +17248,7 @@ class ImageLoader extends React__namespace.Component {
17242
17248
  }
17243
17249
  const staticUrl = getDependencies().staticUrl;
17244
17250
  return /*#__PURE__*/React__namespace.createElement("img", _extends({
17251
+ tabIndex: 0,
17245
17252
  src: staticUrl(src),
17246
17253
  onKeyUp: onKeyUp,
17247
17254
  onKeyDown: onKeyDown
@@ -22456,7 +22463,7 @@ function scoreNumericInput(userInput, rubric, strings) {
22456
22463
  // We may have received TeX; try to parse it before grading.
22457
22464
  // If `currentValue` is not TeX, this should be a no-op.
22458
22465
  const currentValue = ParseTex(userInput.currentValue);
22459
- const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value !== undefined && Math.abs(answer.value) <= 1);
22466
+ const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value != null && Math.abs(answer.value) <= 1);
22460
22467
 
22461
22468
  // The coefficient is an attribute of the widget
22462
22469
  let localValue = currentValue;
@@ -27732,6 +27739,7 @@ class Dropdown extends React__namespace.Component {
27732
27739
  }, this.props.visibleLabel), /*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.SingleSelect, {
27733
27740
  id: dropdownId,
27734
27741
  placeholder: "",
27742
+ className: "perseus-dropdown",
27735
27743
  onChange: value => this._handleChange(parseInt(value)),
27736
27744
  selectedValue: String(this.props.selected),
27737
27745
  disabled: this.props.apiOptions.readOnly,
@@ -32680,7 +32688,7 @@ const LegacyGrid = _ref => {
32680
32688
  * "new-awesomeness": MyAwesomeNewWidget;
32681
32689
  *
32682
32690
  * // A deprecated widget
32683
- * "super-old-widget": AutoCorrectWidget;
32691
+ * "super-old-widget": DeprecatedStandinWidget;
32684
32692
  * }
32685
32693
  * }
32686
32694
  *
@@ -32874,7 +32882,6 @@ function LockedLabel(props) {
32874
32882
 
32875
32883
  function GraphLockedLabelsLayer(props) {
32876
32884
  const {
32877
- flags,
32878
32885
  lockedFigures
32879
32886
  } = props;
32880
32887
  return lockedFigures.map((figure, i) => {
@@ -32883,30 +32890,15 @@ function GraphLockedLabelsLayer(props) {
32883
32890
  key: `label-${i}`
32884
32891
  }, figure));
32885
32892
  }
32886
- if (
32887
- // Point flag + point type
32888
- flags?.["mafs"]?.["locked-point-labels"] && figure.type === "point" ||
32889
- // Line flag + line type
32890
- flags?.["mafs"]?.["locked-line-labels"] && figure.type === "line" ||
32891
- // Vector flag + vector type
32892
- flags?.["mafs"]?.["locked-vector-labels"] && figure.type === "vector" ||
32893
- // Ellipse flag + ellipse type
32894
- flags?.["mafs"]?.["locked-ellipse-labels"] && figure.type === "ellipse" ||
32895
- // Polygon flag + polygon type
32896
- flags?.["mafs"]?.["locked-polygon-labels"] && figure.type === "polygon" ||
32897
- // Function flag + function type
32898
- flags?.["mafs"]?.["locked-function-labels"] && figure.type === "function") {
32899
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, {
32900
- key: i
32901
- }, figure.labels?.map((label, j) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
32902
- key: `${i}-label-${j}`
32903
- }, label))), figure.type === "line" && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, figure.points[0].labels?.map((label, k) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
32904
- key: `locked-figure-${i}-point-1-label-${k}`
32905
- }, label))), figure.points[1].labels?.map((label, k) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
32906
- key: `locked-figure-${i}-point-2-label-${k}`
32907
- }, label)))));
32908
- }
32909
- return null;
32893
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, {
32894
+ key: i
32895
+ }, figure.labels?.map((label, j) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
32896
+ key: `${i}-label-${j}`
32897
+ }, label))), figure.type === "line" && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, figure.points[0].labels?.map((label, k) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
32898
+ key: `locked-figure-${i}-point-1-label-${k}`
32899
+ }, label))), figure.points[1].labels?.map((label, k) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
32900
+ key: `locked-figure-${i}-point-2-label-${k}`
32901
+ }, label)))));
32910
32902
  });
32911
32903
  }
32912
32904
 
@@ -32918,10 +32910,9 @@ const LockedEllipse = props => {
32918
32910
  color,
32919
32911
  fillStyle,
32920
32912
  strokeStyle,
32921
- ariaLabel,
32922
- flags
32913
+ ariaLabel
32923
32914
  } = props;
32924
- const hasAria = ariaLabel && flags?.["mafs"]?.["locked-figures-aria"];
32915
+ const hasAria = !!ariaLabel;
32925
32916
  return /*#__PURE__*/React__namespace.createElement("g", {
32926
32917
  className: "locked-ellipse",
32927
32918
  "aria-label": hasAria ? ariaLabel : undefined,
@@ -32958,7 +32949,7 @@ const LockedFunction = props => {
32958
32949
  style: strokeStyle,
32959
32950
  domain
32960
32951
  };
32961
- const hasAria = props.ariaLabel && props.flags?.["mafs"]?.["locked-figures-aria"];
32952
+ const hasAria = !!props.ariaLabel;
32962
32953
  React.useEffect(() => {
32963
32954
  // Parsing the equation in a "useEffect" hook saves about 2ms each frame
32964
32955
  // when the learner is interacting with the graph (i.e. moving points).
@@ -33085,11 +33076,10 @@ const LockedLine = props => {
33085
33076
  showPoint1,
33086
33077
  showPoint2,
33087
33078
  ariaLabel,
33088
- flags,
33089
33079
  range
33090
33080
  } = props;
33091
33081
  const [point1, point2] = points;
33092
- const hasAria = ariaLabel && flags?.["mafs"]?.["locked-figures-aria"];
33082
+ const hasAria = !!ariaLabel;
33093
33083
  let line;
33094
33084
  if (kind === "ray") {
33095
33085
  // Rays extend to the end of the graph in one direction.
@@ -33157,14 +33147,13 @@ const LockedLine = props => {
33157
33147
 
33158
33148
  const LockedPoint = props => {
33159
33149
  const {
33160
- flags,
33161
33150
  color,
33162
33151
  coord,
33163
33152
  filled,
33164
33153
  ariaLabel
33165
33154
  } = props;
33166
33155
  const [x, y] = coord;
33167
- const hasAria = ariaLabel && flags?.["mafs"]?.["locked-figures-aria"];
33156
+ const hasAria = !!ariaLabel;
33168
33157
  return /*#__PURE__*/React__namespace.createElement("g", {
33169
33158
  className: "locked-point",
33170
33159
  "aria-label": hasAria ? ariaLabel : undefined,
@@ -33190,7 +33179,7 @@ const LockedPolygon = props => {
33190
33179
  fillStyle,
33191
33180
  strokeStyle
33192
33181
  } = props;
33193
- const hasAria = props.ariaLabel && props.flags?.["mafs"]?.["locked-figures-aria"];
33182
+ const hasAria = !!props.ariaLabel;
33194
33183
  return /*#__PURE__*/React__namespace.createElement("g", {
33195
33184
  className: "locked-polygon",
33196
33185
  "aria-label": hasAria ? props.ariaLabel : undefined,
@@ -33221,11 +33210,10 @@ const LockedVector = props => {
33221
33210
  const {
33222
33211
  color,
33223
33212
  points,
33224
- ariaLabel,
33225
- flags
33213
+ ariaLabel
33226
33214
  } = props;
33227
33215
  const [tail, tip] = points;
33228
- const hasAria = ariaLabel && flags?.["mafs"]?.["locked-figures-aria"];
33216
+ const hasAria = !!ariaLabel;
33229
33217
  return /*#__PURE__*/React__namespace.createElement("g", {
33230
33218
  className: "locked-vector",
33231
33219
  "aria-label": hasAria ? ariaLabel : undefined,
@@ -33239,7 +33227,6 @@ const LockedVector = props => {
33239
33227
 
33240
33228
  const GraphLockedLayer = props => {
33241
33229
  const {
33242
- flags,
33243
33230
  lockedFigures
33244
33231
  } = props;
33245
33232
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, lockedFigures.map((figure, index) => {
@@ -33247,40 +33234,28 @@ const GraphLockedLayer = props => {
33247
33234
  case "point":
33248
33235
  return /*#__PURE__*/React__namespace.createElement(LockedPoint, _extends({
33249
33236
  key: `point-${index}`
33250
- }, figure, {
33251
- flags: flags
33252
- }));
33237
+ }, figure));
33253
33238
  case "line":
33254
33239
  return /*#__PURE__*/React__namespace.createElement(LockedLine, _extends({
33255
33240
  key: `line-${index}`,
33256
33241
  range: props.range
33257
- }, figure, {
33258
- flags: flags
33259
- }));
33242
+ }, figure));
33260
33243
  case "vector":
33261
33244
  return /*#__PURE__*/React__namespace.createElement(LockedVector, _extends({
33262
33245
  key: `vector-${index}`
33263
- }, figure, {
33264
- flags: flags
33265
- }));
33246
+ }, figure));
33266
33247
  case "ellipse":
33267
33248
  return /*#__PURE__*/React__namespace.createElement(LockedEllipse, _extends({
33268
33249
  key: `ellipse-${index}`
33269
- }, figure, {
33270
- flags: flags
33271
- }));
33250
+ }, figure));
33272
33251
  case "polygon":
33273
33252
  return /*#__PURE__*/React__namespace.createElement(LockedPolygon, _extends({
33274
33253
  key: `polygon-${index}`
33275
- }, figure, {
33276
- flags: flags
33277
- }));
33254
+ }, figure));
33278
33255
  case "function":
33279
33256
  return /*#__PURE__*/React__namespace.createElement(LockedFunction, _extends({
33280
33257
  key: `function-${index}`
33281
- }, figure, {
33282
- flags: flags
33283
- }));
33258
+ }, figure));
33284
33259
  case "label":
33285
33260
  // This is rendered outside the SVG element, since
33286
33261
  // TeX cannot be rendered inside an SVG.
@@ -34459,10 +34434,10 @@ function AngleGraph(props) {
34459
34434
  angleMeasure,
34460
34435
  vertexX: srFormatNumber(coords[1][X], locale),
34461
34436
  vertexY: srFormatNumber(coords[1][Y], locale),
34462
- isX: srFormatNumber(coords[2][X], locale),
34463
- isY: srFormatNumber(coords[2][Y], locale),
34464
- tsX: srFormatNumber(coords[0][X], locale),
34465
- tsY: srFormatNumber(coords[0][Y], locale)
34437
+ startingSideX: srFormatNumber(coords[2][X], locale),
34438
+ startingSideY: srFormatNumber(coords[2][Y], locale),
34439
+ endingSideX: srFormatNumber(coords[0][X], locale),
34440
+ endingSideY: srFormatNumber(coords[0][Y], locale)
34466
34441
  });
34467
34442
  const formatCoordinates = (x, y) => ({
34468
34443
  x: srFormatNumber(x, locale),
@@ -35922,11 +35897,9 @@ const MafsGraph = props => {
35922
35897
  })),
35923
35898
  // Only render the axis ticks and arrows if the markings are set to a full "graph"
35924
35899
  props.markings === "graph" && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisTicks, null), /*#__PURE__*/React__namespace.createElement(AxisArrows, null)), /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
35925
- flags: props.flags,
35926
35900
  lockedFigures: props.lockedFigures,
35927
35901
  range: state.range
35928
- }))), props.flags?.["mafs"]?.["interactive-graph-locked-features-labels"] && props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLabelsLayer, {
35929
- flags: props.flags,
35902
+ }))), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLabelsLayer, {
35930
35903
  lockedFigures: props.lockedFigures
35931
35904
  }), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
35932
35905
  style: {
@@ -45936,7 +45909,7 @@ const init = function () {
45936
45909
 
45937
45910
  // This file is processed by a Rollup plugin (replace) to inject the production
45938
45911
  const libName = "@khanacademy/perseus";
45939
- const libVersion = "49.1.7";
45912
+ const libVersion = "49.2.1";
45940
45913
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45941
45914
 
45942
45915
  const apiVersion = {
@@ -48292,9 +48265,9 @@ function parseWidgetWithVersion(parseVersion, parseType, parseOptions) {
48292
48265
  const parseCategorizerWidget = parseWidget(constant("categorizer"), object({
48293
48266
  items: array(string),
48294
48267
  categories: array(string),
48295
- randomizeItems: boolean,
48268
+ randomizeItems: defaulted(boolean, () => false),
48296
48269
  static: defaulted(boolean, () => false),
48297
- values: array(number),
48270
+ values: array(defaulted(number, () => 0)),
48298
48271
  highlightLint: optional(boolean),
48299
48272
  linterContext: optional(object({
48300
48273
  contentType: string,
@@ -48314,7 +48287,7 @@ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
48314
48287
  showButtons: boolean,
48315
48288
  width: number,
48316
48289
  height: number,
48317
- static: boolean
48290
+ static: defaulted(boolean, () => false)
48318
48291
  }));
48319
48292
 
48320
48293
  const parseDefinitionWidget = parseWidget(constant("definition"), object({
@@ -48324,7 +48297,7 @@ const parseDefinitionWidget = parseWidget(constant("definition"), object({
48324
48297
  }));
48325
48298
 
48326
48299
  const parseDropdownWidget = parseWidget(constant("dropdown"), object({
48327
- placeholder: string,
48300
+ placeholder: defaulted(string, () => ""),
48328
48301
  ariaLabel: optional(string),
48329
48302
  visibleLabel: optional(string),
48330
48303
  static: defaulted(boolean, () => false),
@@ -48341,33 +48314,128 @@ const parseExplanationWidget = parseWidget(constant("explanation"), object({
48341
48314
  // We wrap parseWidgetsMap in a function here to make sure it is not
48342
48315
  // referenced before it is defined. There is an import cycle between
48343
48316
  // this file and widgets-map.ts that could cause it to be undefined.
48344
- widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
48345
- static: boolean
48317
+ widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
48318
+ static: defaulted(boolean, () => false)
48346
48319
  }));
48347
48320
 
48348
- const parseAnswerForm = object({
48349
- value: string,
48350
- form: boolean,
48351
- simplify: boolean,
48321
+ // Given a function, creates a PartialParser that converts one type to another
48322
+ // using that function. The returned parser never fails.
48323
+ function convert(f) {
48324
+ return (rawValue, ctx) => ctx.success(f(rawValue));
48325
+ }
48326
+
48327
+ /**
48328
+ * Creates a parser for a widget options type with multiple major versions. Old
48329
+ * versions are migrated to the latest version. The parse fails if the input
48330
+ * data does not match any of the versions.
48331
+ *
48332
+ * @example
48333
+ * const parseOptions = versionedWidgetOptions(3, parseOptionsV3)
48334
+ * .withMigrationFrom(2, parseOptionsV2, migrateV2ToV3)
48335
+ * .withMigrationFrom(1, parseOptionsV1, migrateV1ToV2)
48336
+ * .withMigrationFrom(0, parseOptionsV0, migrateV0ToV1)
48337
+ * .parser;
48338
+ *
48339
+ * @param latestMajorVersion the latest major version of the widget options.
48340
+ * @param parseLatest a {@link Parser} for the latest version of the widget
48341
+ * options.
48342
+ * @returns a builder object, to which migrations from earlier versions can be
48343
+ * added. Migrations must be added in "reverse chronological" order as in the
48344
+ * example above.
48345
+ */
48346
+ function versionedWidgetOptions(latestMajorVersion, parseLatest) {
48347
+ return new VersionedWidgetOptionsParserBuilder(latestMajorVersion, parseLatest, latest => latest, (raw, ctx) => ctx.failure("widget options with a known version number", raw));
48348
+ }
48349
+ class VersionedWidgetOptionsParserBuilder {
48350
+ parser;
48351
+ constructor(majorVersion, parseThisVersion, migrateToLatest, parseOtherVersions) {
48352
+ this.migrateToLatest = migrateToLatest;
48353
+ this.parseOtherVersions = parseOtherVersions;
48354
+ const parseThisVersionAndMigrateToLatest = pipeParsers(parseThisVersion).then(convert(this.migrateToLatest)).parser;
48355
+ this.parser = (raw, ctx) => {
48356
+ if (!isObject(raw)) {
48357
+ return ctx.failure("object", raw);
48358
+ }
48359
+ const versionParseResult = parseVersionedObject(raw, ctx);
48360
+ if (isFailure(versionParseResult)) {
48361
+ return versionParseResult;
48362
+ }
48363
+ if (versionParseResult.value.version.major !== majorVersion) {
48364
+ return this.parseOtherVersions(raw, ctx);
48365
+ }
48366
+ return parseThisVersionAndMigrateToLatest(raw, ctx);
48367
+ };
48368
+ }
48369
+
48370
+ /**
48371
+ * Add a migration from an old version of the widget options.
48372
+ */
48373
+ withMigrationFrom(majorVersion, parseOldVersion, migrateToNextVersion) {
48374
+ const parseOtherVersions = this.parser;
48375
+ const migrateToLatest = old => this.migrateToLatest(migrateToNextVersion(old));
48376
+ return new VersionedWidgetOptionsParserBuilder(majorVersion, parseOldVersion, migrateToLatest, parseOtherVersions);
48377
+ }
48378
+ }
48379
+ const parseVersionedObject = object({
48380
+ version: defaulted(object({
48381
+ major: number,
48382
+ minor: number
48383
+ }), () => ({
48384
+ major: 0,
48385
+ minor: 0
48386
+ }))
48387
+ });
48388
+
48389
+ const stringOrNumberOrNullOrUndefined = union(string).or(number).or(constant(null)).or(constant(undefined)).parser;
48390
+ const parsePossiblyInvalidAnswerForm = object({
48391
+ // `value` is the possibly invalid part of this. It should always be a
48392
+ // string, but some answer forms don't have it. The Expression widget
48393
+ // ignores invalid values, so we can safely filter them out during parsing.
48394
+ value: optional(string),
48395
+ form: defaulted(boolean, () => false),
48396
+ simplify: defaulted(boolean, () => false),
48352
48397
  considered: enumeration("correct", "wrong", "ungraded"),
48353
- key: pipeParsers(optional(union(string).or(number).parser)).then((key, ctx) => ctx.success(String(key))).parser
48398
+ key: pipeParsers(stringOrNumberOrNullOrUndefined).then(convert(String)).parser
48354
48399
  });
48355
- const parseExpressionWidgetV1 = parseWidgetWithVersion(object({
48400
+ function removeInvalidAnswerForms(possiblyInvalid) {
48401
+ const valid = [];
48402
+ for (const answerForm of possiblyInvalid) {
48403
+ const {
48404
+ value
48405
+ } = answerForm;
48406
+ if (value != null) {
48407
+ // Copying the object seems to be needed to make TypeScript happy
48408
+ valid.push({
48409
+ ...answerForm,
48410
+ value
48411
+ });
48412
+ }
48413
+ }
48414
+ return valid;
48415
+ }
48416
+
48417
+ // NOTE(benchristel): I copied the default buttonSets from
48418
+ // expression.tsx. See the parse-perseus-json/README.md for
48419
+ // an explanation of why we want to duplicate the default here.
48420
+ const parseButtonSets = defaulted(array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")), () => ["basic", "trig", "prealgebra", "logarithms"]);
48421
+ const version1 = object({
48356
48422
  major: constant(1),
48357
48423
  minor: number
48358
- }), constant("expression"), object({
48359
- answerForms: array(parseAnswerForm),
48424
+ });
48425
+ const parseExpressionWidgetV1 = parseWidgetWithVersion(version1, constant("expression"), object({
48426
+ answerForms: pipeParsers(array(parsePossiblyInvalidAnswerForm)).then(convert(removeInvalidAnswerForms)).parser,
48360
48427
  functions: array(string),
48361
48428
  times: boolean,
48362
48429
  visibleLabel: optional(string),
48363
48430
  ariaLabel: optional(string),
48364
- buttonSets: array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")),
48431
+ buttonSets: parseButtonSets,
48365
48432
  buttonsVisible: optional(enumeration("always", "never", "focused"))
48366
48433
  }));
48367
- const parseExpressionWidgetV0 = parseWidgetWithVersion(optional(object({
48434
+ const version0 = optional(object({
48368
48435
  major: constant(0),
48369
48436
  minor: number
48370
- })), constant("expression"), object({
48437
+ }));
48438
+ const parseExpressionWidgetV0 = parseWidgetWithVersion(version0, constant("expression"), object({
48371
48439
  functions: array(string),
48372
48440
  times: boolean,
48373
48441
  visibleLabel: optional(string),
@@ -48375,14 +48443,14 @@ const parseExpressionWidgetV0 = parseWidgetWithVersion(optional(object({
48375
48443
  form: boolean,
48376
48444
  simplify: boolean,
48377
48445
  value: string,
48378
- buttonSets: array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")),
48446
+ buttonSets: parseButtonSets,
48379
48447
  buttonsVisible: optional(enumeration("always", "never", "focused"))
48380
48448
  }));
48381
- function migrateV0ToV1(widget, ctx) {
48449
+ function migrateV0ToV1(widget) {
48382
48450
  const {
48383
48451
  options
48384
48452
  } = widget;
48385
- return ctx.success({
48453
+ return {
48386
48454
  ...widget,
48387
48455
  version: {
48388
48456
  major: 1,
@@ -48402,9 +48470,9 @@ function migrateV0ToV1(widget, ctx) {
48402
48470
  value: options.value
48403
48471
  }]
48404
48472
  }
48405
- });
48473
+ };
48406
48474
  }
48407
- const parseExpressionWidget = union(parseExpressionWidgetV1).or(pipeParsers(parseExpressionWidgetV0).then(migrateV0ToV1).parser).parser;
48475
+ const parseExpressionWidget = versionedWidgetOptions(1, parseExpressionWidgetV1).withMigrationFrom(0, parseExpressionWidgetV0, migrateV0ToV1).parser;
48408
48476
 
48409
48477
  const parsePerseusRenderer = defaulted(object({
48410
48478
  // TODO(benchristel): content is also defaulted to empty string in
@@ -48425,13 +48493,14 @@ const parsePerseusRenderer = defaulted(object({
48425
48493
  images: {}
48426
48494
  }));
48427
48495
 
48496
+ const falseToNull = pipeParsers(constant(false)).then(convert(() => null)).parser;
48428
48497
  const parseGradedGroupWidgetOptions = object({
48429
48498
  title: defaulted(string, () => ""),
48430
48499
  hasHint: optional(nullable(boolean)),
48431
48500
  // This module has an import cycle with parsePerseusRenderer.
48432
48501
  // The anonymous function below ensures that we don't try to access
48433
48502
  // parsePerseusRenderer before it's defined.
48434
- hint: optional(nullable((rawVal, ctx) => parsePerseusRenderer(rawVal, ctx))),
48503
+ hint: union(falseToNull).or(constant(null)).or(constant(undefined)).or((rawVal, ctx) => parsePerseusRenderer(rawVal, ctx)).parser,
48435
48504
  content: string,
48436
48505
  // This module has an import cycle with parseWidgetsMap.
48437
48506
  // The anonymous function below ensures that we don't try to access
@@ -48450,31 +48519,69 @@ const parseGradedGroupSetWidget = parseWidget(constant("graded-group-set"), obje
48450
48519
  gradedGroups: array(parseGradedGroupWidgetOptions)
48451
48520
  }));
48452
48521
 
48522
+ /**
48523
+ * discriminatedUnion() should be preferred over union() when parsing a
48524
+ * discriminated union type, because discriminatedUnion() produces more
48525
+ * understandable failure messages. It takes the discriminant as the source of
48526
+ * truth for which variant is to be parsed, and expects the other data to match
48527
+ * that variant.
48528
+ */
48529
+ function discriminatedUnionOn(discriminantKey) {
48530
+ const noMoreBranches = (raw, ctx) => {
48531
+ if (!isObject(raw)) {
48532
+ return ctx.failure("object", raw);
48533
+ }
48534
+ return ctx.forSubtree(discriminantKey).failure("a valid value", raw[discriminantKey]);
48535
+ };
48536
+ return new DiscriminatedUnionBuilder(discriminantKey, noMoreBranches);
48537
+ }
48538
+ class DiscriminatedUnionBuilder {
48539
+ constructor(discriminantKey, parser) {
48540
+ this.discriminantKey = discriminantKey;
48541
+ this.parser = parser;
48542
+ }
48543
+ withBranch(discriminantValue, parseNewVariant) {
48544
+ const parseNewBranch = discriminatedUnionBranch(this.discriminantKey, discriminantValue, parseNewVariant, this.parser);
48545
+ return new DiscriminatedUnionBuilder(this.discriminantKey, parseNewBranch);
48546
+ }
48547
+ }
48548
+ function discriminatedUnionBranch(discriminantKey, discriminantValue, parseVariant, parseOtherBranches) {
48549
+ return (raw, ctx) => {
48550
+ if (!isObject(raw)) {
48551
+ return ctx.failure("object", raw);
48552
+ }
48553
+ if (raw[discriminantKey] === discriminantValue) {
48554
+ return parseVariant(raw, ctx);
48555
+ }
48556
+ return parseOtherBranches(raw, ctx);
48557
+ };
48558
+ }
48559
+
48453
48560
  const pairOfNumbers$3 = pair(number, number);
48454
48561
  const pairOfPoints = pair(pairOfNumbers$3, pairOfNumbers$3);
48455
48562
  const parseGrapherWidget = parseWidget(constant("grapher"), object({
48456
48563
  availableTypes: array(enumeration("absolute_value", "exponential", "linear", "logarithm", "quadratic", "sinusoid", "tangent")),
48457
- correct: union(object({
48564
+ correct: discriminatedUnionOn("type").withBranch("absolute_value", object({
48458
48565
  type: constant("absolute_value"),
48459
48566
  coords: pairOfPoints
48460
- })).or(object({
48567
+ })).withBranch("exponential", object({
48461
48568
  type: constant("exponential"),
48462
48569
  asymptote: pairOfPoints,
48463
48570
  coords: pairOfPoints
48464
- })).or(object({
48571
+ })).withBranch("linear", object({
48465
48572
  type: constant("linear"),
48466
- coords: pairOfPoints
48467
- })).or(object({
48573
+ coords: defaulted(pairOfPoints, () => [[-5, 5], [5, 5]])
48574
+ })).withBranch("logarithm", object({
48468
48575
  type: constant("logarithm"),
48469
48576
  asymptote: pairOfPoints,
48470
48577
  coords: pairOfPoints
48471
- })).or(object({
48578
+ })).withBranch("quadratic", object({
48472
48579
  type: constant("quadratic"),
48473
48580
  coords: pairOfPoints
48474
- })).or(object({
48581
+ })).withBranch("sinusoid", object({
48475
48582
  type: constant("sinusoid"),
48476
48583
  coords: pairOfPoints
48477
- })).or(object({
48584
+ })).withBranch("tangent", object({
48478
48585
  type: constant("tangent"),
48479
48586
  coords: pairOfPoints
48480
48587
  })).parser,
@@ -48534,15 +48641,22 @@ const stringToNumber = (rawValue, ctx) => {
48534
48641
  return ctx.success(parsedNumber);
48535
48642
  };
48536
48643
 
48537
- const numericToNumber = pipeParsers(union(number).or(string).parser).then(stringToNumber).parser;
48644
+ function emptyToZero(x) {
48645
+ return x === "" ? 0 : x;
48646
+ }
48647
+ const imageDimensionToNumber = pipeParsers(union(number).or(string).parser)
48648
+ // In this specific case, empty string is equivalent to zero. An empty
48649
+ // string parses to either NaN (using parseInt) or 0 (using unary +) and
48650
+ // CSS will treat NaN as invalid and default to 0 instead.
48651
+ .then(convert(emptyToZero)).then(stringToNumber).parser;
48538
48652
  const parsePerseusImageBackground = object({
48539
48653
  url: optional(nullable(string)),
48540
- width: optional(numericToNumber),
48541
- height: optional(numericToNumber),
48542
- top: optional(numericToNumber),
48543
- left: optional(numericToNumber),
48544
- bottom: optional(numericToNumber),
48545
- scale: optional(numericToNumber)
48654
+ width: optional(imageDimensionToNumber),
48655
+ height: optional(imageDimensionToNumber),
48656
+ top: optional(imageDimensionToNumber),
48657
+ left: optional(imageDimensionToNumber),
48658
+ bottom: optional(imageDimensionToNumber),
48659
+ scale: optional(imageDimensionToNumber)
48546
48660
  });
48547
48661
 
48548
48662
  const pairOfNumbers$2 = pair(number, number);
@@ -48583,8 +48697,10 @@ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
48583
48697
  }));
48584
48698
 
48585
48699
  const pairOfNumbers$1 = pair(number, number);
48700
+ const stringOrEmpty = defaulted(string, () => "");
48701
+ const parseFunctionType = constant("function");
48586
48702
  const parseFunctionElement = object({
48587
- type: constant("function"),
48703
+ type: parseFunctionType,
48588
48704
  key: string,
48589
48705
  options: object({
48590
48706
  value: string,
@@ -48596,8 +48712,9 @@ const parseFunctionElement = object({
48596
48712
  strokeWidth: number
48597
48713
  })
48598
48714
  });
48715
+ const parseLabelType = constant("label");
48599
48716
  const parseLabelElement = object({
48600
- type: constant("label"),
48717
+ type: parseLabelType,
48601
48718
  key: string,
48602
48719
  options: object({
48603
48720
  label: string,
@@ -48606,8 +48723,9 @@ const parseLabelElement = object({
48606
48723
  coordY: string
48607
48724
  })
48608
48725
  });
48726
+ const parseLineType = constant("line");
48609
48727
  const parseLineElement = object({
48610
- type: constant("line"),
48728
+ type: parseLineType,
48611
48729
  key: string,
48612
48730
  options: object({
48613
48731
  color: string,
@@ -48620,8 +48738,9 @@ const parseLineElement = object({
48620
48738
  arrows: string
48621
48739
  })
48622
48740
  });
48741
+ const parseMovableLineType = constant("movable-line");
48623
48742
  const parseMovableLineElement = object({
48624
- type: constant("movable-line"),
48743
+ type: parseMovableLineType,
48625
48744
  key: string,
48626
48745
  options: object({
48627
48746
  startX: string,
@@ -48639,8 +48758,9 @@ const parseMovableLineElement = object({
48639
48758
  constraintYMax: string
48640
48759
  })
48641
48760
  });
48761
+ const parseMovablePointType = constant("movable-point");
48642
48762
  const parseMovablePointElement = object({
48643
- type: constant("movable-point"),
48763
+ type: parseMovablePointType,
48644
48764
  key: string,
48645
48765
  options: object({
48646
48766
  startX: string,
@@ -48649,14 +48769,15 @@ const parseMovablePointElement = object({
48649
48769
  constraint: string,
48650
48770
  snap: number,
48651
48771
  constraintFn: string,
48652
- constraintXMin: string,
48653
- constraintXMax: string,
48654
- constraintYMin: string,
48655
- constraintYMax: string
48772
+ constraintXMin: stringOrEmpty,
48773
+ constraintXMax: stringOrEmpty,
48774
+ constraintYMin: stringOrEmpty,
48775
+ constraintYMax: stringOrEmpty
48656
48776
  })
48657
48777
  });
48778
+ const parseParametricType = constant("parametric");
48658
48779
  const parseParametricElement = object({
48659
- type: constant("parametric"),
48780
+ type: parseParametricType,
48660
48781
  key: string,
48661
48782
  options: object({
48662
48783
  x: string,
@@ -48668,8 +48789,9 @@ const parseParametricElement = object({
48668
48789
  strokeWidth: number
48669
48790
  })
48670
48791
  });
48792
+ const parsePointType = constant("point");
48671
48793
  const parsePointElement = object({
48672
- type: constant("point"),
48794
+ type: parsePointType,
48673
48795
  key: string,
48674
48796
  options: object({
48675
48797
  color: string,
@@ -48677,8 +48799,9 @@ const parsePointElement = object({
48677
48799
  coordY: string
48678
48800
  })
48679
48801
  });
48802
+ const parseRectangleType = constant("rectangle");
48680
48803
  const parseRectangleElement = object({
48681
- type: constant("rectangle"),
48804
+ type: parseRectangleType,
48682
48805
  key: string,
48683
48806
  options: object({
48684
48807
  color: string,
@@ -48689,7 +48812,7 @@ const parseRectangleElement = object({
48689
48812
  })
48690
48813
  });
48691
48814
  const parseInteractionWidget = parseWidget(constant("interaction"), object({
48692
- static: boolean,
48815
+ static: defaulted(boolean, () => false),
48693
48816
  graph: object({
48694
48817
  editableSettings: optional(array(enumeration("canvas", "graph"))),
48695
48818
  box: pairOfNumbers$1,
@@ -48706,7 +48829,7 @@ const parseInteractionWidget = parseWidget(constant("interaction"), object({
48706
48829
  rulerTicks: optional(number),
48707
48830
  tickStep: pairOfNumbers$1
48708
48831
  }),
48709
- elements: array(union(parseFunctionElement).or(parseLabelElement).or(parseLineElement).or(parseMovableLineElement).or(parseMovablePointElement).or(parseParametricElement).or(parsePointElement).or(parseRectangleElement).parser)
48832
+ elements: array(discriminatedUnionOn("type").withBranch("function", parseFunctionElement).withBranch("label", parseLabelElement).withBranch("line", parseLineElement).withBranch("movable-line", parseMovableLineElement).withBranch("movable-point", parseMovablePointElement).withBranch("parametric", parseParametricElement).withBranch("point", parsePointElement).withBranch("rectangle", parseRectangleElement).parser)
48710
48833
  }));
48711
48834
 
48712
48835
  // Used to represent 2-D points and ranges
@@ -48764,7 +48887,7 @@ const parsePerseusGraphTypePolygon = object({
48764
48887
  showAngles: optional(boolean),
48765
48888
  showSides: optional(boolean),
48766
48889
  snapTo: optional(enumeration("grid", "angles", "sides")),
48767
- match: optional(enumeration("similar", "congruent", "approx")),
48890
+ match: optional(enumeration("similar", "congruent", "approx", "exact")),
48768
48891
  startCoords: optional(array(pairOfNumbers)),
48769
48892
  // TODO: remove coord? it's legacy.
48770
48893
  coord: optional(pairOfNumbers)
@@ -48799,7 +48922,7 @@ const parsePerseusGraphTypeSinusoid = object({
48799
48922
  // TODO: remove coord? it's legacy.
48800
48923
  coord: optional(pairOfNumbers)
48801
48924
  });
48802
- const parsePerseusGraphType = union(parsePerseusGraphTypeAngle).or(parsePerseusGraphTypeAngle).or(parsePerseusGraphTypeCircle).or(parsePerseusGraphTypeLinear).or(parsePerseusGraphTypeLinearSystem).or(parsePerseusGraphTypeNone).or(parsePerseusGraphTypePoint).or(parsePerseusGraphTypePolygon).or(parsePerseusGraphTypeQuadratic).or(parsePerseusGraphTypeRay).or(parsePerseusGraphTypeSegment).or(parsePerseusGraphTypeSinusoid).parser;
48925
+ const parsePerseusGraphType = discriminatedUnionOn("type").withBranch("angle", parsePerseusGraphTypeAngle).withBranch("circle", parsePerseusGraphTypeCircle).withBranch("linear", parsePerseusGraphTypeLinear).withBranch("linear-system", parsePerseusGraphTypeLinearSystem).withBranch("none", parsePerseusGraphTypeNone).withBranch("point", parsePerseusGraphTypePoint).withBranch("polygon", parsePerseusGraphTypePolygon).withBranch("quadratic", parsePerseusGraphTypeQuadratic).withBranch("ray", parsePerseusGraphTypeRay).withBranch("segment", parsePerseusGraphTypeSegment).withBranch("sinusoid", parsePerseusGraphTypeSinusoid).parser;
48803
48926
  const parseLockedFigureColor = enumeration(...lockedFigureColorNames);
48804
48927
  const parseLockedFigureFillType = enumeration("none", "white", "translucent", "solid");
48805
48928
  const parseLockedLineStyle = enumeration("solid", "dashed");
@@ -48918,7 +49041,7 @@ const parseLabelImageWidget = parseWidget(constant("label-image"), object({
48918
49041
  })),
48919
49042
  hideChoicesFromInstructions: boolean,
48920
49043
  multipleAnswers: boolean,
48921
- static: boolean
49044
+ static: defaulted(boolean, () => false)
48922
49045
  }));
48923
49046
 
48924
49047
  const parseMatcherWidget = parseWidget(constant("matcher"), object({
@@ -48929,10 +49052,12 @@ const parseMatcherWidget = parseWidget(constant("matcher"), object({
48929
49052
  padding: boolean
48930
49053
  }));
48931
49054
 
49055
+ const numberOrString = union(number).or(string).parser;
49056
+ const numeric = pipeParsers(defaulted(numberOrString, () => NaN)).then(stringToNumber).parser;
48932
49057
  const parseMatrixWidget = parseWidget(defaulted(constant("matrix"), () => "matrix"), object({
48933
49058
  prefix: optional(string),
48934
49059
  suffix: optional(string),
48935
- answers: array(array(number)),
49060
+ answers: array(array(numeric)),
48936
49061
  cursorPosition: optional(array(number)),
48937
49062
  matrixBoardSize: array(number),
48938
49063
  static: optional(boolean)
@@ -48947,7 +49072,8 @@ const parseMeasurerWidget = parseWidget(constant("measurer"), object({
48947
49072
  rulerPixels: number,
48948
49073
  rulerLength: number,
48949
49074
  box: pair(number, number),
48950
- static: boolean
49075
+ // TODO(benchristel): static is not used. Remove it?
49076
+ static: defaulted(boolean, () => false)
48951
49077
  }));
48952
49078
 
48953
49079
  const parseMoleculeRendererWidget = parseWidget(constant("molecule-renderer"), object({
@@ -48956,9 +49082,10 @@ const parseMoleculeRendererWidget = parseWidget(constant("molecule-renderer"), o
48956
49082
  smiles: optional(string)
48957
49083
  }));
48958
49084
 
49085
+ const emptyStringToNull = pipeParsers(constant("")).then(convert(() => null)).parser;
48959
49086
  const parseNumberLineWidget = parseWidget(constant("number-line"), object({
48960
49087
  range: array(number),
48961
- labelRange: array(nullable(number)),
49088
+ labelRange: array(nullable(union(number).or(emptyStringToNull).parser)),
48962
49089
  labelStyle: string,
48963
49090
  labelTicks: boolean,
48964
49091
  isTickCtrl: optional(nullable(boolean)),
@@ -48970,26 +49097,32 @@ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
48970
49097
  snapDivisions: defaulted(number, () => 2),
48971
49098
  tickStep: optional(nullable(number)),
48972
49099
  correctRel: optional(nullable(string)),
48973
- correctX: number,
49100
+ correctX: nullable(number),
48974
49101
  initialX: optional(nullable(number)),
48975
49102
  showTooltip: optional(boolean),
48976
- static: boolean
49103
+ static: defaulted(boolean, () => false)
48977
49104
  }));
48978
49105
 
48979
49106
  const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "decimal", "percent", "pi");
48980
49107
  const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
48981
49108
  answers: array(object({
48982
49109
  message: string,
48983
- value: optional(number),
49110
+ // TODO(benchristel): value should never be null or undefined,
49111
+ // but we have some content where it is anyway. If we backfill
49112
+ // the data, simplify this.
49113
+ value: optional(nullable(number)),
48984
49114
  status: string,
48985
49115
  answerForms: optional(array(parseMathFormat)),
48986
49116
  strict: boolean,
48987
49117
  maxError: optional(nullable(number)),
48988
- simplify: optional(nullable(string))
49118
+ // TODO(benchristel): simplify should never be a boolean, but we
49119
+ // have some content where it is anyway. If we ever backfill
49120
+ // the data, we should simplify `simplify`.
49121
+ simplify: optional(nullable(union(string).or(pipeParsers(boolean).then(convert(String)).parser).parser))
48989
49122
  })),
48990
49123
  labelText: optional(string),
48991
49124
  size: string,
48992
- coefficient: boolean,
49125
+ coefficient: defaulted(boolean, () => false),
48993
49126
  rightAlign: optional(boolean),
48994
49127
  static: defaulted(boolean, () => false),
48995
49128
  answerForms: optional(array(object({
@@ -49021,13 +49154,13 @@ const parseOrdererWidget = parseWidget(constant("orderer"), object({
49021
49154
  const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
49022
49155
  passageNumber: number,
49023
49156
  referenceNumber: number,
49024
- summaryText: string
49157
+ summaryText: optional(string)
49025
49158
  }));
49026
49159
 
49027
49160
  const parsePassageWidget = parseWidget(constant("passage"), object({
49028
- footnotes: string,
49161
+ footnotes: defaulted(string, () => ""),
49029
49162
  passageText: string,
49030
- passageTitle: string,
49163
+ passageTitle: defaulted(string, () => ""),
49031
49164
  showLineNumbers: boolean,
49032
49165
  static: defaulted(boolean, () => false)
49033
49166
  }));
@@ -49210,6 +49343,12 @@ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
49210
49343
  // sequence is a deprecated widget type, and the corresponding
49211
49344
  // widget component no longer exists.
49212
49345
  return parseAndAssign(`sequence ${id}`, parseDeprecatedWidget);
49346
+ case "lights-puzzle":
49347
+ return parseAndAssign(`lights-puzzle ${id}`, parseDeprecatedWidget);
49348
+ case "simulator":
49349
+ return parseAndAssign(`simulator ${id}`, parseDeprecatedWidget);
49350
+ case "transformer":
49351
+ return parseAndAssign(`transformer ${id}`, parseDeprecatedWidget);
49213
49352
  default:
49214
49353
  if (getWidget(type)) {
49215
49354
  // @ts-expect-error - 'type' is not a valid widget type