@khanacademy/perseus-core 3.2.0 → 3.4.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 (135) hide show
  1. package/dist/data-schema.d.ts +57 -12
  2. package/dist/es/index.js +2968 -280
  3. package/dist/es/index.js.map +1 -1
  4. package/dist/index.d.ts +80 -1
  5. package/dist/index.js +3027 -281
  6. package/dist/index.js.map +1 -1
  7. package/dist/parse-perseus-json/error-tracking-parse-context.d.ts +9 -0
  8. package/dist/parse-perseus-json/exhaustive-test-tool/index.d.ts +2 -0
  9. package/dist/parse-perseus-json/general-purpose-parsers/any.d.ts +2 -0
  10. package/dist/parse-perseus-json/general-purpose-parsers/array.d.ts +2 -0
  11. package/dist/parse-perseus-json/general-purpose-parsers/array.typetest.d.ts +1 -0
  12. package/dist/parse-perseus-json/general-purpose-parsers/boolean.d.ts +2 -0
  13. package/dist/parse-perseus-json/general-purpose-parsers/constant.d.ts +4 -0
  14. package/dist/parse-perseus-json/general-purpose-parsers/convert.d.ts +2 -0
  15. package/dist/parse-perseus-json/general-purpose-parsers/defaulted.d.ts +2 -0
  16. package/dist/parse-perseus-json/general-purpose-parsers/discriminated-union.d.ts +21 -0
  17. package/dist/parse-perseus-json/general-purpose-parsers/discriminated-union.typetest.d.ts +1 -0
  18. package/dist/parse-perseus-json/general-purpose-parsers/enumeration.d.ts +2 -0
  19. package/dist/parse-perseus-json/general-purpose-parsers/enumeration.typetest.d.ts +1 -0
  20. package/dist/parse-perseus-json/general-purpose-parsers/index.d.ts +17 -0
  21. package/dist/parse-perseus-json/general-purpose-parsers/is-object.d.ts +1 -0
  22. package/dist/parse-perseus-json/general-purpose-parsers/nullable.d.ts +2 -0
  23. package/dist/parse-perseus-json/general-purpose-parsers/number.d.ts +2 -0
  24. package/dist/parse-perseus-json/general-purpose-parsers/object.d.ts +6 -0
  25. package/dist/parse-perseus-json/general-purpose-parsers/optional.d.ts +2 -0
  26. package/dist/parse-perseus-json/general-purpose-parsers/pair.d.ts +2 -0
  27. package/dist/parse-perseus-json/general-purpose-parsers/pipe-parsers.d.ts +7 -0
  28. package/dist/parse-perseus-json/general-purpose-parsers/pipe-parsers.typetest.d.ts +1 -0
  29. package/dist/parse-perseus-json/general-purpose-parsers/record.d.ts +2 -0
  30. package/dist/parse-perseus-json/general-purpose-parsers/string-to-number.d.ts +2 -0
  31. package/dist/parse-perseus-json/general-purpose-parsers/string.d.ts +2 -0
  32. package/dist/parse-perseus-json/general-purpose-parsers/test-helpers.d.ts +8 -0
  33. package/dist/parse-perseus-json/general-purpose-parsers/trio.d.ts +2 -0
  34. package/dist/parse-perseus-json/general-purpose-parsers/union.d.ts +7 -0
  35. package/dist/parse-perseus-json/general-purpose-parsers/union.typetest.d.ts +1 -0
  36. package/dist/parse-perseus-json/general-purpose-parsers/unknown.d.ts +2 -0
  37. package/dist/parse-perseus-json/index.d.ts +45 -0
  38. package/dist/parse-perseus-json/object-path.d.ts +2 -0
  39. package/dist/parse-perseus-json/parse-failure-detail.d.ts +2 -0
  40. package/dist/parse-perseus-json/parse.d.ts +3 -0
  41. package/dist/parse-perseus-json/parser-types.d.ts +30 -0
  42. package/dist/parse-perseus-json/perseus-parsers/categorizer-widget.d.ts +3 -0
  43. package/dist/parse-perseus-json/perseus-parsers/cs-program-widget.d.ts +3 -0
  44. package/dist/parse-perseus-json/perseus-parsers/definition-widget.d.ts +3 -0
  45. package/dist/parse-perseus-json/perseus-parsers/dropdown-widget.d.ts +3 -0
  46. package/dist/parse-perseus-json/perseus-parsers/explanation-widget.d.ts +3 -0
  47. package/dist/parse-perseus-json/perseus-parsers/expression-widget.d.ts +3 -0
  48. package/dist/parse-perseus-json/perseus-parsers/graded-group-set-widget.d.ts +3 -0
  49. package/dist/parse-perseus-json/perseus-parsers/graded-group-widget.d.ts +16 -0
  50. package/dist/parse-perseus-json/perseus-parsers/grapher-widget.d.ts +3 -0
  51. package/dist/parse-perseus-json/perseus-parsers/group-widget.d.ts +3 -0
  52. package/dist/parse-perseus-json/perseus-parsers/hint.d.ts +3 -0
  53. package/dist/parse-perseus-json/perseus-parsers/iframe-widget.d.ts +3 -0
  54. package/dist/parse-perseus-json/perseus-parsers/image-widget.d.ts +3 -0
  55. package/dist/parse-perseus-json/perseus-parsers/images-map.d.ts +5 -0
  56. package/dist/parse-perseus-json/perseus-parsers/input-number-widget.d.ts +3 -0
  57. package/dist/parse-perseus-json/perseus-parsers/interaction-widget.d.ts +3 -0
  58. package/dist/parse-perseus-json/perseus-parsers/interactive-graph-widget.d.ts +3 -0
  59. package/dist/parse-perseus-json/perseus-parsers/label-image-widget.d.ts +3 -0
  60. package/dist/parse-perseus-json/perseus-parsers/matcher-widget.d.ts +3 -0
  61. package/dist/parse-perseus-json/perseus-parsers/matrix-widget.d.ts +3 -0
  62. package/dist/parse-perseus-json/perseus-parsers/measurer-widget.d.ts +3 -0
  63. package/dist/parse-perseus-json/perseus-parsers/molecule-renderer-widget.d.ts +3 -0
  64. package/dist/parse-perseus-json/perseus-parsers/number-line-widget.d.ts +3 -0
  65. package/dist/parse-perseus-json/perseus-parsers/numeric-input-widget.d.ts +3 -0
  66. package/dist/parse-perseus-json/perseus-parsers/orderer-widget.d.ts +3 -0
  67. package/dist/parse-perseus-json/perseus-parsers/passage-ref-widget.d.ts +3 -0
  68. package/dist/parse-perseus-json/perseus-parsers/passage-widget.d.ts +3 -0
  69. package/dist/parse-perseus-json/perseus-parsers/perseus-article.d.ts +3 -0
  70. package/dist/parse-perseus-json/perseus-parsers/perseus-image-background.d.ts +3 -0
  71. package/dist/parse-perseus-json/perseus-parsers/perseus-item.d.ts +3 -0
  72. package/dist/parse-perseus-json/perseus-parsers/perseus-renderer.d.ts +3 -0
  73. package/dist/parse-perseus-json/perseus-parsers/phet-simulation-widget.d.ts +3 -0
  74. package/dist/parse-perseus-json/perseus-parsers/plotter-widget.d.ts +3 -0
  75. package/dist/parse-perseus-json/perseus-parsers/python-program-widget.d.ts +3 -0
  76. package/dist/parse-perseus-json/perseus-parsers/radio-widget.d.ts +3 -0
  77. package/dist/parse-perseus-json/perseus-parsers/sorter-widget.d.ts +3 -0
  78. package/dist/parse-perseus-json/perseus-parsers/table-widget.d.ts +3 -0
  79. package/dist/parse-perseus-json/perseus-parsers/versioned-widget-options.d.ts +36 -0
  80. package/dist/parse-perseus-json/perseus-parsers/video-widget.d.ts +3 -0
  81. package/dist/parse-perseus-json/perseus-parsers/widget.d.ts +7 -0
  82. package/dist/parse-perseus-json/perseus-parsers/widgets-map.d.ts +3 -0
  83. package/dist/parse-perseus-json/result.d.ts +16 -0
  84. package/dist/types.d.ts +1 -0
  85. package/dist/utils/is-real-json-parse.d.ts +1 -0
  86. package/dist/utils/widget-id-utils.d.ts +38 -0
  87. package/dist/widgets/categorizer/categorizer-util.d.ts +17 -0
  88. package/dist/widgets/categorizer/index.d.ts +5 -0
  89. package/dist/widgets/core-widget-registry.d.ts +28 -0
  90. package/dist/widgets/cs-program/cs-program-util.d.ts +2 -0
  91. package/dist/widgets/cs-program/index.d.ts +5 -0
  92. package/dist/widgets/definition/index.d.ts +5 -0
  93. package/dist/widgets/dropdown/dropdown-util.d.ts +20 -0
  94. package/dist/widgets/dropdown/index.d.ts +5 -0
  95. package/dist/widgets/explanation/index.d.ts +5 -0
  96. package/dist/widgets/expression/expression-upgrade.d.ts +10 -0
  97. package/dist/widgets/expression/expression-util.d.ts +19 -0
  98. package/dist/widgets/expression/index.d.ts +4 -0
  99. package/dist/widgets/graded-group/index.d.ts +5 -0
  100. package/dist/widgets/graded-group-set/index.d.ts +5 -0
  101. package/dist/widgets/grapher/index.d.ts +5 -0
  102. package/dist/widgets/group/index.d.ts +5 -0
  103. package/dist/widgets/iframe/index.d.ts +5 -0
  104. package/dist/widgets/image/index.d.ts +5 -0
  105. package/dist/widgets/input-number/index.d.ts +5 -0
  106. package/dist/widgets/interaction/index.d.ts +5 -0
  107. package/dist/widgets/interactive-graph/index.d.ts +5 -0
  108. package/dist/widgets/label-image/index.d.ts +5 -0
  109. package/dist/widgets/label-image/label-image-util.d.ts +19 -0
  110. package/dist/widgets/logic-export.types.d.ts +13 -0
  111. package/dist/widgets/matcher/index.d.ts +5 -0
  112. package/dist/widgets/matrix/index.d.ts +5 -0
  113. package/dist/widgets/measurer/index.d.ts +4 -0
  114. package/dist/widgets/measurer/measurer-upgrade.d.ts +10 -0
  115. package/dist/widgets/number-line/index.d.ts +5 -0
  116. package/dist/widgets/number-line/number-line-util.d.ts +4 -0
  117. package/dist/widgets/numeric-input/index.d.ts +5 -0
  118. package/dist/widgets/numeric-input/numeric-input-util.d.ts +19 -0
  119. package/dist/widgets/orderer/index.d.ts +5 -0
  120. package/dist/widgets/orderer/orderer-util.d.ts +16 -0
  121. package/dist/widgets/passage/index.d.ts +5 -0
  122. package/dist/widgets/passage-ref/index.d.ts +4 -0
  123. package/dist/widgets/passage-ref/passage-ref-upgrade.d.ts +7 -0
  124. package/dist/widgets/passage-ref-target/index.d.ts +5 -0
  125. package/dist/widgets/phet-simulation/index.d.ts +5 -0
  126. package/dist/widgets/plotter/index.d.ts +5 -0
  127. package/dist/widgets/python-program/index.d.ts +5 -0
  128. package/dist/widgets/radio/index.d.ts +4 -0
  129. package/dist/widgets/radio/radio-upgrade.d.ts +10 -0
  130. package/dist/widgets/sorter/index.d.ts +5 -0
  131. package/dist/widgets/sorter/sorter-util.d.ts +16 -0
  132. package/dist/widgets/table/index.d.ts +5 -0
  133. package/dist/widgets/upgrade.d.ts +3 -0
  134. package/dist/widgets/video/index.d.ts +5 -0
  135. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -116,7 +116,7 @@ function restArguments(func, startIndex) {
116
116
  }
117
117
 
118
118
  // Is a given variable an object?
