@khanacademy/perseus-core 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -101,3 +101,5 @@ export { default as getTablePublicWidgetOptions } from "./widgets/table/table-ut
101
101
  export { default as getIFramePublicWidgetOptions } from "./widgets/iframe/iframe-util";
102
102
  export { default as getMatrixPublicWidgetOptions } from "./widgets/matrix/matrix-util";
103
103
  export { default as getPlotterPublicWidgetOptions } from "./widgets/plotter/plotter-util";
104
+ export { default as getMatcherPublicWidgetOptions, shuffleMatcher, } from "./widgets/matcher/matcher-util";
105
+ export { shuffle, seededRNG, random } from "./utils/random-util";
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var perseusCore = require('@khanacademy/perseus-core');
6
+
5
7
  /**
6
8
  * Adds the given perseus library version information to the __perseus_debug__
7
9
  * object and ensures that the object is attached to `globalThis` (`window` in
@@ -821,7 +823,7 @@ function times(n, iteratee, context) {
821
823
  }
822
824
 
823
825
  // Return a random integer between `min` and `max` (inclusive).
824
- function random(min, max) {
826
+ function random$1(min, max) {
825
827
  if (max == null) {
826
828
  max = min;
827
829
  min = 0;
@@ -1564,14 +1566,14 @@ function toArray(obj) {
1564
1566
  function sample(obj, n, guard) {
1565
1567
  if (n == null || guard) {
1566
1568
  if (!isArrayLike(obj)) obj = values(obj);
1567
- return obj[random(obj.length - 1)];
1569
+ return obj[random$1(obj.length - 1)];
1568
1570
  }
1569
1571
  var sample = toArray(obj);
1570
1572
  var length = getLength(sample);
1571
1573
  n = Math.max(Math.min(n, length), 0);
1572
1574
  var last = length - 1;
1573
1575
  for (var index = 0; index < n; index++) {
1574
- var rand = random(index, last);
1576
+ var rand = random$1(index, last);
1575
1577
  var temp = sample[index];
1576
1578
  sample[index] = sample[rand];
1577
1579
  sample[rand] = temp;
@@ -1580,7 +1582,7 @@ function sample(obj, n, guard) {
1580
1582
  }
1581
1583
 
1582
1584
  // Shuffle a collection.
1583
- function shuffle(obj) {
1585
+ function shuffle$1(obj) {
1584
1586
  return sample(obj, Infinity);
1585
1587
  }
1586
1588
 
@@ -1966,7 +1968,7 @@ var allExports = /*#__PURE__*/Object.freeze({
1966
1968
  matcher: matcher,
1967
1969
  matches: matcher,
1968
1970
  times: times,
1969
- random: random,
1971
+ random: random$1,
1970
1972
  now: now,
1971
1973
  escape: escape,
1972
1974
  unescape: unescape,
@@ -2023,7 +2025,7 @@ var allExports = /*#__PURE__*/Object.freeze({
2023
2025
  where: where,
2024
2026
  max: max,
2025
2027
  min: min,
2026
- shuffle: shuffle,
2028
+ shuffle: shuffle$1,
2027
2029
  sample: sample,
2028
2030
  sortBy: sortBy,
2029
2031
  groupBy: groupBy,
@@ -3075,7 +3077,7 @@ function parseWidget(parseType, parseOptions) {
3075
3077
  graded: optional(boolean),
3076
3078
  alignment: optional(string),
3077
3079
  options: parseOptions,
3078
- key: optional(number),
3080
+ key: optional(nullable(number)),
3079
3081
  version: optional(object({
3080
3082
  major: number,
3081
3083
  minor: number
@@ -3463,14 +3465,15 @@ const imageDimensionToNumber = pipeParsers(union(number).or(string).parser)
3463
3465
  // string parses to either NaN (using parseInt) or 0 (using unary +) and
3464
3466
  // CSS will treat NaN as invalid and default to 0 instead.
3465
3467
  .then(convert(emptyToZero)).then(stringToNumber).parser;
3468
+ const dimensionOrUndefined = defaulted(imageDimensionToNumber, () => undefined);
3466
3469
  const parsePerseusImageBackground = object({
3467
3470
  url: optional(nullable(string)),
3468
- width: optional(imageDimensionToNumber),
3469
- height: optional(imageDimensionToNumber),
3470
- top: optional(imageDimensionToNumber),
3471
- left: optional(imageDimensionToNumber),
3472
- bottom: optional(imageDimensionToNumber),
3473
- scale: optional(imageDimensionToNumber)
3471
+ width: dimensionOrUndefined,
3472
+ height: dimensionOrUndefined,
3473
+ top: dimensionOrUndefined,
3474
+ left: dimensionOrUndefined,
3475
+ bottom: dimensionOrUndefined,
3476
+ scale: dimensionOrUndefined
3474
3477
  });
3475
3478
 
3476
3479
  const pairOfNumbers$2 = pair(number, number);
@@ -4211,7 +4214,7 @@ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
4211
4214
  // the data, simplify this.
4212
4215
  value: optional(nullable(number)),
4213
4216
  status: string,
4214
- answerForms: optional(array(parseMathFormat)),
4217
+ answerForms: defaulted(array(parseMathFormat), () => undefined),
4215
4218
  strict: boolean,
4216
4219
  maxError: optional(nullable(number)),
4217
4220
  // TODO(benchristel): simplify should never be a boolean, but we
@@ -4309,7 +4312,7 @@ const parseRadioWidget = parseWidget(constant("radio"), object({
4309
4312
  // There is an import cycle between radio-widget.ts and
4310
4313
  // widgets-map.ts. The anonymous function below ensures that we
4311
4314
  // don't refer to parseWidgetsMap before it's defined.
4312
- widgets: optional((rawVal, ctx) => parseWidgetsMap(rawVal, ctx))
4315
+ widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => undefined)
4313
4316
  })),
4314
4317
  hasNoneOfTheAbove: optional(boolean),
4315
4318
  countChoices: optional(boolean),
@@ -4463,13 +4466,17 @@ const parseDeprecatedWidget = parseWidget(
4463
4466
  (_, ctx) => ctx.success("deprecated-standin"),
4464
4467
  // Allow any widget options
4465
4468
  object({}));
4466
- const parseStringToPositiveInt = (rawValue, ctx) => {
4467
- if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
4468
- return ctx.failure("a string representing a positive integer", rawValue);
4469
+ const parseStringToNonNegativeInt = (rawValue, ctx) => {
4470
+ // The article renderer seems to allow the numeric part of a widget ID to
4471
+ // be 0, at least for image widgets. However, if widget IDs in an exercise
4472
+ // contain 0, the exercise renderer will blow up. We allow 0 here for
4473
+ // compatibility with articles.
4474
+ if (typeof rawValue !== "string" || !/^(0|[1-9][0-9]*)$/.test(rawValue)) {
4475
+ return ctx.failure("a string representing a non-negative integer", rawValue);
4469
4476
  }
4470
4477
  return ctx.success(+rawValue);
4471
4478
  };
4472
- const parseWidgetIdComponents = pair(string, parseStringToPositiveInt);
4479
+ const parseWidgetIdComponents = pair(string, parseStringToNonNegativeInt);
4473
4480
 
4474
4481
  const parsePerseusRenderer = defaulted(object({
4475
4482
  // TODO(benchristel): content is also defaulted to empty string in
@@ -4480,8 +4487,9 @@ const parsePerseusRenderer = defaulted(object({
4480
4487
  // The anonymous function below ensures that we don't try to access
4481
4488
  // parseWidgetsMap before it's defined.
4482
4489
  widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
4483
- metadata: optional(array(string)),
4484
- images: parseImages
4490
+ images: parseImages,
4491
+ // deprecated
4492
+ metadata: any
4485
4493
  }),
4486
4494
  // Default value
4487
4495
  () => ({
@@ -4496,14 +4504,44 @@ const parseHint = object({
4496
4504
  replace: optional(boolean),
4497
4505
  content: string,
4498
4506
  widgets: defaulted(parseWidgetsMap, () => ({})),
4499
- metadata: optional(array(string)),
4500
- images: parseImages
4507
+ images: parseImages,
4508
+ // deprecated
4509
+ metadata: any
4501
4510
  });
4502
4511
 
4512
+ const parsePerseusAnswerArea = pipeParsers(defaulted(object({}), () => ({}))).then(convert(toAnswerArea)).parser;
4513
+
4514
+ // Some answerAreas have extra, bogus fields, like:
4515
+ //
4516
+ // "answerArea": {
4517
+ // "type": "multiple",
4518
+ // "options": {},
4519
+ // "version": null,
4520
+ // "static": false,
4521
+ // "graded": false,
4522
+ // "alignment": "",
4523
+ // }
4524
+ //
4525
+ // This function filters the fields of an answerArea object, keeping only the
4526
+ // known ones, and converts `undefined` and `null` values to `false`.
4527
+ function toAnswerArea(raw) {
4528
+ return {
4529
+ zTable: !!raw.zTable,
4530
+ calculator: !!raw.calculator,
4531
+ chi2Table: !!raw.chi2Table,
4532
+ financialCalculatorMonthlyPayment: !!raw.financialCalculatorMonthlyPayment,
4533
+ financialCalculatorTotalAmount: !!raw.financialCalculatorTotalAmount,
4534
+ financialCalculatorTimeToPayOff: !!raw.financialCalculatorTimeToPayOff,
4535
+ periodicTable: !!raw.periodicTable,
4536
+ periodicTableWithKey: !!raw.periodicTableWithKey,
4537
+ tTable: !!raw.tTable
4538
+ };
4539
+ }
4540
+
4503
4541
  const parsePerseusItem$1 = object({
4504
4542
  question: parsePerseusRenderer,
4505
4543
  hints: defaulted(array(parseHint), () => []),
4506
- answerArea: pipeParsers(defaulted(object({}), () => ({}))).then(migrateAnswerArea).then(record(enumeration(...ItemExtras), boolean)).parser,
4544
+ answerArea: parsePerseusAnswerArea,
4507
4545
  itemDataVersion: optional(object({
4508
4546
  major: number,
4509
4547
  minor: number
@@ -4512,28 +4550,6 @@ const parsePerseusItem$1 = object({
4512
4550
  answer: any
4513
4551
  });
4514
4552
 
4515
- // Some answerAreas have extra fields, like:
4516
- //
4517
- // "answerArea": {
4518
- // "type": "multiple",
4519
- // "options": {
4520
- // "content": "",
4521
- // "images": {},
4522
- // "widgets": {}
4523
- // }
4524
- // }
4525
- //
4526
- // The "type" and "options" fields don't seem to be used anywhere. This
4527
- // migration function removes them.
4528
- function migrateAnswerArea(rawValue, ctx) {
4529
- const {
4530
- type: _,
4531
- options: __,
4532
- ...rest
4533
- } = rawValue;
4534
- return ctx.success(rest);
4535
- }
4536
-
4537
4553
  /**
4538
4554
  * Helper to parse PerseusItem JSON
4539
4555
  * Why not just use JSON.parse? We want:
@@ -4608,7 +4624,7 @@ function throwErrorIfCheatingDetected() {
4608
4624
 
4609
4625
  // This file is processed by a Rollup plugin (replace) to inject the production
4610
4626
  const libName = "@khanacademy/perseus-core";
4611
- const libVersion = "3.6.0";
4627
+ const libVersion = "3.7.0";
4612
4628
  addLibraryVersionToPerseusDebug(libName, libVersion);
4613
4629
 
4614
4630
  /**
@@ -4930,10 +4946,7 @@ const grapherWidgetLogic = {
4930
4946
  const defaultWidgetOptions$m = {
4931
4947
  content: "",
4932
4948
  widgets: {},
4933
- images: {},
4934
- // `undefined` instead of `null` so that getDefaultProps works for
4935
- // `the GroupMetadataEditor`
4936
- metadata: undefined
4949
+ images: {}
4937
4950
  };
4938
4951
  const groupWidgetLogic = {
4939
4952
  name: "group",
@@ -5079,6 +5092,64 @@ const labelImageWidgetLogic = {
5079
5092
  getPublicWidgetOptions: getLabelImagePublicWidgetOptions
5080
5093
  };
5081
5094
 
5095
+ // TODO(LEMS-2841): Should be able to remove once getPublicWidgetOptions is hooked up
5096
+
5097
+ // TODO(LEMS-2841): Should be able to remove once getPublicWidgetOptions is hooked up
5098
+ const shuffleMatcher = props => {
5099
+ // Use the same random() function to shuffle both columns sequentially
5100
+ const rng = perseusCore.seededRNG(props.problemNum);
5101
+ let left;
5102
+ if (!props.orderMatters) {
5103
+ // If the order doesn't matter, don't shuffle the left column
5104
+ left = props.left;
5105
+ } else {
5106
+ left = perseusCore.shuffle(props.left, rng, /* ensurePermuted */true);
5107
+ }
5108
+ const right = perseusCore.shuffle(props.right, rng, /* ensurePermuted */true);
5109
+ return {
5110
+ left,
5111
+ right
5112
+ };
5113
+ };
5114
+
5115
+ // TODO(LEMS-2841): Can shorten to shuffleMatcher after above function removed
5116
+ function shuffleMatcherWithRandom(data) {
5117
+ // Use the same random() function to shuffle both columns sequentially
5118
+ let left;
5119
+ if (!data.orderMatters) {
5120
+ // If the order doesn't matter, don't shuffle the left column
5121
+ left = data.left;
5122
+ } else {
5123
+ left = perseusCore.shuffle(data.left, Math.random, /* ensurePermuted */true);
5124
+ }
5125
+ const right = perseusCore.shuffle(data.right, Math.random, /* ensurePermuted */true);
5126
+ return {
5127
+ left,
5128
+ right
5129
+ };
5130
+ }
5131
+
5132
+ /**
5133
+ * For details on the individual options, see the
5134
+ * PerseusMatcherWidgetOptions type
5135
+ */
5136
+
5137
+ /**
5138
+ * Given a PerseusMatcherWidgetOptions object, return a new object with only
5139
+ * the public options that should be exposed to the client.
5140
+ */
5141
+ function getMatcherPublicWidgetOptions(options) {
5142
+ const {
5143
+ left,
5144
+ right
5145
+ } = shuffleMatcherWithRandom(options);
5146
+ return {
5147
+ ...options,
5148
+ left: left,
5149
+ right: right
5150
+ };
5151
+ }
5152
+
5082
5153
  const defaultWidgetOptions$f = {
5083
5154
  left: ["$x$", "$y$", "$z$"],
5084
5155
  right: ["$1$", "$2$", "$3$"],
@@ -5088,7 +5159,8 @@ const defaultWidgetOptions$f = {
5088
5159
  };
5089
5160
  const matcherWidgetLogic = {
5090
5161
  name: "matcher",
5091
- defaultWidgetOptions: defaultWidgetOptions$f
5162
+ defaultWidgetOptions: defaultWidgetOptions$f,
5163
+ getPublicWidgetOptions: getMatcherPublicWidgetOptions
5092
5164
  };
5093
5165
 
5094
5166
  function getMatrixPublicWidgetOptions(options) {
@@ -5430,13 +5502,17 @@ const radioWidgetLogic = {
5430
5502
  * the public options that should be exposed to the client.
5431
5503
  */
5432
5504
  function getSorterPublicWidgetOptions(options) {
5505
+ const shuffledCorrect = perseusCore.shuffle(options.correct, Math.random, /* ensurePermuted */true);
5433
5506
  return {
5507
+ ...options,
5434
5508
  // Note(Tamara): This does not provide correct answer information any longer.
5435
5509
  // To maintain compatibility with the original widget options, we are
5436
5510
  // keeping the key the same. Represents initial state of the cards here.
5437
- correct: options.correct.slice().sort(),
5438
- padding: options.padding,
5439
- layout: options.layout
5511
+ correct: shuffledCorrect,
5512
+ // Note(Tamara): This new key is only added here with "true". There isn't
5513
+ // a place where it is set to false. It indicates that the correct field
5514
+ // has been shuffled and no longer contains correct answer info.
5515
+ isCorrectShuffled: true
5440
5516
  };
5441
5517
  }
5442
5518
 
@@ -5743,6 +5819,60 @@ function splitPerseusItem(originalItem) {
5743
5819
  };
5744
5820
  }
5745
5821
 
5822
+ /* Note(tamara): Brought over from the perseus package packages/perseus/src/util.ts file.
5823
+ May be useful to bring other perseus package utilities here. Contains utility functions
5824
+ and types used across multiple widgets for randomization and shuffling. */
5825
+ const seededRNG = function (seed) {
5826
+ let randomSeed = seed;
5827
+ return function () {
5828
+ // Robert Jenkins' 32 bit integer hash function.
5829
+ let seed = randomSeed;
5830
+ seed = seed + 0x7ed55d16 + (seed << 12) & 0xffffffff;
5831
+ seed = (seed ^ 0xc761c23c ^ seed >>> 19) & 0xffffffff;
5832
+ seed = seed + 0x165667b1 + (seed << 5) & 0xffffffff;
5833
+ seed = (seed + 0xd3a2646c ^ seed << 9) & 0xffffffff;
5834
+ seed = seed + 0xfd7046c5 + (seed << 3) & 0xffffffff;
5835
+ seed = (seed ^ 0xb55a4f09 ^ seed >>> 16) & 0xffffffff;
5836
+ return (randomSeed = seed & 0xfffffff) / 0x10000000;
5837
+ };
5838
+ };
5839
+
5840
+ // Shuffle an array using a given random seed or function.
5841
+ // If `ensurePermuted` is true, the input and output are guaranteed to be
5842
+ // distinct permutations.
5843
+ function shuffle(array, randomSeed) {
5844
+ let ensurePermuted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
5845
+ // Always return a copy of the input array
5846
+ const shuffled = _.clone(array);
5847
+
5848
+ // Handle edge cases (input array is empty or uniform)
5849
+ if (!shuffled.length || _.all(shuffled, function (value) {
5850
+ return _.isEqual(value, shuffled[0]);
5851
+ })) {
5852
+ return shuffled;
5853
+ }
5854
+ let random;
5855
+ if (typeof randomSeed === "function") {
5856
+ random = randomSeed;
5857
+ } else {
5858
+ random = seededRNG(randomSeed);
5859
+ }
5860
+ do {
5861
+ // Fischer-Yates shuffle
5862
+ for (let top = shuffled.length; top > 0; top--) {
5863
+ const newEnd = Math.floor(random() * top);
5864
+ const temp = shuffled[newEnd];
5865
+
5866
+ // @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
5867
+ shuffled[newEnd] = shuffled[top - 1];
5868
+ // @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
5869
+ shuffled[top - 1] = temp;
5870
+ }
5871
+ } while (ensurePermuted && _.isEqual(array, shuffled));
5872
+ return shuffled;
5873
+ }
5874
+ const random = seededRNG(new Date().getTime() & 0xffffffff);
5875
+
5746
5876
  exports.CoreWidgetRegistry = coreWidgetRegistry;
5747
5877
  exports.Errors = Errors;
5748
5878
  exports.GrapherUtil = grapherUtil;
@@ -5769,6 +5899,7 @@ exports.getGrapherPublicWidgetOptions = getGrapherPublicWidgetOptions;
5769
5899
  exports.getIFramePublicWidgetOptions = getIFramePublicWidgetOptions;
5770
5900
  exports.getInteractiveGraphPublicWidgetOptions = getInteractiveGraphPublicWidgetOptions;
5771
5901
  exports.getLabelImagePublicWidgetOptions = getLabelImagePublicWidgetOptions;
5902
+ exports.getMatcherPublicWidgetOptions = getMatcherPublicWidgetOptions;
5772
5903
  exports.getMatrixPublicWidgetOptions = getMatrixPublicWidgetOptions;
5773
5904
  exports.getMatrixSize = getMatrixSize;
5774
5905
  exports.getNumberLinePublicWidgetOptions = getNumberLinePublicWidgetOptions;
@@ -5816,6 +5947,10 @@ exports.plotterPlotTypes = plotterPlotTypes;
5816
5947
  exports.pluck = pluck;
5817
5948
  exports.pythonProgramLogic = pythonProgramWidgetLogic;
5818
5949
  exports.radioLogic = radioWidgetLogic;
5950
+ exports.random = random;
5951
+ exports.seededRNG = seededRNG;
5952
+ exports.shuffle = shuffle;
5953
+ exports.shuffleMatcher = shuffleMatcher;
5819
5954
  exports.sorterLogic = sorterWidgetLogic;
5820
5955
  exports.splitPerseusItem = splitPerseusItem;
5821
5956
  exports.tableLogic = tableWidgetLogic;