119
- function isObject(obj) {
119
+ function isObject$1(obj) {
120
120
  var type = typeof obj;
121
121
  return type === 'function' || (type === 'object' && !!obj);
122
122
  }
@@ -230,7 +230,7 @@ function isNaN$1(obj) {
230
230
  }
231
231
 
232
232
  // Predicate-generating function. Often useful outside of Underscore.
233
- function constant(value) {
233
+ function constant$1(value) {
234
234
  return function() {
235
235
  return value;
236
236
  };
@@ -267,7 +267,7 @@ function isTypedArray(obj) {
267
267
  isBufferLike(obj) && typedArrayPattern.test(toString.call(obj));
268
268
  }
269
269
 
270
- var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : constant(false);
270
+ var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : constant$1(false);
271
271
 
272
272
  // Internal helper to obtain the `length` property of an object.
273
273
  var getLength = shallowProperty('length');
@@ -312,7 +312,7 @@ function collectNonEnumProps(obj, keys) {
312
312
  // Retrieve the names of an object's own properties.
313
313
  // Delegates to **ECMAScript 5**'s native `Object.keys`.
314
314
  function keys(obj) {
315
- if (!isObject(obj)) return [];
315
+ if (!isObject$1(obj)) return [];
316
316
  if (nativeKeys) return nativeKeys(obj);
317
317
  var keys = [];
318
318
  for (var key in obj) if (has$1(obj, key)) keys.push(key);
@@ -510,7 +510,7 @@ function isEqual(a, b) {
510
510
 
511
511
  // Retrieve all the enumerable property names of an object.
512
512
  function allKeys(obj) {
513
- if (!isObject(obj)) return [];
513
+ if (!isObject$1(obj)) return [];
514
514
  var keys = [];
515
515
  for (var key in obj) keys.push(key);
516
516
  // Ahem, IE < 9.
@@ -639,7 +639,7 @@ function ctor() {
639
639
 
640
640
  // An internal function for creating a new object that inherits from another.
641
641
  function baseCreate(prototype) {
642
- if (!isObject(prototype)) return {};
642
+ if (!isObject$1(prototype)) return {};
643
643
  if (nativeCreate) return nativeCreate(prototype);
644
644
  var Ctor = ctor();
645
645
  Ctor.prototype = prototype;
@@ -659,7 +659,7 @@ function create(prototype, props) {
659
659
 
660
660
  // Create a (shallow-cloned) duplicate of an object.
661
661
  function clone(obj) {
662
- if (!isObject(obj)) return obj;
662
+ if (!isObject$1(obj)) return obj;
663
663
  return isArray(obj) ? obj.slice() : extend({}, obj);
664
664
  }
665
665
 
@@ -768,7 +768,7 @@ function optimizeCb(func, context, argCount) {
768
768
  function baseIteratee(value, context, argCount) {
769
769
  if (value == null) return identity;
770
770
  if (isFunction$1(value)) return optimizeCb(value, context, argCount);
771
- if (isObject(value) && !isArray(value)) return matcher(value);
771
+ if (isObject$1(value) && !isArray(value)) return matcher(value);
772
772
  return property(value);
773
773
  }
774
774
 
@@ -1017,7 +1017,7 @@ function executeBound(sourceFunc, boundFunc, context, callingContext, args) {
1017
1017
  if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
1018
1018
  var self = baseCreate(sourceFunc.prototype);
1019
1019
  var result = sourceFunc.apply(self, args);
1020
- if (isObject(result)) return result;
1020
+ if (isObject$1(result)) return result;
1021
1021
  return self;
1022
1022
  }
1023
1023
 
@@ -1779,7 +1779,7 @@ function uniq(array, isSorted, iteratee, context) {
1779
1779
 
1780
1780
  // Produce an array that contains the union: each distinct element from all of
1781
1781
  // the passed-in arrays.
1782
- var union = restArguments(function(arrays) {
1782
+ var union$1 = restArguments(function(arrays) {
1783
1783
  return uniq(flatten$1(arrays, true, true));
1784
1784
  });
1785
1785
 
@@ -1819,7 +1819,7 @@ var zip = restArguments(unzip);
1819
1819
  // Converts lists into objects. Pass either a single array of `[key, value]`
1820
1820
  // pairs, or two parallel arrays of the same length -- one of keys, and one of
1821
1821
  // the corresponding values. Passing by pairs is the reverse of `_.pairs`.
1822
- function object(list, values) {
1822
+ function object$1(list, values) {
1823
1823
  var result = {};
1824
1824
  for (var i = 0, length = getLength(list); i < length; i++) {
1825
1825
  if (values) {
@@ -1914,7 +1914,7 @@ var allExports = /*#__PURE__*/Object.freeze({
1914
1914
  __proto__: null,
1915
1915
  VERSION: VERSION,
1916
1916
  restArguments: restArguments,
1917
- isObject: isObject,
1917
+ isObject: isObject$1,
1918
1918
  isNull: isNull,
1919
1919
  isUndefined: isUndefined,
1920
1920
  isBoolean: isBoolean,
@@ -1958,7 +1958,7 @@ var allExports = /*#__PURE__*/Object.freeze({
1958
1958
  has: has,
1959
1959
  mapObject: mapObject$1,
1960
1960
  identity: identity,
1961
- constant: constant,
1961
+ constant: constant$1,
1962
1962
  noop: noop,
1963
1963
  toPath: toPath$1,
1964
1964
  property: property,
@@ -2047,13 +2047,13 @@ var allExports = /*#__PURE__*/Object.freeze({
2047
2047
  without: without,
2048
2048
  uniq: uniq,
2049
2049
  unique: uniq,
2050
- union: union,
2050
+ union: union$1,
2051
2051
  intersection: intersection,
2052
2052
  difference: difference,
2053
2053
  unzip: unzip,
2054
2054
  transpose: unzip,
2055
2055
  zip: zip,
2056
- object: object,
2056
+ object: object$1,
2057
2057
  range: range,
2058
2058
  chunk: chunk,
2059
2059
  mixin: mixin,
@@ -2166,6 +2166,70 @@ function approximateDeepEqual(x, y) {
2166
2166
  return approximateEqual(x, y);
2167
2167
  }
2168
2168
 
2169
+ /**
2170
+ * Add a widget placeholder using the widget ID.
2171
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
2172
+ *
2173
+ * @param {string} id
2174
+ * @returns {string}
2175
+ */
2176
+ function addWidget(id) {
2177
+ return `[[☃ ${id}]]`;
2178
+ }
2179
+
2180
+ /**
2181
+ * Regex for widget placeholders in a string.
2182
+ *
2183
+ * First capture group is the widget ID (ex. 'radio 1')
2184
+ * Second capture group is the widget type (ex. "radio)
2185
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
2186
+ */
2187
+ function getWidgetRegex() {
2188
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
2189
+ }
2190
+
2191
+ /**
2192
+ * Extract all widget IDs, which includes the widget type and instance number.
2193
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
2194
+ *
2195
+ * Content should contain Perseus widget placeholders,
2196
+ * which look like: '[[☃ radio 1]]'.
2197
+ *
2198
+ * @param {string} content
2199
+ * @returns {ReadonlyArray<string>} widgetIds
2200
+ */
2201
+ function getWidgetIdsFromContent(content) {
2202
+ const widgets = [];
2203
+ const localWidgetRegex = getWidgetRegex();
2204
+ let match = localWidgetRegex.exec(content);
2205
+ while (match !== null) {
2206
+ widgets.push(match[1]);
2207
+ match = localWidgetRegex.exec(content);
2208
+ }
2209
+ return widgets;
2210
+ }
2211
+
2212
+ /**
2213
+ * Get a list of widget IDs from content,
2214
+ * but only for specific widget types
2215
+ *
2216
+ * @param {string} type the type of widget (ie "radio")
2217
+ * @param {string} content the string to parse
2218
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
2219
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
2220
+ */
2221
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
2222
+ const rv = [];
2223
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
2224
+ widgetIdsInContent.forEach(widgetId => {
2225
+ const widget = widgetMap[widgetId];
2226
+ if (widget?.type === type) {
2227
+ rv.push(widgetId);
2228
+ }
2229
+ });
2230
+ return rv;
2231
+ }
2232
+
2169
2233
  // TODO(benchristel): in the future, we may want to make deepClone work for
2170
2234
  // Record<string, Cloneable> as well. Currently, it only does arrays.
2171
2235
 
@@ -2580,358 +2644,3040 @@ var grapherUtil = /*#__PURE__*/Object.freeze({
2580
2644
  functionForType: functionForType
2581
2645
  });
2582
2646
 
2583
- // This file is processed by a Rollup plugin (replace) to inject the production
2584
- const libName = "@khanacademy/perseus-core";
2585
- const libVersion = "3.2.0";
2586
- addLibraryVersionToPerseusDebug(libName, libVersion);
2647
+ function isRealJSONParse(jsonParse) {
2648
+ const randomPhrase = buildRandomPhrase();
2649
+ const randomHintPhrase = buildRandomPhrase();
2650
+ const randomString = buildRandomString();
2651
+ const testingObject = JSON.stringify({
2652
+ answerArea: {
2653
+ calculator: false,
2654
+ chi2Table: false,
2655
+ financialCalculatorMonthlyPayment: false,
2656
+ financialCalculatorTimeToPayOff: false,
2657
+ financialCalculatorTotalAmount: false,
2658
+ periodicTable: false,
2659
+ periodicTableWithKey: false,
2660
+ tTable: false,
2661
+ zTable: false
2662
+ },
2663
+ hints: [randomHintPhrase, `=${Math.floor(Math.random() * 50) + 1}`],
2664
+ itemDataVersion: {
2665
+ major: 0,
2666
+ minor: 1
2667
+ },
2668
+ question: {
2669
+ content: `${randomPhrase}`,
2670
+ images: {},
2671
+ widgets: {
2672
+ expression1: {
2673
+ alignment: "default",
2674
+ graded: false,
2675
+ options: {
2676
+ answerForms: [{
2677
+ considered: "wrong",
2678
+ form: false,
2679
+ key: 0,
2680
+ simplify: false,
2681
+ value: `${randomString}`
2682
+ }],
2683
+ ariaLabel: "Answer",
2684
+ buttonSets: ["basic"],
2685
+ functions: ["f", "g", "h"],
2686
+ static: true,
2687
+ times: false,
2688
+ visibleLabel: "Answer"
2689
+ },
2690
+ static: true,
2691
+ type: "expression",
2692
+ version: {
2693
+ major: 1,
2694
+ minor: 0
2695
+ }
2696
+ }
2697
+ }
2698
+ }
2699
+ });
2700
+ const testJSON = buildTestData(testingObject.replace(/"/g, '\\"'));
2701
+ const parsedTestJSON = jsonParse(testJSON);
2702
+ const parsedTestItemData = parsedTestJSON.data.assessmentItem.item.itemData;
2703
+ return approximateDeepEqual(parsedTestItemData, testingObject);
2704
+ }
2705
+ function buildRandomString() {
2706
+ let capitalize = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
2707
+ let randomString = "";
2708
+ const randomLength = Math.floor(Math.random() * 8) + 3;
2709
+ for (let i = 0; i < randomLength; i++) {
2710
+ const randomLetter = String.fromCharCode(97 + Math.floor(Math.random() * 26));
2711
+ randomString += capitalize && i === 0 ? randomLetter.toUpperCase() : randomLetter;
2712
+ }
2713
+ return randomString;
2714
+ }
2715
+ function buildRandomPhrase() {
2716
+ const phrases = [];
2717
+ const randomLength = Math.floor(Math.random() * 10) + 5;
2718
+ for (let i = 0; i < randomLength; i++) {
2719
+ phrases.push(buildRandomString(i === 0));
2720
+ }
2721
+ const modifierStart = ["**", "$"];
2722
+ const modifierEnd = ["**", "$"];
2723
+ const modifierIndex = Math.floor(Math.random() * modifierStart.length);
2724
+ return `${modifierStart[modifierIndex]}${phrases.join(" ")}${modifierEnd[modifierIndex]}`;
2725
+ }
2726
+ function buildTestData(testObject) {
2727
+ return `{"data":{"assessmentItem":{"__typename":"AssessmentItemOrError","error":null,"item":{"__typename":"AssessmentItem","id":"x890b3c70f3e8f4a6","itemData":"${testObject}","problemType":"Type 1","sha":"c7284a3ad65214b4e62bccce236d92f7f5d35941"}}}}`;
2728
+ }
2587
2729
 
2588
- /**
2589
- * @typedef {Object} Errors utility for referencing the Perseus error taxonomy.
2590
- */
2591
- const Errors = Object.freeze({
2592
- /**
2593
- * @property {ErrorKind} Unknown The kind of error is not known.
2594
- */
2595
- Unknown: "Unknown",
2596
- /**
2597
- * @property {ErrorKind} Internal The error is internal to the executing code.
2598
- */
2599
- Internal: "Internal",
2600
- /**
2601
- * @property {ErrorKind} InvalidInput There was a problem with the provided
2602
- * input, such as the wrong format or a null value.
2603
- */
2604
- InvalidInput: "InvalidInput",
2605
- /**
2606
- * @property {ErrorKind} NotAllowed There was a problem due to the state of
2607
- * the system not matching the requested operation or input. For example,
2608
- * trying to create a username that is valid, but is already taken by
2609
- * another user. Use {@link InvalidInput} instead when the input isn't
2610
- * valid regardless of the state of the system. Use {@link NotFound} when
2611
- * the failure is due to not being able to find a resource.
2612
- */
2613
- NotAllowed: "NotAllowed",
2614
- /**
2615
- * @property {ErrorKind} TransientService There was a problem when making a
2616
- * request to a service.
2617
- */
2618
- TransientService: "TransientService",
2619
- /**
2620
- * @property {ErrorKind} Service There was a non-transient problem when
2621
- * making a request to service.
2622
- */
2623
- Service: "Service"
2624
- });
2730
+ process.env.NODE_ENV === 'production';
2625
2731
 
2626
- /**
2627
- * @type {ErrorKind} The kind of error being reported
2628
- */
2732
+ function success(value) {
2733
+ return {
2734
+ type: "success",
2735
+ value
2736
+ };
2737
+ }
2738
+ function failure(detail) {
2739
+ return {
2740
+ type: "failure",
2741
+ detail
2742
+ };
2743
+ }
2744
+ function isFailure(result) {
2745
+ return result.type === "failure";
2746
+ }
2747
+ function isSuccess(result) {
2748
+ return result.type === "success";
2749
+ }
2629
2750
 
2630
- class PerseusError extends Error {
2631
- kind;
2632
- metadata;
2633
- constructor(message, kind, options) {
2634
- super(message);
2635
- this.kind = kind;
2636
- this.metadata = options?.metadata;
2751
+ // Result's `all` function is similar to Promise.all: given an array of
2752
+ // results, it returns success if all succeeded, and failure if any failed.
2753
+ function all(results) {
2754
+ let combineFailureDetails = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : a => a;
2755
+ const values = [];
2756
+ const failureDetails = [];
2757
+ for (const result of results) {
2758
+ if (result.type === "success") {
2759
+ values.push(result.value);
2760
+ } else {
2761
+ failureDetails.push(result.detail);
2762
+ }
2763
+ }
2764
+ if (failureDetails.length > 0) {
2765
+ return failure(failureDetails.reduce(combineFailureDetails));
2637
2766
  }
2767
+ return success(values);
2638
2768
  }
2639
2769
 
2640
- /**
2641
- * The Perseus "data schema" file.
2642
- *
2643
- * This file, and the types in it, represents the "data schema" that Perseus
2644
- * uses. The @khanacademy/perseus-editor package edits and produces objects
2645
- * that conform to the types in this file. Similarly, the top-level renderers
2646
- * in @khanacademy/perseus, consume objects that conform to these types.
2647
- *
2648
- * WARNING: This file should not import any types from elsewhere so that it is
2649
- * easy to reason about changes that alter the Perseus schema. This helps
2650
- * ensure that it is not changed accidentally when upgrading a dependant
2651
- * package or other part of Perseus code. Note that TypeScript does type
2652
- * checking via something called "structural typing". This means that as long
2653
- * as the shape of a type matches, the name it goes by doesn't matter. As a
2654
- * result, a `Coord` type that looks like this `[x: number, y: number]` is
2655
- * _identical_, in TypeScript's eyes, to this `Vector2` type `[x: number, y:
2656
- * number]`. Also, with tuples, the labels for each entry is ignored, so `[x:
2657
- * number, y: number]` is compatible with `[min: number, max: number]`. The
2658
- * labels are for humans, not TypeScript. :)
2659
- *
2660
- * If you make changes to types in this file, be very sure that:
2661
- *
2662
- * a) the changes are backwards compatible. If they are not, old data from
2663
- * previous versions of the "schema" could become unrenderable, or worse,
2664
- * introduce hard-to-diagnose bugs.
2665
- * b) the parsing code (`util/parse-perseus-json/`) is updated to handle
2666
- * the new format _as well as_ the old format.
2667
- */
2668
-
2669
- // TODO(FEI-4010): Remove `Perseus` prefix for all types here
2670
-
2671
- // Same name as Mafs
2770
+ class ErrorTrackingParseContext {
2771
+ constructor(path) {
2772
+ this.path = path;
2773
+ }
2774
+ failure(expected, badValue) {
2775
+ return failure([{
2776
+ expected: wrapInArray(expected),
2777
+ badValue,
2778
+ path: this.path
2779
+ }]);
2780
+ }
2781
+ forSubtree(key) {
2782
+ return new ErrorTrackingParseContext([...this.path, key]);
2783
+ }
2784
+ success(value) {
2785
+ return success(value);
2786
+ }
2787
+ }
2788
+ function wrapInArray(a) {
2789
+ return Array.isArray(a) ? a : [a];
2790
+ }
2672
2791
 
2673
- /**
2674
- * Our core set of Perseus widgets.
2675
- *
2676
- * This interface is the basis for "registering" all Perseus widget types.
2677
- * There should be one key/value pair for each supported widget. If you create
2678
- * a new widget, an entry should be added to this interface. Note that this
2679
- * only registers the widget options type, you'll also need to register the
2680
- * widget so that it's available at runtime (@see
2681
- * {@link file://./widgets.ts#registerWidget}).
2682
- *
2683
- * Importantly, the key should be the name that is used in widget IDs. For most
2684
- * widgets that is the same as the widget option's `type` field. In cases where
2685
- * a widget has been deprecated and replaced with the deprecated-standin
2686
- * widget, it should be the original widget type!
2687
- *
2688
- * If you define the widget outside of this package, you can still add the new
2689
- * widget to this interface by writing the following in that package that
2690
- * contains the widget. TypeScript will merge that definition of the
2691
- * `PerseusWidgets` with the one defined below.
2692
- *
2693
- * ```typescript
2694
- * declare module "@khanacademy/perseus" {
2695
- * interface PerseusWidgetTypes {
2696
- * // A new widget
2697
- * "new-awesomeness": MyAwesomeNewWidget;
2698
- *
2699
- * // A deprecated widget
2700
- * "super-old-widget": DeprecatedStandinWidget;
2701
- * }
2702
- * }
2703
- *
2704
- * // The new widget's options definition
2705
- * type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>;
2706
- *
2707
- * // The deprecated widget's options definition
2708
- * type SuperOldWidget = WidgetOptions<'super-old-widget', object>;
2709
- * ```
2710
- *
2711
- * This interface can be extended through the magic of TypeScript "Declaration
2712
- * merging". Specifically, we augment this module and extend this interface.
2713
- *
2714
- * @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
2715
- */
2792
+ function formatPath(path) {
2793
+ return "(root)" + path.map(formatPathSegment).join("");
2794
+ }
2795
+ function formatPathSegment(segment) {
2796
+ if (typeof segment === "string") {
2797
+ return validIdentifier.test(segment) ? "." + segment : `[${JSON.stringify(segment)}]`;
2798
+ }
2799
+ return `[${segment.toString()}]`;
2800
+ }
2801
+ const validIdentifier = /^[A-Za-z$_][A-Za-z$_0-9]*$/;
2716
2802
 
2717
- /**
2718
- * A map of widget IDs to widget options. This is most often used as the type
2719
- * for a set of widgets defined in a `PerseusItem` but can also be useful to
2720
- * represent a function parameter where only `widgets` from a `PerseusItem` are
2721
- * needed. Today Widget IDs are made up of the widget type and an incrementing
2722
- * integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid
2723
- * reading/parsing the widget id to derive any information from it, except in
2724
- * the case of this map.
2725
- *
2726
- * @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
2727
- * by augmenting the PerseusWidgetTypes with new widget types!
2728
- */
2803
+ function message(failure) {
2804
+ const expected = conjoin(failure.expected);
2805
+ const path = formatPath(failure.path);
2806
+ const badValue = JSON.stringify(failure.badValue);
2807
+ return `At ${path} -- expected ${expected}, but got ${badValue}`;
2808
+ }
2809
+ function conjoin(items) {
2810
+ switch (items.length) {
2811
+ // TODO(benchristel): handle 0 if this is reused elsewhere.
2812
+ case 1:
2813
+ return items[0];
2814
+ case 2:
2815
+ return items.join(" or ");
2816
+ default:
2817
+ {
2818
+ const allButLast = items.slice(0, items.length - 1);
2819
+ const last = items[items.length - 1];
2820
+ return allButLast.join(", ") + ", or " + last;
2821
+ }
2822
+ }
2823
+ }
2729
2824
 
2730
- /**
2731
- * A "PerseusItem" is a classic Perseus item. It is rendered by the
2732
- * `ServerItemRenderer` and the layout is pre-set.
2733
- *
2734
- * To render more complex Perseus items, see the `Item` type in the multi item
2735
- * area.
2736
- */
2825
+ function parse(value, parser) {
2826
+ const result = parser(value, new ErrorTrackingParseContext([]));
2827
+ if (isFailure(result)) {
2828
+ return failure(result.detail.map(message).join("; "));
2829
+ }
2830
+ return result;
2831
+ }
2737
2832
 
2738
- /**
2739
- * A "PerseusArticle" is an item that is meant to be rendered as an article.
2740
- * This item is never scored and is rendered by the `ArticleRenderer`.
2741
- */
2833
+ const any = (rawValue, ctx) => ctx.success(rawValue);
2742
2834
 
2743
- const ItemExtras = [
2744
- // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
2745
- "calculator",
2746
- // The user might benefit from using a statistics Chi Squared Table like https://people.richland.edu/james/lecture/m170/tbl-chi.html
2747
- "chi2Table",
2748
- // The user might benefit from a monthly payments calculator. Provided on Khan Academy when true
2749
- "financialCalculatorMonthlyPayment",
2750
- // The user might benefit from a total amount calculator. Provided on Khan Academy when true
2751
- "financialCalculatorTotalAmount",
2752
- // The user might benefit from a time to pay off calculator. Provided on Khan Academy when true
2753
- "financialCalculatorTimeToPayOff",
2754
- // The user might benefit from using a Periodic Table of Elements. Provided on Khan Academy when true
2755
- "periodicTable",
2756
- // The user might benefit from using a Periodic Table of Elements with key. Provided on Khan Academy when true
2757
- "periodicTableWithKey",
2758
- // The user might benefit from using a statistics T Table like https://www.statisticshowto.com/tables/t-distribution-table/
2759
- "tTable",
2760
- // The user might benefit from using a statistics Z Table like https://www.ztable.net/
2761
- "zTable"];
2835
+ function array(elementParser) {
2836
+ return (rawValue, ctx) => {
2837
+ if (!Array.isArray(rawValue)) {
2838
+ return ctx.failure("array", rawValue);
2839
+ }
2840
+ const elementResults = rawValue.map((elem, i) => elementParser(elem, ctx.forSubtree(i)));
2841
+ return all(elementResults, concat);
2842
+ };
2843
+ }
2844
+ function concat(a, b) {
2845
+ return [...a, ...b];
2846
+ }
2762
2847
 
2763
- /**
2764
- * The type representing the common structure of all widget's options. The
2765
- * `Options` generic type represents the widget-specific option data.
2766
- */
2848
+ function boolean(rawValue, ctx) {
2849
+ if (typeof rawValue === "boolean") {
2850
+ return ctx.success(rawValue);
2851
+ }
2852
+ return ctx.failure("boolean", rawValue);
2853
+ }
2767
2854
 
2768
- // prettier-ignore
2855
+ function constant(acceptedValue) {
2856
+ return (rawValue, ctx) => {
2857
+ if (rawValue !== acceptedValue) {
2858
+ return ctx.failure(String(JSON.stringify(acceptedValue)), rawValue);
2859
+ }
2860
+ return ctx.success(acceptedValue);
2861
+ };
2862
+ }
2769
2863
 
2770
- // prettier-ignore
2864
+ function enumeration() {
2865
+ for (var _len = arguments.length, acceptedValues = new Array(_len), _key = 0; _key < _len; _key++) {
2866
+ acceptedValues[_key] = arguments[_key];
2867
+ }
2868
+ return (rawValue, ctx) => {
2869
+ if (typeof rawValue === "string") {
2870
+ const index = acceptedValues.indexOf(rawValue);
2871
+ if (index > -1) {
2872
+ return ctx.success(acceptedValues[index]);
2873
+ }
2874
+ }
2875
+ const expected = acceptedValues.map(v => JSON.stringify(v));
2876
+ return ctx.failure(expected, rawValue);
2877
+ };
2878
+ }
2771
2879
 
2772
- // prettier-ignore
2880
+ function isObject(x) {
2881
+ return x != null && Object.getPrototypeOf(x) === Object.prototype;
2882
+ }
2773
2883
 
2774
- // prettier-ignore
2884
+ function nullable(parseValue) {
2885
+ return (rawValue, ctx) => {
2886
+ if (rawValue === null) {
2887
+ return ctx.success(rawValue);
2888
+ }
2889
+ return parseValue(rawValue, ctx);
2890
+ };
2891
+ }
2775
2892
 
2776
- // prettier-ignore
2893
+ const number = (rawValue, ctx) => {
2894
+ if (typeof rawValue === "number") {
2895
+ return ctx.success(rawValue);
2896
+ }
2897
+ return ctx.failure("number", rawValue);
2898
+ };
2899
+
2900
+ function object(schema) {
2901
+ return (rawValue, ctx) => {
2902
+ if (!isObject(rawValue)) {
2903
+ return ctx.failure("object", rawValue);
2904
+ }
2905
+ const ret = {
2906
+ ...rawValue
2907
+ };
2908
+ const mismatches = [];
2909
+ for (const [prop, propParser] of Object.entries(schema)) {
2910
+ const result = propParser(rawValue[prop], ctx.forSubtree(prop));
2911
+ if (isSuccess(result)) {
2912
+ if (result.value !== undefined || prop in rawValue) {
2913
+ ret[prop] = result.value;
2914
+ }
2915
+ } else {
2916
+ mismatches.push(...result.detail);
2917
+ }
2918
+ }
2919
+ if (mismatches.length > 0) {
2920
+ return failure(mismatches);
2921
+ }
2922
+ return ctx.success(ret);
2923
+ };
2924
+ }
2925
+
2926
+ function optional(parseValue) {
2927
+ return (rawValue, ctx) => {
2928
+ if (rawValue === undefined) {
2929
+ return ctx.success(rawValue);
2930
+ }
2931
+ return parseValue(rawValue, ctx);
2932
+ };
2933
+ }
2934
+
2935
+ function pair(parseA, parseB) {
2936
+ return (rawValue, ctx) => {
2937
+ if (!Array.isArray(rawValue)) {
2938
+ return ctx.failure("array", rawValue);
2939
+ }
2940
+ if (rawValue.length !== 2) {
2941
+ return ctx.failure("array of length 2", rawValue);
2942
+ }
2943
+ const [rawA, rawB] = rawValue;
2944
+ const resultA = parseA(rawA, ctx.forSubtree(0));
2945
+ if (isFailure(resultA)) {
2946
+ return resultA;
2947
+ }
2948
+ const resultB = parseB(rawB, ctx.forSubtree(1));
2949
+ if (isFailure(resultB)) {
2950
+ return resultB;
2951
+ }
2952
+ return ctx.success([resultA.value, resultB.value]);
2953
+ };
2954
+ }
2955
+
2956
+ function pipeParsers(p) {
2957
+ return new ParserPipeline(p);
2958
+ }
2959
+ class ParserPipeline {
2960
+ constructor(parser) {
2961
+ this.parser = parser;
2962
+ }
2963
+ then(nextParser) {
2964
+ return new ParserPipeline(composeParsers(this.parser, nextParser));
2965
+ }
2966
+ }
2967
+ function composeParsers(parserA, parserB) {
2968
+ return (rawValue, ctx) => {
2969
+ const partialResult = parserA(rawValue, ctx);
2970
+ if (isFailure(partialResult)) {
2971
+ return partialResult;
2972
+ }
2973
+ return parserB(partialResult.value, ctx);
2974
+ };
2975
+ }
2976
+
2977
+ function record(parseKey, parseValue) {
2978
+ return (rawValue, ctx) => {
2979
+ if (!isObject(rawValue)) {
2980
+ return ctx.failure("object", rawValue);
2981
+ }
2982
+ const result = {};
2983
+ const mismatches = [];
2984
+ for (const [key, value] of Object.entries(rawValue)) {
2985
+ const entryCtx = ctx.forSubtree(key);
2986
+ const keyResult = parseKey(key, entryCtx);
2987
+ if (isFailure(keyResult)) {
2988
+ mismatches.push(...keyResult.detail);
2989
+ }
2990
+ const valueResult = parseValue(value, entryCtx);
2991
+ if (isFailure(valueResult)) {
2992
+ mismatches.push(...valueResult.detail);
2993
+ }
2994
+ if (isSuccess(keyResult) && isSuccess(valueResult)) {
2995
+ result[keyResult.value] = valueResult.value;
2996
+ }
2997
+ }
2998
+ if (mismatches.length > 0) {
2999
+ return failure(mismatches);
3000
+ }
3001
+ return ctx.success(result);
3002
+ };
3003
+ }
3004
+
3005
+ const string = (rawValue, ctx) => {
3006
+ if (typeof rawValue === "string") {
3007
+ return ctx.success(rawValue);
3008
+ }
3009
+ return ctx.failure("string", rawValue);
3010
+ };
3011
+
3012
+ function trio(parseA, parseB, parseC) {
3013
+ return (rawValue, ctx) => {
3014
+ if (!Array.isArray(rawValue)) {
3015
+ return ctx.failure("array", rawValue);
3016
+ }
3017
+ if (rawValue.length !== 3) {
3018
+ return ctx.failure("array of length 3", rawValue);
3019
+ }
3020
+ const resultA = parseA(rawValue[0], ctx.forSubtree(0));
3021
+ if (isFailure(resultA)) {
3022
+ return resultA;
3023
+ }
3024
+ const resultB = parseB(rawValue[1], ctx.forSubtree(1));
3025
+ if (isFailure(resultB)) {
3026
+ return resultB;
3027
+ }
3028
+ const resultC = parseC(rawValue[2], ctx.forSubtree(2));
3029
+ if (isFailure(resultC)) {
3030
+ return resultC;
3031
+ }
3032
+ return ctx.success([resultA.value, resultB.value, resultC.value]);
3033
+ };
3034
+ }
3035
+
3036
+ function union(parseBranch) {
3037
+ return new UnionBuilder(parseBranch);
3038
+ }
3039
+ class UnionBuilder {
3040
+ constructor(parser) {
3041
+ this.parser = parser;
3042
+ }
3043
+ or(newBranch) {
3044
+ return new UnionBuilder(either(this.parser, newBranch));
3045
+ }
3046
+ }
3047
+ function either(parseA, parseB) {
3048
+ return (rawValue, ctx) => {
3049
+ const resultA = parseA(rawValue, ctx);
3050
+ if (isSuccess(resultA)) {
3051
+ return resultA;
3052
+ }
3053
+ return parseB(rawValue, ctx);
3054
+ };
3055
+ }
3056
+
3057
+ function defaulted(parser, fallback) {
3058
+ return (rawValue, ctx) => {
3059
+ if (rawValue == null) {
3060
+ return success(fallback(rawValue));
3061
+ }
3062
+ return parser(rawValue, ctx);
3063
+ };
3064
+ }
3065
+
3066
+ const parseImages = defaulted(record(string, object({
3067
+ width: number,
3068
+ height: number
3069
+ })), () => ({}));
3070
+
3071
+ function parseWidget(parseType, parseOptions) {
3072
+ return object({
3073
+ type: parseType,
3074
+ static: optional(boolean),
3075
+ graded: optional(boolean),
3076
+ alignment: optional(string),
3077
+ options: parseOptions,
3078
+ key: optional(number),
3079
+ version: optional(object({
3080
+ major: number,
3081
+ minor: number
3082
+ }))
3083
+ });
3084
+ }
3085
+ function parseWidgetWithVersion(parseVersion, parseType, parseOptions) {
3086
+ return object({
3087
+ type: parseType,
3088
+ static: optional(boolean),
3089
+ graded: optional(boolean),
3090
+ alignment: optional(string),
3091
+ options: parseOptions,
3092
+ key: optional(number),
3093
+ version: parseVersion
3094
+ });
3095
+ }
3096
+
3097
+ const parseCategorizerWidget = parseWidget(constant("categorizer"), object({
3098
+ items: array(string),
3099
+ categories: array(string),
3100
+ randomizeItems: defaulted(boolean, () => false),
3101
+ static: defaulted(boolean, () => false),
3102
+ values: array(defaulted(number, () => 0)),
3103
+ highlightLint: optional(boolean),
3104
+ linterContext: optional(object({
3105
+ contentType: string,
3106
+ paths: array(string),
3107
+ stack: array(string)
3108
+ }))
3109
+ }));
3110
+
3111
+ const parseCSProgramWidget = parseWidget(constant("cs-program"), object({
3112
+ programID: string,
3113
+ programType: any,
3114
+ settings: array(object({
3115
+ name: string,
3116
+ value: string
3117
+ })),
3118
+ showEditor: boolean,
3119
+ showButtons: boolean,
3120
+ height: number,
3121
+ static: defaulted(boolean, () => false)
3122
+ }));
3123
+
3124
+ const parseDefinitionWidget = parseWidget(constant("definition"), object({
3125
+ togglePrompt: string,
3126
+ definition: string,
3127
+ static: defaulted(boolean, () => false)
3128
+ }));
3129
+
3130
+ const parseDropdownWidget = parseWidget(constant("dropdown"), object({
3131
+ placeholder: defaulted(string, () => ""),
3132
+ ariaLabel: optional(string),
3133
+ visibleLabel: optional(string),
3134
+ static: defaulted(boolean, () => false),
3135
+ choices: array(object({
3136
+ content: string,
3137
+ correct: boolean
3138
+ }))
3139
+ }));
3140
+
3141
+ const parseExplanationWidget = parseWidget(constant("explanation"), object({
3142
+ showPrompt: string,
3143
+ hidePrompt: string,
3144
+ explanation: string,
3145
+ // We wrap parseWidgetsMap in a function here to make sure it is not
3146
+ // referenced before it is defined. There is an import cycle between
3147
+ // this file and widgets-map.ts that could cause it to be undefined.
3148
+ widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
3149
+ static: defaulted(boolean, () => false)
3150
+ }));
3151
+
3152
+ // Given a function, creates a PartialParser that converts one type to another
3153
+ // using that function. The returned parser never fails.
3154
+ function convert(f) {
3155
+ return (rawValue, ctx) => ctx.success(f(rawValue));
3156
+ }
3157
+
3158
+ /**
3159
+ * Creates a parser for a widget options type with multiple major versions. Old
3160
+ * versions are migrated to the latest version. The parse fails if the input
3161
+ * data does not match any of the versions.
3162
+ *
3163
+ * @example
3164
+ * const parseOptions = versionedWidgetOptions(3, parseOptionsV3)
3165
+ * .withMigrationFrom(2, parseOptionsV2, migrateV2ToV3)
3166
+ * .withMigrationFrom(1, parseOptionsV1, migrateV1ToV2)
3167
+ * .withMigrationFrom(0, parseOptionsV0, migrateV0ToV1)
3168
+ * .parser;
3169
+ *
3170
+ * @param latestMajorVersion the latest major version of the widget options.
3171
+ * @param parseLatest a {@link Parser} for the latest version of the widget
3172
+ * options.
3173
+ * @returns a builder object, to which migrations from earlier versions can be
3174
+ * added. Migrations must be added in "reverse chronological" order as in the
3175
+ * example above.
3176
+ */
3177
+ function versionedWidgetOptions(latestMajorVersion, parseLatest) {
3178
+ return new VersionedWidgetOptionsParserBuilder(latestMajorVersion, parseLatest, latest => latest, (raw, ctx) => ctx.failure("widget options with a known version number", raw));
3179
+ }
3180
+ class VersionedWidgetOptionsParserBuilder {
3181
+ parser;
3182
+ constructor(majorVersion, parseThisVersion, migrateToLatest, parseOtherVersions) {
3183
+ this.migrateToLatest = migrateToLatest;
3184
+ this.parseOtherVersions = parseOtherVersions;
3185
+ const parseThisVersionAndMigrateToLatest = pipeParsers(parseThisVersion).then(convert(this.migrateToLatest)).parser;
3186
+ this.parser = (raw, ctx) => {
3187
+ if (!isObject(raw)) {
3188
+ return ctx.failure("object", raw);
3189
+ }
3190
+ const versionParseResult = parseVersionedObject(raw, ctx);
3191
+ if (isFailure(versionParseResult)) {
3192
+ return versionParseResult;
3193
+ }
3194
+ if (versionParseResult.value.version.major !== majorVersion) {
3195
+ return this.parseOtherVersions(raw, ctx);
3196
+ }
3197
+ return parseThisVersionAndMigrateToLatest(raw, ctx);
3198
+ };
3199
+ }
3200
+
3201
+ /**
3202
+ * Add a migration from an old version of the widget options.
3203
+ */
3204
+ withMigrationFrom(majorVersion, parseOldVersion, migrateToNextVersion) {
3205
+ const parseOtherVersions = this.parser;
3206
+ const migrateToLatest = old => this.migrateToLatest(migrateToNextVersion(old));
3207
+ return new VersionedWidgetOptionsParserBuilder(majorVersion, parseOldVersion, migrateToLatest, parseOtherVersions);
3208
+ }
3209
+ }
3210
+ const parseVersionedObject = object({
3211
+ version: defaulted(object({
3212
+ major: number,
3213
+ minor: number
3214
+ }), () => ({
3215
+ major: 0,
3216
+ minor: 0
3217
+ }))
3218
+ });
3219
+
3220
+ const stringOrNumberOrNullOrUndefined = union(string).or(number).or(constant(null)).or(constant(undefined)).parser;
3221
+ const parsePossiblyInvalidAnswerForm = object({
3222
+ // `value` is the possibly invalid part of this. It should always be a
3223
+ // string, but some answer forms don't have it. The Expression widget
3224
+ // ignores invalid values, so we can safely filter them out during parsing.
3225
+ value: optional(string),
3226
+ form: defaulted(boolean, () => false),
3227
+ simplify: defaulted(boolean, () => false),
3228
+ considered: enumeration("correct", "wrong", "ungraded"),
3229
+ key: pipeParsers(stringOrNumberOrNullOrUndefined).then(convert(String)).parser
3230
+ });
3231
+ function removeInvalidAnswerForms(possiblyInvalid) {
3232
+ const valid = [];
3233
+ for (const answerForm of possiblyInvalid) {
3234
+ const {
3235
+ value
3236
+ } = answerForm;
3237
+ if (value != null) {
3238
+ // Copying the object seems to be needed to make TypeScript happy
3239
+ valid.push({
3240
+ ...answerForm,
3241
+ value
3242
+ });
3243
+ }
3244
+ }
3245
+ return valid;
3246
+ }
3247
+
3248
+ // NOTE(benchristel): I copied the default buttonSets from
3249
+ // expression.tsx. See the parse-perseus-json/README.md for
3250
+ // an explanation of why we want to duplicate the default here.
3251
+ const parseButtonSets = defaulted(array(enumeration("basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations")), () => ["basic", "trig", "prealgebra", "logarithms"]);
3252
+ const version1 = object({
3253
+ major: constant(1),
3254
+ minor: number
3255
+ });
3256
+ const parseExpressionWidgetV1 = parseWidgetWithVersion(version1, constant("expression"), object({
3257
+ answerForms: pipeParsers(array(parsePossiblyInvalidAnswerForm)).then(convert(removeInvalidAnswerForms)).parser,
3258
+ functions: array(string),
3259
+ times: boolean,
3260
+ visibleLabel: optional(string),
3261
+ ariaLabel: optional(string),
3262
+ buttonSets: parseButtonSets,
3263
+ buttonsVisible: optional(enumeration("always", "never", "focused"))
3264
+ }));
3265
+ const version0 = optional(object({
3266
+ major: constant(0),
3267
+ minor: number
3268
+ }));
3269
+ const parseExpressionWidgetV0 = parseWidgetWithVersion(version0, constant("expression"), object({
3270
+ functions: array(string),
3271
+ times: boolean,
3272
+ visibleLabel: optional(string),
3273
+ ariaLabel: optional(string),
3274
+ form: boolean,
3275
+ simplify: boolean,
3276
+ value: string,
3277
+ buttonSets: parseButtonSets,
3278
+ buttonsVisible: optional(enumeration("always", "never", "focused"))
3279
+ }));
3280
+ function migrateV0ToV1(widget) {
3281
+ const {
3282
+ options
3283
+ } = widget;
3284
+ return {
3285
+ ...widget,
3286
+ version: {
3287
+ major: 1,
3288
+ minor: 0
3289
+ },
3290
+ options: {
3291
+ times: options.times,
3292
+ buttonSets: options.buttonSets,
3293
+ functions: options.functions,
3294
+ buttonsVisible: options.buttonsVisible,
3295
+ visibleLabel: options.visibleLabel,
3296
+ ariaLabel: options.ariaLabel,
3297
+ answerForms: [{
3298
+ considered: "correct",
3299
+ form: options.form,
3300
+ simplify: options.simplify,
3301
+ value: options.value
3302
+ }]
3303
+ }
3304
+ };
3305
+ }
3306
+ const parseExpressionWidget = versionedWidgetOptions(1, parseExpressionWidgetV1).withMigrationFrom(0, parseExpressionWidgetV0, migrateV0ToV1).parser;
3307
+
3308
+ const falseToNull = pipeParsers(constant(false)).then(convert(() => null)).parser;
3309
+ const parseGradedGroupWidgetOptions = object({
3310
+ title: defaulted(string, () => ""),
3311
+ hasHint: optional(nullable(boolean)),
3312
+ // This module has an import cycle with parsePerseusRenderer.
3313
+ // The anonymous function below ensures that we don't try to access
3314
+ // parsePerseusRenderer before it's defined.
3315
+ hint: union(falseToNull).or(constant(null)).or(constant(undefined)).or((rawVal, ctx) => parsePerseusRenderer(rawVal, ctx)).parser,
3316
+ content: string,
3317
+ // This module has an import cycle with parseWidgetsMap.
3318
+ // The anonymous function below ensures that we don't try to access
3319
+ // parseWidgetsMap before it's defined.
3320
+ widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx),
3321
+ widgetEnabled: optional(nullable(boolean)),
3322
+ immutableWidgets: optional(nullable(boolean)),
3323
+ images: record(string, object({
3324
+ width: number,
3325
+ height: number
3326
+ }))
3327
+ });
3328
+ const parseGradedGroupWidget = parseWidget(constant("graded-group"), parseGradedGroupWidgetOptions);
3329
+
3330
+ const parseGradedGroupSetWidget = parseWidget(constant("graded-group-set"), object({
3331
+ gradedGroups: array(parseGradedGroupWidgetOptions)
3332
+ }));
3333
+
3334
+ /**
3335
+ * discriminatedUnion() should be preferred over union() when parsing a
3336
+ * discriminated union type, because discriminatedUnion() produces more
3337
+ * understandable failure messages. It takes the discriminant as the source of
3338
+ * truth for which variant is to be parsed, and expects the other data to match
3339
+ * that variant.
3340
+ */
3341
+ function discriminatedUnionOn(discriminantKey) {
3342
+ const noMoreBranches = (raw, ctx) => {
3343
+ if (!isObject(raw)) {
3344
+ return ctx.failure("object", raw);
3345
+ }
3346
+ return ctx.forSubtree(discriminantKey).failure("a valid value", raw[discriminantKey]);
3347
+ };
3348
+ return new DiscriminatedUnionBuilder(discriminantKey, noMoreBranches);
3349
+ }
3350
+ class DiscriminatedUnionBuilder {
3351
+ constructor(discriminantKey, parser) {
3352
+ this.discriminantKey = discriminantKey;
3353
+ this.parser = parser;
3354
+ }
3355
+ withBranch(discriminantValue, parseNewVariant) {
3356
+ const parseNewBranch = discriminatedUnionBranch(this.discriminantKey, discriminantValue, parseNewVariant, this.parser);
3357
+ return new DiscriminatedUnionBuilder(this.discriminantKey, parseNewBranch);
3358
+ }
3359
+ }
3360
+ function discriminatedUnionBranch(discriminantKey, discriminantValue, parseVariant, parseOtherBranches) {
3361
+ return (raw, ctx) => {
3362
+ if (!isObject(raw)) {
3363
+ return ctx.failure("object", raw);
3364
+ }
3365
+ if (raw[discriminantKey] === discriminantValue) {
3366
+ return parseVariant(raw, ctx);
3367
+ }
3368
+ return parseOtherBranches(raw, ctx);
3369
+ };
3370
+ }
3371
+
3372
+ const pairOfNumbers$3 = pair(number, number);
3373
+ const pairOfPoints = pair(pairOfNumbers$3, pairOfNumbers$3);
3374
+ const parseGrapherWidget = parseWidget(constant("grapher"), object({
3375
+ availableTypes: array(enumeration("absolute_value", "exponential", "linear", "logarithm", "quadratic", "sinusoid", "tangent")),
3376
+ correct: discriminatedUnionOn("type").withBranch("absolute_value", object({
3377
+ type: constant("absolute_value"),
3378
+ coords: nullable(pairOfPoints)
3379
+ })).withBranch("exponential", object({
3380
+ type: constant("exponential"),
3381
+ asymptote: pairOfPoints,
3382
+ coords: nullable(pairOfPoints)
3383
+ })).withBranch("linear", object({
3384
+ type: constant("linear"),
3385
+ coords: nullable(pairOfPoints)
3386
+ })).withBranch("logarithm", object({
3387
+ type: constant("logarithm"),
3388
+ asymptote: pairOfPoints,
3389
+ coords: nullable(pairOfPoints)
3390
+ })).withBranch("quadratic", object({
3391
+ type: constant("quadratic"),
3392
+ coords: nullable(pairOfPoints)
3393
+ })).withBranch("sinusoid", object({
3394
+ type: constant("sinusoid"),
3395
+ coords: nullable(pairOfPoints)
3396
+ })).withBranch("tangent", object({
3397
+ type: constant("tangent"),
3398
+ coords: nullable(pairOfPoints)
3399
+ })).parser,
3400
+ graph: object({
3401
+ backgroundImage: object({
3402
+ bottom: optional(number),
3403
+ height: optional(number),
3404
+ left: optional(number),
3405
+ scale: optional(number),
3406
+ url: optional(nullable(string)),
3407
+ width: optional(number)
3408
+ }),
3409
+ box: optional(pairOfNumbers$3),
3410
+ editableSettings: optional(array(enumeration("graph", "snap", "image", "measure"))),
3411
+ gridStep: optional(pairOfNumbers$3),
3412
+ labels: pair(string, string),
3413
+ markings: enumeration("graph", "none", "grid"),
3414
+ range: pair(pairOfNumbers$3, pairOfNumbers$3),
3415
+ rulerLabel: constant(""),
3416
+ rulerTicks: number,
3417
+ showProtractor: optional(boolean),
3418
+ showRuler: optional(boolean),
3419
+ showTooltips: optional(boolean),
3420
+ snapStep: optional(pairOfNumbers$3),
3421
+ step: pairOfNumbers$3,
3422
+ valid: optional(union(boolean).or(string).parser)
3423
+ })
3424
+ }));
3425
+
3426
+ const parseGroupWidget = parseWidget(constant("group"),
3427
+ // This module has an import cycle with parsePerseusRenderer.
3428
+ // The anonymous function below ensures that we don't try to access
3429
+ // parsePerseusRenderer before it's defined.
3430
+ (rawVal, ctx) => parsePerseusRenderer(rawVal, ctx));
3431
+
3432
+ const parseIframeWidget = parseWidget(constant("iframe"), object({
3433
+ url: string,
3434
+ settings: optional(array(object({
3435
+ name: string,
3436
+ value: string
3437
+ }))),
3438
+ width: union(number).or(string).parser,
3439
+ height: union(number).or(string).parser,
3440
+ allowFullScreen: defaulted(boolean, () => false),
3441
+ allowTopNavigation: optional(boolean),
3442
+ static: defaulted(boolean, () => false)
3443
+ }));
3444
+
3445
+ const stringToNumber = (rawValue, ctx) => {
3446
+ if (typeof rawValue === "number") {
3447
+ return ctx.success(rawValue);
3448
+ }
3449
+ const parsedNumber = +rawValue;
3450
+ if (rawValue === "" || isNaN(parsedNumber)) {
3451
+ return ctx.failure("a number or numeric string", rawValue);
3452
+ }
3453
+ return ctx.success(parsedNumber);
3454
+ };
3455
+
3456
+ function emptyToZero(x) {
3457
+ return x === "" ? 0 : x;
3458
+ }
3459
+ const imageDimensionToNumber = pipeParsers(union(number).or(string).parser)
3460
+ // In this specific case, empty string is equivalent to zero. An empty
3461
+ // string parses to either NaN (using parseInt) or 0 (using unary +) and
3462
+ // CSS will treat NaN as invalid and default to 0 instead.
3463
+ .then(convert(emptyToZero)).then(stringToNumber).parser;
3464
+ const parsePerseusImageBackground = object({
3465
+ url: optional(nullable(string)),
3466
+ width: optional(imageDimensionToNumber),
3467
+ height: optional(imageDimensionToNumber),
3468
+ top: optional(imageDimensionToNumber),
3469
+ left: optional(imageDimensionToNumber),
3470
+ bottom: optional(imageDimensionToNumber),
3471
+ scale: optional(imageDimensionToNumber)
3472
+ });
3473
+
3474
+ const pairOfNumbers$2 = pair(number, number);
3475
+ const parseImageWidget = parseWidget(constant("image"), object({
3476
+ title: optional(string),
3477
+ caption: optional(string),
3478
+ alt: optional(string),
3479
+ backgroundImage: parsePerseusImageBackground,
3480
+ static: optional(boolean),
3481
+ labels: optional(array(object({
3482
+ content: string,
3483
+ alignment: string,
3484
+ coordinates: array(number)
3485
+ }))),
3486
+ range: optional(pair(pairOfNumbers$2, pairOfNumbers$2)),
3487
+ box: optional(pairOfNumbers$2)
3488
+ }));
3489
+
3490
+ const booleanToString = (rawValue, ctx) => {
3491
+ if (typeof rawValue === "boolean") {
3492
+ return ctx.success(String(rawValue));
3493
+ }
3494
+ return ctx.failure("boolean", rawValue);
3495
+ };
3496
+ const parseInputNumberWidget = parseWidget(constant("input-number"), object({
3497
+ answerType: optional(enumeration("number", "decimal", "integer", "rational", "improper", "mixed", "percent", "pi")),
3498
+ inexact: optional(boolean),
3499
+ maxError: optional(union(number).or(string).parser),
3500
+ rightAlign: optional(boolean),
3501
+ simplify: enumeration("required", "optional", "enforced"),
3502
+ size: enumeration("normal", "small"),
3503
+ // TODO(benchristel): there are some content items where value is a
3504
+ // boolean, even though that makes no sense. We should figure out if
3505
+ // those content items are actually published anywhere, and consider
3506
+ // updating them.
3507
+ value: union(number).or(string).or(booleanToString).parser,
3508
+ customKeypad: optional(boolean)
3509
+ }));
3510
+
3511
+ const pairOfNumbers$1 = pair(number, number);
3512
+ const stringOrEmpty = defaulted(string, () => "");
3513
+ const parseKey = pipeParsers(optional(string)).then(convert(String)).parser;
3514
+ const parseFunctionElement = object({
3515
+ type: constant("function"),
3516
+ key: parseKey,
3517
+ options: object({
3518
+ value: string,
3519
+ funcName: string,
3520
+ rangeMin: string,
3521
+ rangeMax: string,
3522
+ color: string,
3523
+ strokeDasharray: string,
3524
+ strokeWidth: number
3525
+ })
3526
+ });
3527
+ const parseLabelElement = object({
3528
+ type: constant("label"),
3529
+ key: parseKey,
3530
+ options: object({
3531
+ label: string,
3532
+ color: string,
3533
+ coordX: string,
3534
+ coordY: string
3535
+ })
3536
+ });
3537
+ const parseLineElement = object({
3538
+ type: constant("line"),
3539
+ key: parseKey,
3540
+ options: object({
3541
+ color: string,
3542
+ startX: string,
3543
+ startY: string,
3544
+ endX: string,
3545
+ endY: string,
3546
+ strokeDasharray: string,
3547
+ strokeWidth: number,
3548
+ arrows: string
3549
+ })
3550
+ });
3551
+ const parseMovableLineElement = object({
3552
+ type: constant("movable-line"),
3553
+ key: parseKey,
3554
+ options: object({
3555
+ startX: string,
3556
+ startY: string,
3557
+ startSubscript: number,
3558
+ endX: string,
3559
+ endY: string,
3560
+ endSubscript: number,
3561
+ constraint: string,
3562
+ snap: number,
3563
+ constraintFn: string,
3564
+ constraintXMin: string,
3565
+ constraintXMax: string,
3566
+ constraintYMin: string,
3567
+ constraintYMax: string
3568
+ })
3569
+ });
3570
+ const parseMovablePointElement = object({
3571
+ type: constant("movable-point"),
3572
+ key: parseKey,
3573
+ options: object({
3574
+ startX: string,
3575
+ startY: string,
3576
+ varSubscript: number,
3577
+ constraint: string,
3578
+ snap: number,
3579
+ constraintFn: string,
3580
+ constraintXMin: stringOrEmpty,
3581
+ constraintXMax: stringOrEmpty,
3582
+ constraintYMin: stringOrEmpty,
3583
+ constraintYMax: stringOrEmpty
3584
+ })
3585
+ });
3586
+ const parseParametricElement = object({
3587
+ type: constant("parametric"),
3588
+ key: parseKey,
3589
+ options: object({
3590
+ x: string,
3591
+ y: string,
3592
+ rangeMin: string,
3593
+ rangeMax: string,
3594
+ color: string,
3595
+ strokeDasharray: string,
3596
+ strokeWidth: number
3597
+ })
3598
+ });
3599
+ const parsePointElement = object({
3600
+ type: constant("point"),
3601
+ key: parseKey,
3602
+ options: object({
3603
+ color: string,
3604
+ coordX: string,
3605
+ coordY: string
3606
+ })
3607
+ });
3608
+ const parseRectangleElement = object({
3609
+ type: constant("rectangle"),
3610
+ key: parseKey,
3611
+ options: object({
3612
+ color: string,
3613
+ coordX: string,
3614
+ coordY: string,
3615
+ width: string,
3616
+ height: string
3617
+ })
3618
+ });
3619
+ const parseInteractionWidget = parseWidget(constant("interaction"), object({
3620
+ static: defaulted(boolean, () => false),
3621
+ graph: object({
3622
+ editableSettings: optional(array(enumeration("canvas", "graph"))),
3623
+ box: pairOfNumbers$1,
3624
+ labels: array(string),
3625
+ range: pair(pairOfNumbers$1, pairOfNumbers$1),
3626
+ gridStep: pairOfNumbers$1,
3627
+ markings: enumeration("graph", "grid", "none"),
3628
+ snapStep: optional(pairOfNumbers$1),
3629
+ valid: optional(union(boolean).or(string).parser),
3630
+ backgroundImage: optional(parsePerseusImageBackground),
3631
+ showProtractor: optional(boolean),
3632
+ showRuler: optional(boolean),
3633
+ rulerLabel: optional(string),
3634
+ rulerTicks: optional(number),
3635
+ tickStep: pairOfNumbers$1
3636
+ }),
3637
+ 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)
3638
+ }));
3639
+
3640
+ /**
3641
+ * The Perseus "data schema" file.
3642
+ *
3643
+ * This file, and the types in it, represents the "data schema" that Perseus
3644
+ * uses. The @khanacademy/perseus-editor package edits and produces objects
3645
+ * that conform to the types in this file. Similarly, the top-level renderers
3646
+ * in @khanacademy/perseus, consume objects that conform to these types.
3647
+ *
3648
+ * WARNING: This file should not import any types from elsewhere so that it is
3649
+ * easy to reason about changes that alter the Perseus schema. This helps
3650
+ * ensure that it is not changed accidentally when upgrading a dependant
3651
+ * package or other part of Perseus code. Note that TypeScript does type
3652
+ * checking via something called "structural typing". This means that as long
3653
+ * as the shape of a type matches, the name it goes by doesn't matter. As a
3654
+ * result, a `Coord` type that looks like this `[x: number, y: number]` is
3655
+ * _identical_, in TypeScript's eyes, to this `Vector2` type `[x: number, y:
3656
+ * number]`. Also, with tuples, the labels for each entry is ignored, so `[x:
3657
+ * number, y: number]` is compatible with `[min: number, max: number]`. The
3658
+ * labels are for humans, not TypeScript. :)
3659
+ *
3660
+ * If you make changes to types in this file, be very sure that:
3661
+ *
3662
+ * a) the changes are backwards compatible. If they are not, old data from
3663
+ * previous versions of the "schema" could become unrenderable, or worse,
3664
+ * introduce hard-to-diagnose bugs.
3665
+ * b) the parsing code (`util/parse-perseus-json/`) is updated to handle
3666
+ * the new format _as well as_ the old format.
3667
+ */
3668
+
3669
+ // TODO(FEI-4010): Remove `Perseus` prefix for all types here
3670
+
3671
+ // Same name as Mafs
3672
+
3673
+ /**
3674
+ * A utility type that constructs a widget map from a "registry interface".
3675
+ * The keys of the registry should be the widget type (aka, "categorizer" or
3676
+ * "radio", etc) and the value should be the option type stored in the value
3677
+ * of the map.
3678
+ *
3679
+ * You can think of this as a type that generates another type. We use
3680
+ * "registry interfaces" as a way to keep a set of widget types to their data
3681
+ * type in several places in Perseus. This type then allows us to generate a
3682
+ * map type that maps a widget id to its data type and keep strong typing by
3683
+ * widget id.
3684
+ *
3685
+ * For example, given a fictitious registry such as this:
3686
+ *
3687
+ * ```
3688
+ * interface DummyRegistry {
3689
+ * categorizer: { categories: ReadonlyArray<string> };
3690
+ * dropdown: { choices: ReadonlyArray<string> }:
3691
+ * }
3692
+ * ```
3693
+ *
3694
+ * If we create a DummyMap using this helper:
3695
+ *
3696
+ * ```
3697
+ * type DummyMap = MakeWidgetMap<DummyRegistry>;
3698
+ * ```
3699
+ *
3700
+ * We'll get a map that looks like this:
3701
+ *
3702
+ * ```
3703
+ * type DummyMap = {
3704
+ * `categorizer ${number}`: { categories: ReadonlyArray<string> };
3705
+ * `dropdown ${number}`: { choices: ReadonlyArray<string> };
3706
+ * }
3707
+ * ```
3708
+ *
3709
+ * We use interfaces for the registries so that they can be extended in cases
3710
+ * where the consuming app brings along their own widgets. Interfaces in
3711
+ * TypeScript are always open (ie. you can extend them) whereas types aren't.
3712
+ */
3713
+
3714
+ /**
3715
+ * Our core set of Perseus widgets.
3716
+ *
3717
+ * This interface is the basis for "registering" all Perseus widget types.
3718
+ * There should be one key/value pair for each supported widget. If you create
3719
+ * a new widget, an entry should be added to this interface. Note that this
3720
+ * only registers the widget options type, you'll also need to register the
3721
+ * widget so that it's available at runtime (@see
3722
+ * {@link file://./widgets.ts#registerWidget}).
3723
+ *
3724
+ * Importantly, the key should be the name that is used in widget IDs. For most
3725
+ * widgets that is the same as the widget option's `type` field. In cases where
3726
+ * a widget has been deprecated and replaced with the deprecated-standin
3727
+ * widget, it should be the original widget type!
3728
+ *
3729
+ * If you define the widget outside of this package, you can still add the new
3730
+ * widget to this interface by writing the following in that package that
3731
+ * contains the widget. TypeScript will merge that definition of the
3732
+ * `PerseusWidgets` with the one defined below.
3733
+ *
3734
+ * ```typescript
3735
+ * declare module "@khanacademy/perseus-core" {
3736
+ * interface PerseusWidgetTypes {
3737
+ * // A new widget
3738
+ * "new-awesomeness": MyAwesomeNewWidget;
3739
+ *
3740
+ * // A deprecated widget
3741
+ * "super-old-widget": DeprecatedStandinWidget;
3742
+ * }
3743
+ * }
3744
+ *
3745
+ * // The new widget's options definition
3746
+ * type MyAwesomeNewWidget = WidgetOptions<'new-awesomeness', MyAwesomeNewWidgetOptions>;
3747
+ *
3748
+ * // The deprecated widget's options definition
3749
+ * type SuperOldWidget = WidgetOptions<'super-old-widget', object>;
3750
+ * ```
3751
+ *
3752
+ * This interface can be extended through the magic of TypeScript "Declaration
3753
+ * merging". Specifically, we augment this module and extend this interface.
3754
+ *
3755
+ * @see {@link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation}
3756
+ */
3757
+
3758
+ /**
3759
+ * A map of widget IDs to widget options. This is most often used as the type
3760
+ * for a set of widgets defined in a `PerseusItem` but can also be useful to
3761
+ * represent a function parameter where only `widgets` from a `PerseusItem` are
3762
+ * needed. Today Widget IDs are made up of the widget type and an incrementing
3763
+ * integer (eg. `interactive-graph 1` or `radio 3`). It is suggested to avoid
3764
+ * reading/parsing the widget id to derive any information from it, except in
3765
+ * the case of this map.
3766
+ *
3767
+ * @see {@link PerseusWidgetTypes} additional widgets can be added to this map type
3768
+ * by augmenting the PerseusWidgetTypes with new widget types!
3769
+ */
3770
+
3771
+ /**
3772
+ * PerseusWidget is a union of all the different types of widget options that
3773
+ * Perseus knows about.
3774
+ *
3775
+ * Thanks to it being based on PerseusWidgetTypes interface, this union is
3776
+ * automatically extended to include widgets used in tests without those widget
3777
+ * option types seeping into our production types.
3778
+ *
3779
+ * @see MockWidget for an example
3780
+ */
3781
+
3782
+ /**
3783
+ * A "PerseusItem" is a classic Perseus item. It is rendered by the
3784
+ * `ServerItemRenderer` and the layout is pre-set.
3785
+ *
3786
+ * To render more complex Perseus items, see the `Item` type in the multi item
3787
+ * area.
3788
+ */
3789
+
3790
+ /**
3791
+ * A "PerseusArticle" is an item that is meant to be rendered as an article.
3792
+ * This item is never scored and is rendered by the `ArticleRenderer`.
3793
+ */
3794
+
3795
+ const ItemExtras = [
3796
+ // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
3797
+ "calculator",
3798
+ // The user might benefit from using a statistics Chi Squared Table like https://people.richland.edu/james/lecture/m170/tbl-chi.html
3799
+ "chi2Table",
3800
+ // The user might benefit from a monthly payments calculator. Provided on Khan Academy when true
3801
+ "financialCalculatorMonthlyPayment",
3802
+ // The user might benefit from a total amount calculator. Provided on Khan Academy when true
3803
+ "financialCalculatorTotalAmount",
3804
+ // The user might benefit from a time to pay off calculator. Provided on Khan Academy when true
3805
+ "financialCalculatorTimeToPayOff",
3806
+ // The user might benefit from using a Periodic Table of Elements. Provided on Khan Academy when true
3807
+ "periodicTable",
3808
+ // The user might benefit from using a Periodic Table of Elements with key. Provided on Khan Academy when true
3809
+ "periodicTableWithKey",
3810
+ // The user might benefit from using a statistics T Table like https://www.statisticshowto.com/tables/t-distribution-table/
3811
+ "tTable",
3812
+ // The user might benefit from using a statistics Z Table like https://www.ztable.net/
3813
+ "zTable"];
3814
+
3815
+ /**
3816
+ * The type representing the common structure of all widget's options. The
3817
+ * `Options` generic type represents the widget-specific option data.
3818
+ */
3819
+
3820
+ // prettier-ignore
3821
+
3822
+ // prettier-ignore
3823
+
3824
+ // prettier-ignore
3825
+
3826
+ // prettier-ignore
3827
+
3828
+ // prettier-ignore
3829
+
3830
+ // prettier-ignore
3831
+
3832
+ // prettier-ignore
3833
+
3834
+ // prettier-ignore
3835
+
3836
+ // prettier-ignore
3837
+
3838
+ // prettier-ignore
3839
+
3840
+ // prettier-ignore
3841
+
3842
+ // prettier-ignore
3843
+
3844
+ // prettier-ignore
3845
+
3846
+ // prettier-ignore
3847
+
3848
+ // prettier-ignore
3849
+
3850
+ // prettier-ignore
3851
+
3852
+ // prettier-ignore
3853
+
3854
+ // prettier-ignore
3855
+
3856
+ // prettier-ignore
3857
+
3858
+ // prettier-ignore
3859
+
3860
+ // prettier-ignore
3861
+
3862
+ // prettier-ignore
3863
+
3864
+ // prettier-ignore
3865
+
3866
+ // prettier-ignore
3867
+
3868
+ // prettier-ignore
3869
+
3870
+ // prettier-ignore
3871
+
3872
+ // prettier-ignore
3873
+
3874
+ // prettier-ignore
3875
+
3876
+ // prettier-ignore
3877
+
3878
+ // prettier-ignore
3879
+
3880
+ // prettier-ignore
3881
+
3882
+ // prettier-ignore
2777
3883
 
2778
3884
  // prettier-ignore
2779
3885
 
2780
- // prettier-ignore
3886
+ //prettier-ignore
3887
+
3888
+ /**
3889
+ * A background image applied to various widgets.
3890
+ */
3891
+
3892
+ /**
3893
+ * The type of markings to display on the graph.
3894
+ * - axes: shows the axes without the gride lines
3895
+ * - graph: shows the axes and the grid lines
3896
+ * - grid: shows only the grid lines
3897
+ * - none: shows no markings
3898
+ */
3899
+
3900
+ const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
3901
+
3902
+ // 2D range: xMin, xMax, yMin, yMax
3903
+
3904
+ const lockedFigureColorNames = ["blue", "green", "grayH", "purple", "pink", "orange", "red"];
3905
+ const lockedFigureColors = {
3906
+ blue: "#3D7586",
3907
+ green: "#447A53",
3908
+ grayH: "#3B3D45",
3909
+ purple: "#594094",
3910
+ pink: "#B25071",
3911
+ red: "#D92916",
3912
+ orange: "#946700"
3913
+ };
3914
+ const lockedFigureFillStyles = {
3915
+ none: 0,
3916
+ white: 1,
3917
+ translucent: 0.4,
3918
+ solid: 1
3919
+ };
3920
+
3921
+ // Not associated with a specific figure
3922
+
3923
+ const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
3924
+
3925
+ // Used to represent 2-D points and ranges
3926
+ const pairOfNumbers = pair(number, number);
3927
+ const parsePerseusGraphTypeAngle = object({
3928
+ type: constant("angle"),
3929
+ showAngles: optional(boolean),
3930
+ allowReflexAngles: optional(boolean),
3931
+ angleOffsetDeg: optional(number),
3932
+ snapDegrees: optional(number),
3933
+ match: optional(constant("congruent")),
3934
+ coords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
3935
+ startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))
3936
+ });
3937
+ const parsePerseusGraphTypeCircle = object({
3938
+ type: constant("circle"),
3939
+ center: optional(pairOfNumbers),
3940
+ radius: optional(number),
3941
+ startCoords: optional(object({
3942
+ center: pairOfNumbers,
3943
+ radius: number
3944
+ })),
3945
+ // TODO: remove coord? it's legacy.
3946
+ coord: optional(pairOfNumbers)
3947
+ });
3948
+ const parsePerseusGraphTypeLinear = object({
3949
+ type: constant("linear"),
3950
+ coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
3951
+ startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
3952
+ // TODO: remove coord? it's legacy.
3953
+ coord: optional(pairOfNumbers)
3954
+ });
3955
+ const parsePerseusGraphTypeLinearSystem = object({
3956
+ type: constant("linear-system"),
3957
+ // TODO(benchristel): default coords to empty array?
3958
+ coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
3959
+ startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
3960
+ // TODO: remove coord? it's legacy.
3961
+ coord: optional(pairOfNumbers)
3962
+ });
3963
+ const parsePerseusGraphTypeNone = object({
3964
+ type: constant("none")
3965
+ });
3966
+ const parsePerseusGraphTypePoint = object({
3967
+ type: constant("point"),
3968
+ numPoints: optional(union(number).or(constant("unlimited")).parser),
3969
+ coords: optional(nullable(array(pairOfNumbers))),
3970
+ startCoords: optional(array(pairOfNumbers)),
3971
+ // TODO: remove coord? it's legacy.
3972
+ coord: optional(pairOfNumbers)
3973
+ });
3974
+ const parsePerseusGraphTypePolygon = object({
3975
+ type: constant("polygon"),
3976
+ numSides: optional(union(number).or(constant("unlimited")).parser),
3977
+ showAngles: optional(boolean),
3978
+ showSides: optional(boolean),
3979
+ snapTo: optional(enumeration("grid", "angles", "sides")),
3980
+ match: optional(enumeration("similar", "congruent", "approx", "exact")),
3981
+ startCoords: optional(array(pairOfNumbers)),
3982
+ // TODO: remove coord? it's legacy.
3983
+ coord: optional(pairOfNumbers)
3984
+ });
3985
+ const parsePerseusGraphTypeQuadratic = object({
3986
+ type: constant("quadratic"),
3987
+ coords: optional(nullable(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers))),
3988
+ startCoords: optional(trio(pairOfNumbers, pairOfNumbers, pairOfNumbers)),
3989
+ // TODO: remove coord? it's legacy.
3990
+ coord: optional(pairOfNumbers)
3991
+ });
3992
+ const parsePerseusGraphTypeRay = object({
3993
+ type: constant("ray"),
3994
+ coords: optional(nullable(pair(pairOfNumbers, pairOfNumbers))),
3995
+ startCoords: optional(pair(pairOfNumbers, pairOfNumbers)),
3996
+ // TODO: remove coord? it's legacy.
3997
+ coord: optional(pairOfNumbers)
3998
+ });
3999
+ const parsePerseusGraphTypeSegment = object({
4000
+ type: constant("segment"),
4001
+ // TODO(benchristel): default numSegments?
4002
+ numSegments: optional(number),
4003
+ coords: optional(nullable(array(pair(pairOfNumbers, pairOfNumbers)))),
4004
+ startCoords: optional(array(pair(pairOfNumbers, pairOfNumbers))),
4005
+ // TODO: remove coord? it's legacy.
4006
+ coord: optional(pairOfNumbers)
4007
+ });
4008
+ const parsePerseusGraphTypeSinusoid = object({
4009
+ type: constant("sinusoid"),
4010
+ coords: optional(nullable(array(pairOfNumbers))),
4011
+ startCoords: optional(array(pairOfNumbers)),
4012
+ // TODO: remove coord? it's legacy.
4013
+ coord: optional(pairOfNumbers)
4014
+ });
4015
+ 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;
4016
+ const parseLockedFigureColor = enumeration(...lockedFigureColorNames);
4017
+ const parseLockedFigureFillType = enumeration("none", "white", "translucent", "solid");
4018
+ const parseLockedLineStyle = enumeration("solid", "dashed");
4019
+ const parseLockedLabelType = object({
4020
+ type: constant("label"),
4021
+ coord: pairOfNumbers,
4022
+ text: string,
4023
+ color: parseLockedFigureColor,
4024
+ size: enumeration("small", "medium", "large")
4025
+ });
4026
+ const parseLockedPointType = object({
4027
+ type: constant("point"),
4028
+ coord: pairOfNumbers,
4029
+ color: parseLockedFigureColor,
4030
+ filled: boolean,
4031
+ // TODO(benchristel): default labels to empty array?
4032
+ labels: optional(array(parseLockedLabelType)),
4033
+ ariaLabel: optional(string)
4034
+ });
4035
+ const parseLockedLineType = object({
4036
+ type: constant("line"),
4037
+ kind: enumeration("line", "ray", "segment"),
4038
+ points: pair(parseLockedPointType, parseLockedPointType),
4039
+ color: parseLockedFigureColor,
4040
+ lineStyle: parseLockedLineStyle,
4041
+ showPoint1: defaulted(boolean, () => false),
4042
+ showPoint2: defaulted(boolean, () => false),
4043
+ // TODO(benchristel): default labels to empty array?
4044
+ labels: optional(array(parseLockedLabelType)),
4045
+ ariaLabel: optional(string)
4046
+ });
4047
+ const parseLockedVectorType = object({
4048
+ type: constant("vector"),
4049
+ points: pair(pairOfNumbers, pairOfNumbers),
4050
+ color: parseLockedFigureColor,
4051
+ // TODO(benchristel): default labels to empty array?
4052
+ labels: optional(array(parseLockedLabelType)),
4053
+ ariaLabel: optional(string)
4054
+ });
4055
+ const parseLockedEllipseType = object({
4056
+ type: constant("ellipse"),
4057
+ center: pairOfNumbers,
4058
+ radius: pairOfNumbers,
4059
+ angle: number,
4060
+ color: parseLockedFigureColor,
4061
+ fillStyle: parseLockedFigureFillType,
4062
+ strokeStyle: parseLockedLineStyle,
4063
+ // TODO(benchristel): default labels to empty array?
4064
+ labels: optional(array(parseLockedLabelType)),
4065
+ ariaLabel: optional(string)
4066
+ });
4067
+ const parseLockedPolygonType = object({
4068
+ type: constant("polygon"),
4069
+ points: array(pairOfNumbers),
4070
+ color: parseLockedFigureColor,
4071
+ showVertices: boolean,
4072
+ fillStyle: parseLockedFigureFillType,
4073
+ strokeStyle: parseLockedLineStyle,
4074
+ // TODO(benchristel): default labels to empty array?
4075
+ labels: optional(array(parseLockedLabelType)),
4076
+ ariaLabel: optional(string)
4077
+ });
4078
+ const parseLockedFunctionType = object({
4079
+ type: constant("function"),
4080
+ color: parseLockedFigureColor,
4081
+ strokeStyle: parseLockedLineStyle,
4082
+ equation: string,
4083
+ directionalAxis: enumeration("x", "y"),
4084
+ domain: optional(pairOfNumbers),
4085
+ // TODO(benchristel): default labels to empty array?
4086
+ labels: optional(array(parseLockedLabelType)),
4087
+ ariaLabel: optional(string)
4088
+ });
4089
+ const parseLockedFigure = discriminatedUnionOn("type").withBranch("point", parseLockedPointType).withBranch("line", parseLockedLineType).withBranch("vector", parseLockedVectorType).withBranch("ellipse", parseLockedEllipseType).withBranch("polygon", parseLockedPolygonType).withBranch("function", parseLockedFunctionType).withBranch("label", parseLockedLabelType).parser;
4090
+ const parseInteractiveGraphWidget = parseWidget(constant("interactive-graph"), object({
4091
+ step: pairOfNumbers,
4092
+ // TODO(benchristel): rather than making gridStep and snapStep
4093
+ // optional, we should duplicate the defaulting logic from the
4094
+ // InteractiveGraph component. See parse-perseus-json/README.md for
4095
+ // why.
4096
+ gridStep: optional(pairOfNumbers),
4097
+ snapStep: optional(pairOfNumbers),
4098
+ backgroundImage: optional(parsePerseusImageBackground),
4099
+ markings: enumeration("graph", "grid", "none"),
4100
+ labels: optional(array(string)),
4101
+ showProtractor: boolean,
4102
+ showRuler: optional(boolean),
4103
+ showTooltips: optional(boolean),
4104
+ rulerLabel: optional(string),
4105
+ rulerTicks: optional(number),
4106
+ range: pair(pairOfNumbers, pairOfNumbers),
4107
+ // NOTE(benchristel): I copied the default graph from
4108
+ // interactive-graph.tsx. See the parse-perseus-json/README.md for
4109
+ // an explanation of why we want to duplicate the default here.
4110
+ graph: defaulted(parsePerseusGraphType, () => ({
4111
+ type: "linear"
4112
+ })),
4113
+ correct: parsePerseusGraphType,
4114
+ // TODO(benchristel): default lockedFigures to empty array
4115
+ lockedFigures: optional(array(parseLockedFigure)),
4116
+ fullGraphLabel: optional(string),
4117
+ fullGraphAriaDescription: optional(string)
4118
+ }));
4119
+
4120
+ const parseLabelImageWidget = parseWidget(constant("label-image"), object({
4121
+ choices: array(string),
4122
+ imageUrl: string,
4123
+ imageAlt: string,
4124
+ imageHeight: number,
4125
+ imageWidth: number,
4126
+ markers: array(object({
4127
+ answers: array(string),
4128
+ label: string,
4129
+ x: number,
4130
+ y: number
4131
+ })),
4132
+ hideChoicesFromInstructions: boolean,
4133
+ multipleAnswers: boolean,
4134
+ static: defaulted(boolean, () => false)
4135
+ }));
4136
+
4137
+ const parseMatcherWidget = parseWidget(constant("matcher"), object({
4138
+ labels: array(string),
4139
+ left: array(string),
4140
+ right: array(string),
4141
+ orderMatters: boolean,
4142
+ padding: boolean
4143
+ }));
4144
+
4145
+ const numberOrString = union(number).or(string).parser;
4146
+ const numeric = pipeParsers(defaulted(numberOrString, () => NaN)).then(stringToNumber).parser;
4147
+ const parseMatrixWidget = parseWidget(defaulted(constant("matrix"), () => "matrix"), object({
4148
+ prefix: optional(string),
4149
+ suffix: optional(string),
4150
+ answers: array(array(numeric)),
4151
+ cursorPosition: optional(array(number)),
4152
+ matrixBoardSize: array(number),
4153
+ static: optional(boolean)
4154
+ }));
4155
+
4156
+ const parseMeasurerWidget = parseWidget(constant("measurer"), object({
4157
+ // The default value for image comes from measurer.tsx.
4158
+ // See parse-perseus-json/README.md for why we want to duplicate the
4159
+ // defaults here.
4160
+ image: defaulted(parsePerseusImageBackground, () => ({
4161
+ url: null,
4162
+ top: 0,
4163
+ left: 0
4164
+ })),
4165
+ showProtractor: boolean,
4166
+ showRuler: boolean,
4167
+ rulerLabel: string,
4168
+ rulerTicks: number,
4169
+ rulerPixels: number,
4170
+ rulerLength: number,
4171
+ box: pair(number, number),
4172
+ // TODO(benchristel): static is not used. Remove it?
4173
+ static: defaulted(boolean, () => false)
4174
+ }));
4175
+
4176
+ const parseMoleculeRendererWidget = parseWidget(constant("molecule-renderer"), object({
4177
+ widgetId: string,
4178
+ rotationAngle: optional(number),
4179
+ smiles: optional(string)
4180
+ }));
4181
+
4182
+ const emptyStringToNull = pipeParsers(constant("")).then(convert(() => null)).parser;
4183
+ const parseNumberLineWidget = parseWidget(constant("number-line"), object({
4184
+ range: array(number),
4185
+ labelRange: array(nullable(union(number).or(emptyStringToNull).parser)),
4186
+ labelStyle: string,
4187
+ labelTicks: boolean,
4188
+ isTickCtrl: optional(nullable(boolean)),
4189
+ divisionRange: array(number),
4190
+ numDivisions: optional(nullable(number)),
4191
+ // NOTE(benchristel): I copied the default snapDivisions from
4192
+ // number-line.tsx. See the parse-perseus-json/README.md for
4193
+ // an explanation of why we want to duplicate the default here.
4194
+ snapDivisions: defaulted(number, () => 2),
4195
+ tickStep: optional(nullable(number)),
4196
+ correctRel: optional(nullable(string)),
4197
+ correctX: nullable(number),
4198
+ initialX: optional(nullable(number)),
4199
+ showTooltips: optional(boolean),
4200
+ static: defaulted(boolean, () => false)
4201
+ }));
4202
+
4203
+ const parseMathFormat = enumeration("integer", "mixed", "improper", "proper", "decimal", "percent", "pi");
4204
+ const parseNumericInputWidget = parseWidget(constant("numeric-input"), object({
4205
+ answers: array(object({
4206
+ message: string,
4207
+ // TODO(benchristel): value should never be null or undefined,
4208
+ // but we have some content where it is anyway. If we backfill
4209
+ // the data, simplify this.
4210
+ value: optional(nullable(number)),
4211
+ status: string,
4212
+ answerForms: optional(array(parseMathFormat)),
4213
+ strict: boolean,
4214
+ maxError: optional(nullable(number)),
4215
+ // TODO(benchristel): simplify should never be a boolean, but we
4216
+ // have some content where it is anyway. If we ever backfill
4217
+ // the data, we should simplify `simplify`.
4218
+ simplify: optional(nullable(union(string).or(pipeParsers(boolean).then(convert(String)).parser).parser))
4219
+ })),
4220
+ labelText: optional(string),
4221
+ size: string,
4222
+ coefficient: defaulted(boolean, () => false),
4223
+ rightAlign: optional(boolean),
4224
+ static: defaulted(boolean, () => false),
4225
+ answerForms: optional(array(object({
4226
+ name: parseMathFormat,
4227
+ simplify: optional(nullable(enumeration("required", "correct", "enforced", "optional")))
4228
+ })))
4229
+ }));
4230
+
4231
+ // There is an import cycle between orderer-widget.ts and perseus-renderer.ts.
4232
+ // This wrapper ensures that we don't refer to parsePerseusRenderer before
4233
+ // it's defined.
4234
+ function parseRenderer(rawValue, ctx) {
4235
+ return parsePerseusRenderer(rawValue, ctx);
4236
+ }
4237
+ const largeToAuto = (height, ctx) => {
4238
+ if (height === "large") {
4239
+ return ctx.success("auto");
4240
+ }
4241
+ return ctx.success(height);
4242
+ };
4243
+ const parseOrdererWidget = parseWidget(constant("orderer"), object({
4244
+ options: defaulted(array(parseRenderer), () => []),
4245
+ correctOptions: array(parseRenderer),
4246
+ otherOptions: array(parseRenderer),
4247
+ height: pipeParsers(enumeration("normal", "auto", "large")).then(largeToAuto).parser,
4248
+ layout: defaulted(enumeration("horizontal", "vertical"), () => "horizontal")
4249
+ }));
4250
+
4251
+ const parsePassageRefWidget = parseWidget(constant("passage-ref"), object({
4252
+ passageNumber: number,
4253
+ referenceNumber: number,
4254
+ summaryText: optional(string)
4255
+ }));
4256
+
4257
+ const parsePassageWidget = parseWidget(constant("passage"), object({
4258
+ footnotes: defaulted(string, () => ""),
4259
+ passageText: string,
4260
+ passageTitle: defaulted(string, () => ""),
4261
+ showLineNumbers: boolean,
4262
+ static: defaulted(boolean, () => false)
4263
+ }));
4264
+
4265
+ const parsePhetSimulationWidget = parseWidget(constant("phet-simulation"), object({
4266
+ url: string,
4267
+ description: string
4268
+ }));
4269
+
4270
+ const parsePlotterWidget = parseWidget(constant("plotter"), object({
4271
+ labels: array(string),
4272
+ categories: array(string),
4273
+ type: enumeration(...plotterPlotTypes),
4274
+ maxY: number,
4275
+ // The default value for scaleY comes from plotter.tsx.
4276
+ // See parse-perseus-json/README.md for why we want to duplicate the
4277
+ // defaults here.
4278
+ scaleY: defaulted(number, () => 1),
4279
+ labelInterval: optional(nullable(number)),
4280
+ // The default value for snapsPerLine comes from plotter.tsx.
4281
+ // See parse-perseus-json/README.md for why we want to duplicate the
4282
+ // defaults here.
4283
+ snapsPerLine: defaulted(number, () => 2),
4284
+ starting: array(number),
4285
+ correct: array(number),
4286
+ picUrl: optional(nullable(string)),
4287
+ picSize: optional(nullable(number)),
4288
+ picBoxHeight: optional(nullable(number)),
4289
+ // NOTE(benchristel): I copied the default plotDimensions from
4290
+ // plotter.tsx. See the parse-perseus-json/README.md for an explanation
4291
+ // of why we want to duplicate the defaults here.
4292
+ plotDimensions: defaulted(array(number), () => [380, 300])
4293
+ }));
4294
+
4295
+ const parsePythonProgramWidget = parseWidget(constant("python-program"), object({
4296
+ programID: string,
4297
+ height: number
4298
+ }));
4299
+
4300
+ const parseRadioWidget = parseWidget(constant("radio"), object({
4301
+ choices: array(object({
4302
+ content: defaulted(string, () => ""),
4303
+ clue: optional(string),
4304
+ correct: optional(boolean),
4305
+ isNoneOfTheAbove: optional(boolean),
4306
+ // deprecated
4307
+ // There is an import cycle between radio-widget.ts and
4308
+ // widgets-map.ts. The anonymous function below ensures that we
4309
+ // don't refer to parseWidgetsMap before it's defined.
4310
+ widgets: optional((rawVal, ctx) => parseWidgetsMap(rawVal, ctx))
4311
+ })),
4312
+ hasNoneOfTheAbove: optional(boolean),
4313
+ countChoices: optional(boolean),
4314
+ randomize: optional(boolean),
4315
+ multipleSelect: optional(boolean),
4316
+ deselectEnabled: optional(boolean),
4317
+ // deprecated
4318
+ onePerLine: optional(boolean),
4319
+ // deprecated
4320
+ displayCount: optional(any),
4321
+ // v0 props
4322
+ // `noneOfTheAbove` is still in use (but only set to `false`).
4323
+ noneOfTheAbove: optional(constant(false))
4324
+ }));
4325
+
4326
+ const parseSorterWidget = parseWidget(constant("sorter"), object({
4327
+ correct: array(string),
4328
+ padding: boolean,
4329
+ layout: enumeration("horizontal", "vertical")
4330
+ }));
4331
+
4332
+ const parseTableWidget = parseWidget(constant("table"), object({
4333
+ headers: array(string),
4334
+ rows: number,
4335
+ columns: number,
4336
+ answers: array(array(string))
4337
+ }));
4338
+
4339
+ const parseVideoWidget = parseWidget(constant("video"), object({
4340
+ location: string,
4341
+ static: optional(boolean)
4342
+ }));
4343
+
4344
+ const parseWidgetsMap = (rawValue, ctx) => {
4345
+ if (!isObject(rawValue)) {
4346
+ return ctx.failure("PerseusWidgetsMap", rawValue);
4347
+ }
4348
+ const widgetsMap = {};
4349
+ for (const key of Object.keys(rawValue)) {
4350
+ // parseWidgetsMapEntry modifies the widgetsMap. This is kind of gross,
4351
+ // but it's the only way I could find to make TypeScript check the key
4352
+ // against the widget type.
4353
+ const entryResult = parseWidgetsMapEntry([key, rawValue[key]], widgetsMap, ctx.forSubtree(key));
4354
+ if (isFailure(entryResult)) {
4355
+ return entryResult;
4356
+ }
4357
+ }
4358
+ return ctx.success(widgetsMap);
4359
+ };
4360
+ const parseWidgetsMapEntry = (_ref, widgetMap, ctx) => {
4361
+ let [id, widget] = _ref;
4362
+ const idComponentsResult = parseWidgetIdComponents(id.split(" "), ctx.forSubtree("(widget ID)"));
4363
+ if (isFailure(idComponentsResult)) {
4364
+ return idComponentsResult;
4365
+ }
4366
+ const [type, n] = idComponentsResult.value;
4367
+ function parseAndAssign(key, parse) {
4368
+ const widgetResult = parse(widget, ctx);
4369
+ if (isFailure(widgetResult)) {
4370
+ return widgetResult;
4371
+ }
4372
+ widgetMap[key] = widgetResult.value;
4373
+ return ctx.success(undefined);
4374
+ }
4375
+ switch (type) {
4376
+ case "categorizer":
4377
+ return parseAndAssign(`categorizer ${n}`, parseCategorizerWidget);
4378
+ case "cs-program":
4379
+ return parseAndAssign(`cs-program ${n}`, parseCSProgramWidget);
4380
+ case "definition":
4381
+ return parseAndAssign(`definition ${n}`, parseDefinitionWidget);
4382
+ case "dropdown":
4383
+ return parseAndAssign(`dropdown ${n}`, parseDropdownWidget);
4384
+ case "explanation":
4385
+ return parseAndAssign(`explanation ${n}`, parseExplanationWidget);
4386
+ case "expression":
4387
+ return parseAndAssign(`expression ${n}`, parseExpressionWidget);
4388
+ case "grapher":
4389
+ return parseAndAssign(`grapher ${n}`, parseGrapherWidget);
4390
+ case "group":
4391
+ return parseAndAssign(`group ${n}`, parseGroupWidget);
4392
+ case "graded-group":
4393
+ return parseAndAssign(`graded-group ${n}`, parseGradedGroupWidget);
4394
+ case "graded-group-set":
4395
+ return parseAndAssign(`graded-group-set ${n}`, parseGradedGroupSetWidget);
4396
+ case "iframe":
4397
+ return parseAndAssign(`iframe ${n}`, parseIframeWidget);
4398
+ case "image":
4399
+ return parseAndAssign(`image ${n}`, parseImageWidget);
4400
+ case "input-number":
4401
+ return parseAndAssign(`input-number ${n}`, parseInputNumberWidget);
4402
+ case "interaction":
4403
+ return parseAndAssign(`interaction ${n}`, parseInteractionWidget);
4404
+ case "interactive-graph":
4405
+ return parseAndAssign(`interactive-graph ${n}`, parseInteractiveGraphWidget);
4406
+ case "label-image":
4407
+ return parseAndAssign(`label-image ${n}`, parseLabelImageWidget);
4408
+ case "matcher":
4409
+ return parseAndAssign(`matcher ${n}`, parseMatcherWidget);
4410
+ case "matrix":
4411
+ return parseAndAssign(`matrix ${n}`, parseMatrixWidget);
4412
+ case "measurer":
4413
+ return parseAndAssign(`measurer ${n}`, parseMeasurerWidget);
4414
+ case "molecule-renderer":
4415
+ return parseAndAssign(`molecule-renderer ${n}`, parseMoleculeRendererWidget);
4416
+ case "number-line":
4417
+ return parseAndAssign(`number-line ${n}`, parseNumberLineWidget);
4418
+ case "numeric-input":
4419
+ return parseAndAssign(`numeric-input ${n}`, parseNumericInputWidget);
4420
+ case "orderer":
4421
+ return parseAndAssign(`orderer ${n}`, parseOrdererWidget);
4422
+ case "passage":
4423
+ return parseAndAssign(`passage ${n}`, parsePassageWidget);
4424
+ case "passage-ref":
4425
+ return parseAndAssign(`passage-ref ${n}`, parsePassageRefWidget);
4426
+ case "passage-ref-target":
4427
+ // NOTE(benchristel): as of 2024-11-12, passage-ref-target is only
4428
+ // used in test content. See:
4429
+ // https://www.khanacademy.org/devadmin/content/search?query=widget:passage-ref-target
4430
+ return parseAndAssign(`passage-ref-target ${n}`, any);
4431
+ case "phet-simulation":
4432
+ return parseAndAssign(`phet-simulation ${n}`, parsePhetSimulationWidget);
4433
+ case "plotter":
4434
+ return parseAndAssign(`plotter ${n}`, parsePlotterWidget);
4435
+ case "python-program":
4436
+ return parseAndAssign(`python-program ${n}`, parsePythonProgramWidget);
4437
+ case "radio":
4438
+ return parseAndAssign(`radio ${n}`, parseRadioWidget);
4439
+ case "sorter":
4440
+ return parseAndAssign(`sorter ${n}`, parseSorterWidget);
4441
+ case "table":
4442
+ return parseAndAssign(`table ${n}`, parseTableWidget);
4443
+ case "video":
4444
+ return parseAndAssign(`video ${n}`, parseVideoWidget);
4445
+ case "sequence":
4446
+ // sequence is a deprecated widget type, and the corresponding
4447
+ // widget component no longer exists.
4448
+ return parseAndAssign(`sequence ${n}`, parseDeprecatedWidget);
4449
+ case "lights-puzzle":
4450
+ return parseAndAssign(`lights-puzzle ${n}`, parseDeprecatedWidget);
4451
+ case "simulator":
4452
+ return parseAndAssign(`simulator ${n}`, parseDeprecatedWidget);
4453
+ case "transformer":
4454
+ return parseAndAssign(`transformer ${n}`, parseDeprecatedWidget);
4455
+ default:
4456
+ return parseAndAssign(`${type} ${n}`, parseWidget(constant(type), any));
4457
+ }
4458
+ };
4459
+ const parseDeprecatedWidget = parseWidget(
4460
+ // Ignore the incoming widget type and hardcode "deprecated-standin"
4461
+ (_, ctx) => ctx.success("deprecated-standin"),
4462
+ // Allow any widget options
4463
+ object({}));
4464
+ const parseStringToPositiveInt = (rawValue, ctx) => {
4465
+ if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) {
4466
+ return ctx.failure("a string representing a positive integer", rawValue);
4467
+ }
4468
+ return ctx.success(+rawValue);
4469
+ };
4470
+ const parseWidgetIdComponents = pair(string, parseStringToPositiveInt);
4471
+
4472
+ const parsePerseusRenderer = defaulted(object({
4473
+ // TODO(benchristel): content is also defaulted to empty string in
4474
+ // renderer.tsx. See if we can remove one default or the other.
4475
+ content: defaulted(string, () => ""),
4476
+ // This module has an import cycle with parseWidgetsMap, because the
4477
+ // `group` widget can contain another renderer.
4478
+ // The anonymous function below ensures that we don't try to access
4479
+ // parseWidgetsMap before it's defined.
4480
+ widgets: defaulted((rawVal, ctx) => parseWidgetsMap(rawVal, ctx), () => ({})),
4481
+ metadata: optional(array(string)),
4482
+ images: parseImages
4483
+ }),
4484
+ // Default value
4485
+ () => ({
4486
+ content: "",
4487
+ widgets: {},
4488
+ images: {}
4489
+ }));
4490
+
4491
+ const parsePerseusArticle = union(parsePerseusRenderer).or(array(parsePerseusRenderer)).parser;
4492
+
4493
+ const parseHint = object({
4494
+ replace: optional(boolean),
4495
+ content: string,
4496
+ widgets: defaulted(parseWidgetsMap, () => ({})),
4497
+ metadata: optional(array(string)),
4498
+ images: parseImages
4499
+ });
4500
+
4501
+ const parsePerseusItem$1 = object({
4502
+ question: parsePerseusRenderer,
4503
+ hints: defaulted(array(parseHint), () => []),
4504
+ answerArea: pipeParsers(defaulted(object({}), () => ({}))).then(migrateAnswerArea).then(record(enumeration(...ItemExtras), boolean)).parser,
4505
+ itemDataVersion: optional(object({
4506
+ major: number,
4507
+ minor: number
4508
+ })),
4509
+ // Deprecated field
4510
+ answer: any
4511
+ });
4512
+
4513
+ // Some answerAreas have extra fields, like:
4514
+ //
4515
+ // "answerArea": {
4516
+ // "type": "multiple",
4517
+ // "options": {
4518
+ // "content": "",
4519
+ // "images": {},
4520
+ // "widgets": {}
4521
+ // }
4522
+ // }
4523
+ //
4524
+ // The "type" and "options" fields don't seem to be used anywhere. This
4525
+ // migration function removes them.
4526
+ function migrateAnswerArea(rawValue, ctx) {
4527
+ const {
4528
+ type: _,
4529
+ options: __,
4530
+ ...rest
4531
+ } = rawValue;
4532
+ return ctx.success(rest);
4533
+ }
4534
+
4535
+ /**
4536
+ * Helper to parse PerseusItem JSON
4537
+ * Why not just use JSON.parse? We want:
4538
+ * - To make sure types are correct
4539
+ * - To give us a central place to validate/transform output if needed
4540
+ * @deprecated - use parseAndMigratePerseusItem instead
4541
+ * @param {string} json - the stringified PerseusItem JSON
4542
+ * @returns {PerseusItem} the parsed PerseusItem object
4543
+ */
4544
+ function parsePerseusItem(json) {
4545
+ // Try to block a cheating vector which relies on monkey-patching
4546
+ // JSON.parse
4547
+ if (isRealJSONParse(JSON.parse)) {
4548
+ return JSON.parse(json);
4549
+ }
4550
+ throw new Error("Something went wrong.");
4551
+ }
4552
+ /**
4553
+ * Parses a PerseusItem from a JSON string, migrates old formats to the latest
4554
+ * schema, and runtime-typechecks the result. Use this to parse assessmentItem
4555
+ * data.
4556
+ *
4557
+ * @returns a {@link Result} of the parsed PerseusItem. If the result is a
4558
+ * failure, it will contain an error message describing where in the tree
4559
+ * parsing failed.
4560
+ * @throws SyntaxError if the argument is not well-formed JSON.
4561
+ */
4562
+ function parseAndMigratePerseusItem(json) {
4563
+ throwErrorIfCheatingDetected();
4564
+ const object = JSON.parse(json);
4565
+ const result = parse(object, parsePerseusItem$1);
4566
+ if (isFailure(result)) {
4567
+ return failure({
4568
+ message: result.detail,
4569
+ invalidObject: object
4570
+ });
4571
+ }
4572
+ return result;
4573
+ }
4574
+
4575
+ /**
4576
+ * Parses a PerseusArticle from a JSON string, migrates old formats to the
4577
+ * latest schema, and runtime-typechecks the result.
4578
+ *
4579
+ * @returns a {@link Result} of the parsed PerseusArticle. If the result is a
4580
+ * failure, it will contain an error message describing where in the tree
4581
+ * parsing failed.
4582
+ * @throws SyntaxError if the argument is not well-formed JSON.
4583
+ */
4584
+ function parseAndMigratePerseusArticle(json) {
4585
+ throwErrorIfCheatingDetected();
4586
+ const object = JSON.parse(json);
4587
+ const result = parse(object, parsePerseusArticle);
4588
+ if (isFailure(result)) {
4589
+ return failure({
4590
+ message: result.detail,
4591
+ invalidObject: object
4592
+ });
4593
+ }
4594
+ return result;
4595
+ }
4596
+
4597
+ /**
4598
+ * Tries to block a cheating vector that relies on monkey-patching JSON.parse.
4599
+ */
4600
+ // TODO(LEMS-2331): delete this function once server-side scoring is done.
4601
+ function throwErrorIfCheatingDetected() {
4602
+ if (!isRealJSONParse(JSON.parse)) {
4603
+ throw new Error("Something went wrong.");
4604
+ }
4605
+ }
4606
+
4607
+ // This file is processed by a Rollup plugin (replace) to inject the production
4608
+ const libName = "@khanacademy/perseus-core";
4609
+ const libVersion = "3.4.0";
4610
+ addLibraryVersionToPerseusDebug(libName, libVersion);
4611
+
4612
+ /**
4613
+ * @typedef {Object} Errors utility for referencing the Perseus error taxonomy.
4614
+ */
4615
+ const Errors = Object.freeze({
4616
+ /**
4617
+ * @property {ErrorKind} Unknown The kind of error is not known.
4618
+ */
4619
+ Unknown: "Unknown",
4620
+ /**
4621
+ * @property {ErrorKind} Internal The error is internal to the executing code.
4622
+ */
4623
+ Internal: "Internal",
4624
+ /**
4625
+ * @property {ErrorKind} InvalidInput There was a problem with the provided
4626
+ * input, such as the wrong format or a null value.
4627
+ */
4628
+ InvalidInput: "InvalidInput",
4629
+ /**
4630
+ * @property {ErrorKind} NotAllowed There was a problem due to the state of
4631
+ * the system not matching the requested operation or input. For example,
4632
+ * trying to create a username that is valid, but is already taken by
4633
+ * another user. Use {@link InvalidInput} instead when the input isn't
4634
+ * valid regardless of the state of the system. Use {@link NotFound} when
4635
+ * the failure is due to not being able to find a resource.
4636
+ */
4637
+ NotAllowed: "NotAllowed",
4638
+ /**
4639
+ * @property {ErrorKind} TransientService There was a problem when making a
4640
+ * request to a service.
4641
+ */
4642
+ TransientService: "TransientService",
4643
+ /**
4644
+ * @property {ErrorKind} Service There was a non-transient problem when
4645
+ * making a request to service.
4646
+ */
4647
+ Service: "Service"
4648
+ });
4649
+
4650
+ /**
4651
+ * @type {ErrorKind} The kind of error being reported
4652
+ */
4653
+
4654
+ class PerseusError extends Error {
4655
+ kind;
4656
+ metadata;
4657
+ constructor(message, kind, options) {
4658
+ super(message);
4659
+ this.kind = kind;
4660
+ this.metadata = options?.metadata;
4661
+ }
4662
+ }
4663
+
4664
+ /**
4665
+ * _ utilities for objects
4666
+ */
4667
+
4668
+ /**
4669
+ * Does a pluck on keys inside objects in an object
4670
+ *
4671
+ * Ex:
4672
+ * tools = {
4673
+ * translation: {
4674
+ * enabled: true
4675
+ * },
4676
+ * rotation: {
4677
+ * enabled: false
4678
+ * }
4679
+ * };
4680
+ * pluckObject(tools, "enabled") returns {
4681
+ * translation: true
4682
+ * rotation: false
4683
+ * }
4684
+ */
4685
+ const pluck = function (table, subKey) {
4686
+ return _.object(_.map(table, function (value, key) {
4687
+ return [key, value[subKey]];
4688
+ }));
4689
+ };
4690
+
4691
+ /**
4692
+ * Maps an object to an object
4693
+ *
4694
+ * > mapObject({a: '1', b: '2'}, (value, key) => {
4695
+ * return value + 1;
4696
+ * });
4697
+ * {a: 2, b: 3}
4698
+ */
4699
+ const mapObject = function (obj, lambda) {
4700
+ const result = {};
4701
+ Object.keys(obj).forEach(key => {
4702
+ // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
4703
+ result[key] = lambda(obj[key], key);
4704
+ });
4705
+ return result;
4706
+ };
4707
+
4708
+ const defaultWidgetOptions$v = {
4709
+ items: [],
4710
+ categories: [],
4711
+ values: [],
4712
+ randomizeItems: false
4713
+ };
4714
+ const categorizerWidgetLogic = {
4715
+ name: "categorizer",
4716
+ defaultWidgetOptions: defaultWidgetOptions$v
4717
+ };
4718
+
4719
+ const DEFAULT_HEIGHT = 400;
4720
+ const defaultWidgetOptions$u = {
4721
+ programID: "",
4722
+ programType: null,
4723
+ settings: [{
4724
+ name: "",
4725
+ value: ""
4726
+ }],
4727
+ showEditor: false,
4728
+ showButtons: false,
4729
+ height: DEFAULT_HEIGHT
4730
+ };
4731
+ const csProgramWidgetLogic = {
4732
+ name: "cs-program",
4733
+ defaultWidgetOptions: defaultWidgetOptions$u,
4734
+ supportedAlignments: ["block", "full-width"]
4735
+ };
4736
+
4737
+ const defaultWidgetOptions$t = {
4738
+ togglePrompt: "",
4739
+ definition: ""
4740
+ };
4741
+ const definitionWidgetLogic = {
4742
+ name: "definition",
4743
+ defaultWidgetOptions: defaultWidgetOptions$t,
4744
+ defaultAlignment: "inline"
4745
+ };
4746
+
4747
+ const defaultWidgetOptions$s = {
4748
+ placeholder: "",
4749
+ choices: [{
4750
+ content: "",
4751
+ correct: false
4752
+ }]
4753
+ };
4754
+ const dropdownWidgetLogic = {
4755
+ name: "definition",
4756
+ defaultWidgetOptions: defaultWidgetOptions$s,
4757
+ defaultAlignment: "inline-block"
4758
+ };
4759
+
4760
+ const defaultWidgetOptions$r = {
4761
+ showPrompt: "Explain",
4762
+ hidePrompt: "Hide explanation",
4763
+ explanation: "explanation goes here\n\nmore explanation",
4764
+ widgets: {}
4765
+ };
4766
+ const explanationWidgetLogic = {
4767
+ name: "explanation",
4768
+ defaultWidgetOptions: defaultWidgetOptions$r,
4769
+ defaultAlignment: "inline"
4770
+ };
4771
+
4772
+ const currentVersion$3 = {
4773
+ major: 1,
4774
+ minor: 0
4775
+ };
4776
+ const widgetOptionsUpgrades$2 = {
4777
+ "1": v0options => ({
4778
+ times: v0options.times,
4779
+ buttonSets: v0options.buttonSets,
4780
+ functions: v0options.functions,
4781
+ buttonsVisible: v0options.buttonsVisible,
4782
+ visibleLabel: v0options.visibleLabel,
4783
+ ariaLabel: v0options.ariaLabel,
4784
+ answerForms: [{
4785
+ considered: "correct",
4786
+ form: v0options.form,
4787
+ simplify: v0options.simplify,
4788
+ value: v0options.value
4789
+ }]
4790
+ })
4791
+ };
4792
+ const defaultWidgetOptions$q = {
4793
+ answerForms: [],
4794
+ times: false,
4795
+ buttonSets: ["basic"],
4796
+ functions: ["f", "g", "h"]
4797
+ };
4798
+
4799
+ const expressionWidgetLogic = {
4800
+ name: "expression",
4801
+ version: currentVersion$3,
4802
+ widgetOptionsUpgrades: widgetOptionsUpgrades$2,
4803
+ defaultWidgetOptions: defaultWidgetOptions$q,
4804
+ defaultAlignment: "inline-block"
4805
+ };
2781
4806
 
2782
- // prettier-ignore
4807
+ const defaultWidgetOptions$p = {
4808
+ title: "",
4809
+ content: "",
4810
+ widgets: {},
4811
+ images: {},
4812
+ hint: null
4813
+ };
4814
+ const gradedGroupWidgetLogic = {
4815
+ name: "graded-group",
4816
+ defaultWidgetOptions: defaultWidgetOptions$p
4817
+ };
2783
4818
 
2784
- // prettier-ignore
4819
+ const defaultWidgetOptions$o = {
4820
+ gradedGroups: []
4821
+ };
4822
+ const gradedGroupSetWidgetLogic = {
4823
+ name: "graded-group-set",
4824
+ defaultWidgetOptions: defaultWidgetOptions$o
4825
+ };
2785
4826
 
2786
- // prettier-ignore
4827
+ const defaultWidgetOptions$n = {
4828
+ graph: {
4829
+ labels: ["x", "y"],
4830
+ range: [[-10, 10], [-10, 10]],
4831
+ step: [1, 1],
4832
+ backgroundImage: {
4833
+ url: null
4834
+ },
4835
+ markings: "graph",
4836
+ rulerLabel: "",
4837
+ rulerTicks: 10,
4838
+ valid: true,
4839
+ showTooltips: false
4840
+ },
4841
+ correct: {
4842
+ type: "linear",
4843
+ coords: null
4844
+ },
4845
+ availableTypes: ["linear"]
4846
+ };
4847
+ const grapherWidgetLogic = {
4848
+ name: "grapher",
4849
+ defaultWidgetOptions: defaultWidgetOptions$n
4850
+ };
2787
4851
 
2788
- // prettier-ignore
4852
+ const defaultWidgetOptions$m = {
4853
+ content: "",
4854
+ widgets: {},
4855
+ images: {},
4856
+ // `undefined` instead of `null` so that getDefaultProps works for
4857
+ // `the GroupMetadataEditor`
4858
+ metadata: undefined
4859
+ };
4860
+ const groupWidgetLogic = {
4861
+ name: "group",
4862
+ defaultWidgetOptions: defaultWidgetOptions$m
4863
+ };
2789
4864
 
2790
- // prettier-ignore
4865
+ const defaultWidgetOptions$l = {
4866
+ url: "",
4867
+ settings: [{
4868
+ name: "",
4869
+ value: ""
4870
+ }],
4871
+ width: "400",
4872
+ height: "400",
4873
+ allowFullScreen: false,
4874
+ allowTopNavigation: false
4875
+ };
4876
+ const iframeWidgetLogic = {
4877
+ name: "iframe",
4878
+ defaultWidgetOptions: defaultWidgetOptions$l
4879
+ };
2791
4880
 
2792
- // prettier-ignore
4881
+ const defaultWidgetOptions$k = {
4882
+ title: "",
4883
+ range: [[0, 10], [0, 10]],
4884
+ box: [400, 400],
4885
+ backgroundImage: {
4886
+ url: null,
4887
+ width: 0,
4888
+ height: 0
4889
+ },
4890
+ labels: [],
4891
+ alt: "",
4892
+ caption: ""
4893
+ };
4894
+ const imageWidgetLogic = {
4895
+ name: "image",
4896
+ defaultWidgetOptions: defaultWidgetOptions$k,
4897
+ supportedAlignments: ["block", "full-width"],
4898
+ defaultAlignment: "block"
4899
+ };
2793
4900
 
2794
- // prettier-ignore
4901
+ const defaultWidgetOptions$j = {
4902
+ value: 0,
4903
+ simplify: "required",
4904
+ size: "normal",
4905
+ inexact: false,
4906
+ maxError: 0.1,
4907
+ answerType: "number",
4908
+ rightAlign: false
4909
+ };
4910
+ const inputNumberWidgetLogic = {
4911
+ name: "input-number",
4912
+ defaultWidgetOptions: defaultWidgetOptions$j,
4913
+ defaultAlignment: "inline-block"
4914
+ };
2795
4915
 
2796
- // prettier-ignore
4916
+ const defaultWidgetOptions$i = {
4917
+ graph: {
4918
+ box: [400, 400],
4919
+ labels: ["x", "y"],
4920
+ range: [[-10, 10], [-10, 10]],
4921
+ tickStep: [1, 1],
4922
+ gridStep: [1, 1],
4923
+ markings: "graph"
4924
+ },
4925
+ elements: []
4926
+ };
4927
+ const interactionWidgetLogic = {
4928
+ name: "interaction",
4929
+ defaultWidgetOptions: defaultWidgetOptions$i
4930
+ };
2797
4931
 
2798
- // prettier-ignore
4932
+ const defaultWidgetOptions$h = {
4933
+ labels: ["x", "y"],
4934
+ range: [[-10, 10], [-10, 10]],
4935
+ step: [1, 1],
4936
+ backgroundImage: {
4937
+ url: null
4938
+ },
4939
+ markings: "graph",
4940
+ showTooltips: false,
4941
+ showProtractor: false,
4942
+ graph: {
4943
+ type: "linear"
4944
+ },
4945
+ correct: {
4946
+ type: "linear",
4947
+ coords: null
4948
+ }
4949
+ };
4950
+ const interactiveGraphWidgetLogic = {
4951
+ name: "interactive-graph",
4952
+ defaultWidgetOptions: defaultWidgetOptions$h
4953
+ };
2799
4954
 
2800
- // prettier-ignore
4955
+ const defaultWidgetOptions$g = {
4956
+ choices: [],
4957
+ imageAlt: "",
4958
+ imageUrl: "",
4959
+ imageWidth: 0,
4960
+ imageHeight: 0,
4961
+ markers: [],
4962
+ multipleAnswers: false,
4963
+ hideChoicesFromInstructions: false
4964
+ };
4965
+ const labelImageWidgetLogic = {
4966
+ name: "label-image",
4967
+ defaultWidgetOptions: defaultWidgetOptions$g
4968
+ };
2801
4969
 
2802
- // prettier-ignore
4970
+ const defaultWidgetOptions$f = {
4971
+ left: ["$x$", "$y$", "$z$"],
4972
+ right: ["$1$", "$2$", "$3$"],
4973
+ labels: ["test", "label"],
4974
+ orderMatters: false,
4975
+ padding: true
4976
+ };
4977
+ const matcherWidgetLogic = {
4978
+ name: "matcher",
4979
+ defaultWidgetOptions: defaultWidgetOptions$f
4980
+ };
2803
4981
 
2804
- // prettier-ignore
4982
+ const defaultWidgetOptions$e = {
4983
+ matrixBoardSize: [3, 3],
4984
+ answers: [[]],
4985
+ prefix: "",
4986
+ suffix: "",
4987
+ cursorPosition: [0, 0]
4988
+ };
4989
+ const matrixWidgetLogic = {
4990
+ name: "matrix",
4991
+ defaultWidgetOptions: defaultWidgetOptions$e
4992
+ };
2805
4993
 
2806
- // prettier-ignore
4994
+ const currentVersion$2 = {
4995
+ major: 1,
4996
+ minor: 0
4997
+ };
4998
+ const widgetOptionsUpgrades$1 = {
4999
+ "1": v0options => {
5000
+ const {
5001
+ imageUrl,
5002
+ imageTop,
5003
+ imageLeft,
5004
+ ...rest
5005
+ } = v0options;
5006
+ return {
5007
+ ...rest,
5008
+ image: {
5009
+ url: imageUrl,
5010
+ top: imageTop,
5011
+ left: imageLeft
5012
+ }
5013
+ };
5014
+ }
5015
+ };
5016
+ const defaultWidgetOptions$d = {
5017
+ box: [480, 480],
5018
+ image: {},
5019
+ showProtractor: true,
5020
+ showRuler: false,
5021
+ rulerLabel: "",
5022
+ rulerTicks: 10,
5023
+ rulerPixels: 40,
5024
+ rulerLength: 10
5025
+ };
2807
5026
 
2808
- // prettier-ignore
5027
+ const measurerWidgetLogic = {
5028
+ name: "measurer",
5029
+ version: currentVersion$2,
5030
+ widgetOptionsUpgrades: widgetOptionsUpgrades$1,
5031
+ defaultWidgetOptions: defaultWidgetOptions$d
5032
+ };
2809
5033
 
2810
- // prettier-ignore
5034
+ const defaultWidgetOptions$c = {
5035
+ range: [0, 10],
5036
+ labelRange: [null, null],
5037
+ labelStyle: "decimal",
5038
+ labelTicks: true,
5039
+ divisionRange: [1, 12],
5040
+ numDivisions: 5,
5041
+ snapDivisions: 2,
5042
+ tickStep: null,
5043
+ correctRel: "eq",
5044
+ correctX: null,
5045
+ initialX: null,
5046
+ showTooltips: false
5047
+ };
5048
+ const numberLineWidgetLogic = {
5049
+ name: "number-line",
5050
+ defaultWidgetOptions: defaultWidgetOptions$c
5051
+ };
2811
5052
 
2812
- // prettier-ignore
5053
+ const defaultWidgetOptions$b = {
5054
+ answers: [{
5055
+ value: null,
5056
+ status: "correct",
5057
+ message: "",
5058
+ simplify: "required",
5059
+ answerForms: [],
5060
+ strict: false,
5061
+ maxError: null
5062
+ }],
5063
+ size: "normal",
5064
+ coefficient: false,
5065
+ labelText: "",
5066
+ rightAlign: false
5067
+ };
5068
+ const numericInputWidgetLogic = {
5069
+ name: "numeric-input",
5070
+ defaultWidgetOptions: defaultWidgetOptions$b,
5071
+ defaultAlignment: "inline-block"
5072
+ };
2813
5073
 
2814
- // prettier-ignore
5074
+ const defaultWidgetOptions$a = {
5075
+ correctOptions: [{
5076
+ content: "$x$"
5077
+ }],
5078
+ otherOptions: [{
5079
+ content: "$y$"
5080
+ }],
5081
+ height: "normal",
5082
+ layout: "horizontal"
5083
+ };
5084
+ const ordererWidgetLogic = {
5085
+ name: "orderer",
5086
+ defaultWidgetOptions: defaultWidgetOptions$a
5087
+ };
2815
5088
 
2816
- // prettier-ignore
5089
+ const defaultWidgetOptions$9 = {
5090
+ passageTitle: "",
5091
+ passageText: "",
5092
+ footnotes: "",
5093
+ showLineNumbers: true
5094
+ };
5095
+ const passageWidgetLogic = {
5096
+ name: "passage",
5097
+ defaultWidgetOptions: defaultWidgetOptions$9
5098
+ };
2817
5099
 
2818
- // prettier-ignore
5100
+ const currentVersion$1 = {
5101
+ major: 0,
5102
+ minor: 1
5103
+ };
5104
+ const defaultWidgetOptions$8 = {
5105
+ passageNumber: 1,
5106
+ referenceNumber: 1,
5107
+ summaryText: ""
5108
+ };
2819
5109
 
2820
- // prettier-ignore
5110
+ const passageRefWidgetLogic = {
5111
+ name: "passageRef",
5112
+ version: currentVersion$1,
5113
+ defaultWidgetOptions: defaultWidgetOptions$8,
5114
+ defaultAlignment: "inline"
5115
+ };
2821
5116
 
2822
- // prettier-ignore
5117
+ const defaultWidgetOptions$7 = {
5118
+ content: ""
5119
+ };
5120
+ const passageRefTargetWidgetLogic = {
5121
+ name: "passageRefTarget",
5122
+ defaultWidgetOptions: defaultWidgetOptions$7,
5123
+ defaultAlignment: "inline"
5124
+ };
2823
5125
 
2824
- // prettier-ignore
5126
+ const defaultWidgetOptions$6 = {
5127
+ url: "",
5128
+ description: ""
5129
+ };
5130
+ const phetSimulationWidgetLogic = {
5131
+ name: "phet-simulation",
5132
+ defaultWidgetOptions: defaultWidgetOptions$6
5133
+ };
2825
5134
 
2826
- // prettier-ignore
5135
+ const defaultWidgetOptions$5 = {
5136
+ scaleY: 1,
5137
+ maxY: 10,
5138
+ snapsPerLine: 2,
5139
+ correct: [1],
5140
+ starting: [1],
5141
+ type: "bar",
5142
+ labels: ["", ""],
5143
+ categories: [""],
5144
+ picSize: 30,
5145
+ picBoxHeight: 36,
5146
+ plotDimensions: [275, 200],
5147
+ labelInterval: 1,
5148
+ picUrl: null
5149
+ };
5150
+ const plotterWidgetLogic = {
5151
+ name: "plotter",
5152
+ defaultWidgetOptions: defaultWidgetOptions$5
5153
+ };
2827
5154
 
2828
- // prettier-ignore
5155
+ const defaultWidgetOptions$4 = {
5156
+ programID: "",
5157
+ height: 400
5158
+ };
5159
+ const pythonProgramWidgetLogic = {
5160
+ name: "python-program",
5161
+ defaultWidgetOptions: defaultWidgetOptions$4
5162
+ };
2829
5163
 
2830
- // prettier-ignore
5164
+ const currentVersion = {
5165
+ major: 1,
5166
+ minor: 0
5167
+ };
5168
+ const widgetOptionsUpgrades = {
5169
+ "1": v0props => {
5170
+ const {
5171
+ noneOfTheAbove,
5172
+ ...rest
5173
+ } = v0props;
5174
+ if (noneOfTheAbove) {
5175
+ throw new Error("radio widget v0 no longer supports auto noneOfTheAbove");
5176
+ }
5177
+ return {
5178
+ ...rest,
5179
+ hasNoneOfTheAbove: false
5180
+ };
5181
+ }
5182
+ };
5183
+ const defaultWidgetOptions$3 = {
5184
+ choices: [{}, {}, {}, {}],
5185
+ displayCount: null,
5186
+ randomize: false,
5187
+ hasNoneOfTheAbove: false,
5188
+ multipleSelect: false,
5189
+ countChoices: false,
5190
+ deselectEnabled: false
5191
+ };
2831
5192
 
2832
- // prettier-ignore
5193
+ const radioWidgetLogic = {
5194
+ name: "radio",
5195
+ version: currentVersion,
5196
+ widgetOptionsUpgrades: widgetOptionsUpgrades,
5197
+ defaultWidgetOptions: defaultWidgetOptions$3
5198
+ };
2833
5199
 
2834
- // prettier-ignore
5200
+ const defaultWidgetOptions$2 = {
5201
+ correct: ["$x$", "$y$", "$z$"],
5202
+ layout: "horizontal",
5203
+ padding: true
5204
+ };
5205
+ const sorterWidgetLogic = {
5206
+ name: "sorter",
5207
+ defaultWidgetOptions: defaultWidgetOptions$2
5208
+ };
2835
5209
 
2836
- //prettier-ignore
5210
+ const defaultRows = 4;
5211
+ const defaultColumns = 1;
5212
+
5213
+ // initialize a 2D array
5214
+ // (defaultRows x defaultColumns) of empty strings
5215
+ const answers = new Array(defaultRows).fill(0).map(() => new Array(defaultColumns).fill(""));
5216
+ const defaultWidgetOptions$1 = {
5217
+ headers: [""],
5218
+ rows: defaultRows,
5219
+ columns: defaultColumns,
5220
+ answers: answers
5221
+ };
5222
+ const tableWidgetLogic = {
5223
+ name: "table",
5224
+ defaultWidgetOptions: defaultWidgetOptions$1
5225
+ };
5226
+
5227
+ const defaultWidgetOptions = {
5228
+ location: ""
5229
+ };
5230
+ const videoWidgetLogic = {
5231
+ name: "video",
5232
+ defaultWidgetOptions,
5233
+ supportedAlignments: ["block", "float-left", "float-right", "full-width"],
5234
+ defaultAlignment: "block"
5235
+ };
5236
+
5237
+ const widgets = {};
5238
+ function registerWidget(type, logic) {
5239
+ widgets[type] = logic;
5240
+ }
5241
+ function isWidgetRegistered(type) {
5242
+ const widgetLogic = widgets[type];
5243
+ return !!widgetLogic;
5244
+ }
5245
+ function getCurrentVersion(type) {
5246
+ const widgetLogic = widgets[type];
5247
+ return widgetLogic?.version || {
5248
+ major: 0,
5249
+ minor: 0
5250
+ };
5251
+ }
5252
+ function getWidgetOptionsUpgrades(type) {
5253
+ const widgetLogic = widgets[type];
5254
+ return widgetLogic?.widgetOptionsUpgrades || {};
5255
+ }
5256
+ function getDefaultWidgetOptions(type) {
5257
+ const widgetLogic = widgets[type];
5258
+ return widgetLogic?.defaultWidgetOptions || {};
5259
+ }
2837
5260
 
2838
5261
  /**
2839
- * A background image applied to various widgets.
5262
+ * Handling for the optional alignments for widgets
5263
+ * See widget-container.jsx for details on how alignments are implemented.
2840
5264
  */
2841
5265
 
2842
5266
  /**
2843
- * The type of markings to display on the graph.
2844
- * - axes: shows the axes without the gride lines
2845
- * - graph: shows the axes and the grid lines
2846
- * - grid: shows only the grid lines
2847
- * - none: shows no markings
5267
+ * Returns the list of supported alignments for the given (string) widget
5268
+ * type. This is used primarily at editing time to display the choices
5269
+ * for the user.
5270
+ *
5271
+ * Supported alignments are given as an array of strings in the exports of
5272
+ * a widget's module.
2848
5273
  */
5274
+ const getSupportedAlignments = type => {
5275
+ const widgetLogic = widgets[type];
5276
+ if (!widgetLogic?.supportedAlignments?.[0]) {
5277
+ // default alignments
5278
+ return ["default"];
5279
+ }
5280
+ return widgetLogic?.supportedAlignments;
5281
+ };
2849
5282
 
2850
- const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
5283
+ /**
5284
+ * For the given (string) widget type, determine the default alignment for
5285
+ * the widget. This is used at rendering time to go from "default" alignment
5286
+ * to the actual alignment displayed on the screen.
5287
+ *
5288
+ * The default alignment is given either as a string (called
5289
+ * `defaultAlignment`) or a function (called `getDefaultAlignment`) on
5290
+ * the exports of a widget's module.
5291
+ */
5292
+ const getDefaultAlignment = type => {
5293
+ const widgetLogic = widgets[type];
5294
+ if (!widgetLogic?.defaultAlignment) {
5295
+ return "block";
5296
+ }
5297
+ return widgetLogic.defaultAlignment;
5298
+ };
5299
+ registerWidget("categorizer", categorizerWidgetLogic);
5300
+ registerWidget("cs-program", csProgramWidgetLogic);
5301
+ registerWidget("definition", definitionWidgetLogic);
5302
+ registerWidget("dropdown", dropdownWidgetLogic);
5303
+ registerWidget("explanation", explanationWidgetLogic);
5304
+ registerWidget("expression", expressionWidgetLogic);
5305
+ registerWidget("graded-group", gradedGroupWidgetLogic);
5306
+ registerWidget("graded-group-set", gradedGroupSetWidgetLogic);
5307
+ registerWidget("grapher", grapherWidgetLogic);
5308
+ registerWidget("group", groupWidgetLogic);
5309
+ registerWidget("iframe", iframeWidgetLogic);
5310
+ registerWidget("image", imageWidgetLogic);
5311
+ registerWidget("input-number", inputNumberWidgetLogic);
5312
+ registerWidget("interaction", interactionWidgetLogic);
5313
+ registerWidget("interactive-graph", interactiveGraphWidgetLogic);
5314
+ registerWidget("label-image", labelImageWidgetLogic);
5315
+ registerWidget("matcher", matcherWidgetLogic);
5316
+ registerWidget("matrix", matrixWidgetLogic);
5317
+ registerWidget("measurer", measurerWidgetLogic);
5318
+ registerWidget("number-line", numberLineWidgetLogic);
5319
+ registerWidget("numeric-input", numericInputWidgetLogic);
5320
+ registerWidget("orderer", ordererWidgetLogic);
5321
+ registerWidget("passage", passageWidgetLogic);
5322
+ registerWidget("passage-ref", passageRefWidgetLogic);
5323
+ registerWidget("passage-ref-target", passageRefTargetWidgetLogic);
5324
+ registerWidget("phet-simulation", phetSimulationWidgetLogic);
5325
+ registerWidget("plotter", plotterWidgetLogic);
5326
+ registerWidget("python-program", pythonProgramWidgetLogic);
5327
+ registerWidget("radio", radioWidgetLogic);
5328
+ registerWidget("sorter", sorterWidgetLogic);
5329
+ registerWidget("table", tableWidgetLogic);
5330
+ registerWidget("video", videoWidgetLogic);
5331
+
5332
+ var coreWidgetRegistry = /*#__PURE__*/Object.freeze({
5333
+ __proto__: null,
5334
+ isWidgetRegistered: isWidgetRegistered,
5335
+ getCurrentVersion: getCurrentVersion,
5336
+ getWidgetOptionsUpgrades: getWidgetOptionsUpgrades,
5337
+ getDefaultWidgetOptions: getDefaultWidgetOptions,
5338
+ getSupportedAlignments: getSupportedAlignments,
5339
+ getDefaultAlignment: getDefaultAlignment
5340
+ });
2851
5341
 
2852
- // 2D range: xMin, xMax, yMin, yMax
5342
+ const DEFAULT_STATIC = false;
5343
+ const upgradeWidgetInfoToLatestVersion = oldWidgetInfo => {
5344
+ const type = oldWidgetInfo.type;
5345
+ // NOTE(jeremy): This looks like it could be replaced by fixing types so
5346
+ // that `type` is non-optional. But we're seeing this in Sentry today so I
5347
+ // suspect we have legacy data (potentially unpublished) and we should
5348
+ // figure that out before depending solely on types.
5349
+ if (!_.isString(type)) {
5350
+ throw new PerseusError("widget type must be a string, but was: " + type, Errors.Internal);
5351
+ }
5352
+ if (!isWidgetRegistered(type)) {
5353
+ // If we have a widget that isn't registered, we can't upgrade it
5354
+ // TODO(aria): Figure out what the best thing to do here would be
5355
+ return oldWidgetInfo;
5356
+ }
2853
5357
 
2854
- const lockedFigureColorNames = ["blue", "green", "grayH", "purple", "pink", "orange", "red"];
2855
- const lockedFigureColors = {
2856
- blue: "#3D7586",
2857
- green: "#447A53",
2858
- grayH: "#3B3D45",
2859
- purple: "#594094",
2860
- pink: "#B25071",
2861
- red: "#D92916",
2862
- orange: "#946700"
2863
- };
2864
- const lockedFigureFillStyles = {
2865
- none: 0,
2866
- white: 1,
2867
- translucent: 0.4,
2868
- solid: 1
5358
+ // Unversioned widgets (pre-July 2014) are all implicitly 0.0
5359
+ const initialVersion = oldWidgetInfo.version || {
5360
+ major: 0,
5361
+ minor: 0
5362
+ };
5363
+ const latestVersion = getCurrentVersion(type);
5364
+
5365
+ // If the widget version is later than what we understand (major
5366
+ // version is higher than latest, or major versions are equal and minor
5367
+ // version is higher than latest), don't perform any upgrades.
5368
+ if (initialVersion.major > latestVersion.major || initialVersion.major === latestVersion.major && initialVersion.minor > latestVersion.minor) {
5369
+ return oldWidgetInfo;
5370
+ }
5371
+
5372
+ // We do a clone here so that it's safe to mutate the input parameter
5373
+ // in propUpgrades functions (which I will probably accidentally do at
5374
+ // some point, and we would like to not break when that happens).
5375
+ let newEditorOptions = _.clone(oldWidgetInfo.options) || {};
5376
+ const upgradePropsMap = getWidgetOptionsUpgrades(type);
5377
+
5378
+ // Empty props usually mean a newly created widget by the editor,
5379
+ // and are always considerered up-to-date.
5380
+ // Mostly, we'd rather not run upgrade functions on props that are
5381
+ // not complete.
5382
+ if (_.keys(newEditorOptions).length !== 0) {
5383
+ // We loop through all the versions after the current version of
5384
+ // the loaded widget, up to and including the latest version of the
5385
+ // loaded widget, and run the upgrade function to bring our loaded
5386
+ // widget's props up to that version.
5387
+ // There is a little subtlety here in that we call
5388
+ // upgradePropsMap[1] to upgrade *to* version 1,
5389
+ // (not from version 1).
5390
+ for (let nextVersion = initialVersion.major + 1; nextVersion <= latestVersion.major; nextVersion++) {
5391
+ if (upgradePropsMap[String(nextVersion)]) {
5392
+ newEditorOptions = upgradePropsMap[String(nextVersion)](newEditorOptions);
5393
+ } else {
5394
+ throw new PerseusError("No upgrade found for widget. Cannot render.", Errors.Internal, {
5395
+ metadata: {
5396
+ type,
5397
+ fromMajorVersion: nextVersion - 1,
5398
+ toMajorVersion: nextVersion,
5399
+ oldWidgetInfo: JSON.stringify(oldWidgetInfo)
5400
+ }
5401
+ });
5402
+ }
5403
+ }
5404
+ }
5405
+
5406
+ // Minor version upgrades (eg. new optional props) don't have
5407
+ // transform functions. Instead, we fill in the new props with their
5408
+ // defaults.
5409
+ const defaultOptions = getDefaultWidgetOptions(type);
5410
+ newEditorOptions = {
5411
+ ...defaultOptions,
5412
+ ...newEditorOptions
5413
+ };
5414
+ let alignment = oldWidgetInfo.alignment;
5415
+
5416
+ // Widgets that support multiple alignments will "lock in" the
5417
+ // alignment to the alignment that would be listed first in the
5418
+ // select box. If the widget only supports one alignment, the
5419
+ // alignment value will likely just end up as "default".
5420
+ if (alignment == null || alignment === "default") {
5421
+ alignment = getSupportedAlignments(type)?.[0];
5422
+ if (!alignment) {
5423
+ throw new PerseusError("No default alignment found when upgrading widget", Errors.Internal, {
5424
+ metadata: {
5425
+ widgetType: type
5426
+ }
5427
+ });
5428
+ }
5429
+ }
5430
+ let widgetStatic = oldWidgetInfo.static;
5431
+ if (widgetStatic == null) {
5432
+ widgetStatic = DEFAULT_STATIC;
5433
+ }
5434
+ return {
5435
+ ...oldWidgetInfo,
5436
+ // maintain other info, like type
5437
+ // After upgrading we guarantee that the version is up-to-date
5438
+ version: latestVersion,
5439
+ // Default graded to true (so null/undefined becomes true):
5440
+ graded: oldWidgetInfo.graded != null ? oldWidgetInfo.graded : true,
5441
+ alignment: alignment,
5442
+ static: widgetStatic,
5443
+ options: newEditorOptions
5444
+ };
2869
5445
  };
5446
+ function getUpgradedWidgetOptions(oldWidgetOptions) {
5447
+ return mapObject(oldWidgetOptions, (widgetInfo, widgetId) => {
5448
+ if (!widgetInfo.type || !widgetInfo.alignment) {
5449
+ const newValues = {};
5450
+ if (!widgetInfo.type) {
5451
+ // TODO: why does widget have no type?
5452
+ // We don't want to derive type from widget ID
5453
+ // see: LEMS-1845
5454
+ newValues.type = widgetId.split(" ")[0];
5455
+ }
5456
+ if (!widgetInfo.alignment) {
5457
+ newValues.alignment = "default";
5458
+ }
5459
+ widgetInfo = {
5460
+ ...widgetInfo,
5461
+ ...newValues
5462
+ };
5463
+ }
5464
+ return upgradeWidgetInfoToLatestVersion(widgetInfo);
5465
+ });
5466
+ }
2870
5467
 
2871
- // Not associated with a specific figure
5468
+ /**
5469
+ * For details on the individual options, see the
5470
+ * PerseusOrdererWidgetOptions type
5471
+ */
2872
5472
 
2873
- const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
5473
+ /**
5474
+ * Given a PerseusOrdererWidgetOptions object, return a new object with only
5475
+ * the public options that should be exposed to the client.
5476
+ */
5477
+ function getOrdererPublicWidgetOptions(options) {
5478
+ return {
5479
+ options: options.options,
5480
+ height: options.height,
5481
+ layout: options.layout
5482
+ };
5483
+ }
2874
5484
 
2875
5485
  /**
2876
- * _ utilities for objects
5486
+ * For details on the individual options, see the
5487
+ * PerseusCategorizerWidgetOptions type
2877
5488
  */
2878
5489
 
2879
5490
  /**
2880
- * Does a pluck on keys inside objects in an object
2881
- *
2882
- * Ex:
2883
- * tools = {
2884
- * translation: {
2885
- * enabled: true
2886
- * },
2887
- * rotation: {
2888
- * enabled: false
2889
- * }
2890
- * };
2891
- * pluckObject(tools, "enabled") returns {
2892
- * translation: true
2893
- * rotation: false
2894
- * }
5491
+ * Given a PerseusCategorizerWidgetOptions object, return a new object with only
5492
+ * the public options that should be exposed to the client.
2895
5493
  */
2896
- const pluck = function (table, subKey) {
2897
- return _.object(_.map(table, function (value, key) {
2898
- return [key, value[subKey]];
2899
- }));
2900
- };
5494
+ function getCategorizerPublicWidgetOptions(options) {
5495
+ return {
5496
+ items: options.items,
5497
+ categories: options.categories,
5498
+ randomizeItems: options.randomizeItems,
5499
+ static: options.static
5500
+ };
5501
+ }
5502
+
5503
+ function getCSProgramPublicWidgetOptions(options) {
5504
+ return options;
5505
+ }
2901
5506
 
2902
5507
  /**
2903
- * Maps an object to an object
2904
- *
2905
- * > mapObject({a: '1', b: '2'}, (value, key) => {
2906
- * return value + 1;
2907
- * });
2908
- * {a: 2, b: 3}
5508
+ * For details on the individual options, see the
5509
+ * PerseusExpressionWidgetOptions type
2909
5510
  */
2910
- const mapObject = function (obj, lambda) {
2911
- const result = {};
2912
- Object.keys(obj).forEach(key => {
2913
- // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
2914
- result[key] = lambda(obj[key], key);
2915
- });
2916
- return result;
2917
- };
2918
5511
 
5512
+ /**
5513
+ * Given a PerseusExpressionWidgetOptions object, return a new object with only
5514
+ * the public options that should be exposed to the client.
5515
+ */
5516
+ function getExpressionPublicWidgetOptions(options) {
5517
+ return {
5518
+ buttonSets: options.buttonSets,
5519
+ functions: options.functions,
5520
+ times: options.times,
5521
+ visibleLabel: options.visibleLabel,
5522
+ ariaLabel: options.ariaLabel,
5523
+ buttonsVisible: options.buttonsVisible
5524
+ };
5525
+ }
5526
+
5527
+ /**
5528
+ * For details on the individual options, see the
5529
+ * PerseusLabelImageWidgetOptions type
5530
+ */
5531
+
5532
+ function getLabelImagePublicWidgetOptions(options) {
5533
+ return {
5534
+ ...options,
5535
+ markers: options.markers.map(getLabelImageMarkerPublicData)
5536
+ };
5537
+ }
5538
+ function getLabelImageMarkerPublicData(marker) {
5539
+ const {
5540
+ answers: _,
5541
+ ...publicData
5542
+ } = marker;
5543
+ return publicData;
5544
+ }
5545
+
5546
+ /**
5547
+ * For details on the individual options, see the
5548
+ * PerseusSorterWidgetOptions type
5549
+ */
5550
+
5551
+ /**
5552
+ * Given a PerseusSorterWidgetOptions object, return a new object with only
5553
+ * the public options that should be exposed to the client.
5554
+ */
5555
+ function getSorterPublicWidgetOptions(options) {
5556
+ return {
5557
+ // Note(Tamara): This does not provide correct answer information any longer.
5558
+ // To maintain compatibility with the original widget options, we are
5559
+ // keeping the key the same. Represents initial state of the cards here.
5560
+ correct: options.correct.slice().sort(),
5561
+ padding: options.padding,
5562
+ layout: options.layout
5563
+ };
5564
+ }
5565
+
5566
+ /**
5567
+ * For details on the individual options, see the
5568
+ * PerseusDropdownWidgetOptions type
5569
+ */
5570
+
5571
+ /**
5572
+ * Given a PerseusDropdownWidgetOptions object, return a new object with only
5573
+ * the public options that should be exposed to the client.
5574
+ */
5575
+ function getDropdownPublicWidgetOptions(options) {
5576
+ return {
5577
+ choices: options.choices.map(choice => ({
5578
+ content: choice.content
5579
+ })),
5580
+ placeholder: options.placeholder,
5581
+ static: options.static,
5582
+ visibleLabel: options.visibleLabel,
5583
+ ariaLabel: options.ariaLabel
5584
+ };
5585
+ }
5586
+
5587
+ /**
5588
+ * For details on the individual options, see the
5589
+ * PerseusNumericInputWidgetOptions type
5590
+ */
5591
+
5592
+ /**
5593
+ * Given a PerseusNumericInputWidgetOptions object, return a new object with only
5594
+ * the public options that should be exposed to the client.
5595
+ */
5596
+ function getNumericInputPublicWidgetOptions(options) {
5597
+ const {
5598
+ answers: _,
5599
+ ...publicWidgetOptions
5600
+ } = options;
5601
+ return publicWidgetOptions;
5602
+ }
5603
+
5604
+ function getNumberLinePublicWidgetOptions(options) {
5605
+ const {
5606
+ correctX: _,
5607
+ correctRel: __,
5608
+ ...publicOptions
5609
+ } = options;
5610
+ return publicOptions;
5611
+ }
5612
+
5613
+ exports.CoreWidgetRegistry = coreWidgetRegistry;
2919
5614
  exports.Errors = Errors;
2920
5615
  exports.GrapherUtil = grapherUtil;
2921
5616
  exports.ItemExtras = ItemExtras;
2922
5617
  exports.PerseusError = PerseusError;
2923
5618
  exports.PerseusExpressionAnswerFormConsidered = PerseusExpressionAnswerFormConsidered;
2924
5619
  exports.addLibraryVersionToPerseusDebug = addLibraryVersionToPerseusDebug;
5620
+ exports.addWidget = addWidget;
2925
5621
  exports.approximateDeepEqual = approximateDeepEqual;
2926
5622
  exports.approximateEqual = approximateEqual;
5623
+ exports.categorizerLogic = categorizerWidgetLogic;
5624
+ exports.csProgramLogic = csProgramWidgetLogic;
2927
5625
  exports.deepClone = deepClone;
5626
+ exports.definitionLogic = definitionWidgetLogic;
5627
+ exports.dropdownLogic = dropdownWidgetLogic;
5628
+ exports.explanationLogic = explanationWidgetLogic;
5629
+ exports.expressionLogic = expressionWidgetLogic;
5630
+ exports.getCSProgramPublicWidgetOptions = getCSProgramPublicWidgetOptions;
5631
+ exports.getCategorizerPublicWidgetOptions = getCategorizerPublicWidgetOptions;
2928
5632
  exports.getDecimalSeparator = getDecimalSeparator;
5633
+ exports.getDropdownPublicWidgetOptions = getDropdownPublicWidgetOptions;
5634
+ exports.getExpressionPublicWidgetOptions = getExpressionPublicWidgetOptions;
5635
+ exports.getLabelImagePublicWidgetOptions = getLabelImagePublicWidgetOptions;
2929
5636
  exports.getMatrixSize = getMatrixSize;
5637
+ exports.getNumberLinePublicWidgetOptions = getNumberLinePublicWidgetOptions;
5638
+ exports.getNumericInputPublicWidgetOptions = getNumericInputPublicWidgetOptions;
5639
+ exports.getOrdererPublicWidgetOptions = getOrdererPublicWidgetOptions;
5640
+ exports.getSorterPublicWidgetOptions = getSorterPublicWidgetOptions;
5641
+ exports.getUpgradedWidgetOptions = getUpgradedWidgetOptions;
5642
+ exports.getWidgetIdsFromContent = getWidgetIdsFromContent;
5643
+ exports.getWidgetIdsFromContentByType = getWidgetIdsFromContentByType;
5644
+ exports.gradedGroupLogic = gradedGroupWidgetLogic;
5645
+ exports.gradedGroupSetLogic = gradedGroupSetWidgetLogic;
5646
+ exports.grapherLogic = grapherWidgetLogic;
5647
+ exports.groupLogic = groupWidgetLogic;
5648
+ exports.iframeLogic = iframeWidgetLogic;
5649
+ exports.imageLogic = imageWidgetLogic;
5650
+ exports.inputNumberLogic = inputNumberWidgetLogic;
5651
+ exports.interactionLogic = interactionWidgetLogic;
5652
+ exports.interactiveGraphLogic = interactiveGraphWidgetLogic;
5653
+ exports.isFailure = isFailure;
5654
+ exports.isSuccess = isSuccess;
5655
+ exports.labelImageLogic = labelImageWidgetLogic;
2930
5656
  exports.libVersion = libVersion;
2931
5657
  exports.lockedFigureColorNames = lockedFigureColorNames;
2932
5658
  exports.lockedFigureColors = lockedFigureColors;
2933
5659
  exports.lockedFigureFillStyles = lockedFigureFillStyles;
2934
5660
  exports.mapObject = mapObject;
5661
+ exports.matcherLogic = matcherWidgetLogic;
5662
+ exports.matrixLogic = matrixWidgetLogic;
5663
+ exports.measurerLogic = measurerWidgetLogic;
5664
+ exports.numberLineLogic = numberLineWidgetLogic;
5665
+ exports.numericInputLogic = numericInputWidgetLogic;
5666
+ exports.ordererLogic = ordererWidgetLogic;
5667
+ exports.parseAndMigratePerseusArticle = parseAndMigratePerseusArticle;
5668
+ exports.parseAndMigratePerseusItem = parseAndMigratePerseusItem;
5669
+ exports.parsePerseusItem = parsePerseusItem;
5670
+ exports.passageLogic = passageWidgetLogic;
5671
+ exports.passageRefLogic = passageRefWidgetLogic;
5672
+ exports.passageRefTargetLogic = passageRefTargetWidgetLogic;
5673
+ exports.phetSimulationLogic = phetSimulationWidgetLogic;
5674
+ exports.plotterLogic = plotterWidgetLogic;
2935
5675
  exports.plotterPlotTypes = plotterPlotTypes;
2936
5676
  exports.pluck = pluck;
5677
+ exports.pythonProgramLogic = pythonProgramWidgetLogic;
5678
+ exports.radioLogic = radioWidgetLogic;
5679
+ exports.sorterLogic = sorterWidgetLogic;
5680
+ exports.tableLogic = tableWidgetLogic;
5681
+ exports.upgradeWidgetInfoToLatestVersion = upgradeWidgetInfoToLatestVersion;
5682
+ exports.videoLogic = videoWidgetLogic;
2937
5683
  //# sourceMappingURL=index.js.map