@khanacademy/perseus 19.1.4 → 19.1.6

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 (487) hide show
  1. package/dist/components/graphie.d.ts +9 -1
  2. package/dist/es/index.js +255 -122
  3. package/dist/es/index.js.map +1 -1
  4. package/dist/index.js +261 -126
  5. package/dist/index.js.map +1 -1
  6. package/dist/interactive2/interactive-util.d.ts +1 -1
  7. package/dist/interactive2/movable-point-options.d.ts +5 -4
  8. package/dist/interactive2/movable.d.ts +90 -13
  9. package/dist/interactive2/types.d.ts +6 -0
  10. package/dist/util/graphie.d.ts +5 -0
  11. package/dist/widgets/expression.d.ts +4 -4
  12. package/package.json +11 -8
  13. package/.babelrc.js +0 -8
  14. package/.eslintrc.js +0 -12
  15. package/CHANGELOG.md +0 -1422
  16. package/src/__stories__/article-renderer.stories.tsx +0 -100
  17. package/src/__stories__/hints-renderer.stories.tsx +0 -55
  18. package/src/__stories__/server-item-renderer.stories.tsx +0 -44
  19. package/src/__testdata__/article-renderer.testdata.ts +0 -212
  20. package/src/__testdata__/graphie.testdata.ts +0 -51
  21. package/src/__testdata__/renderer.testdata.ts +0 -274
  22. package/src/__testdata__/server-item-renderer.testdata.ts +0 -171
  23. package/src/__tests__/__snapshots__/perseus-markdown.test.ts.snap +0 -507
  24. package/src/__tests__/__snapshots__/renderer.test.tsx.snap +0 -525
  25. package/src/__tests__/__snapshots__/server-item-renderer.test.tsx.snap +0 -285
  26. package/src/__tests__/a11y.test.ts +0 -303
  27. package/src/__tests__/article-renderer.test.tsx +0 -130
  28. package/src/__tests__/error-boundary.test.tsx +0 -76
  29. package/src/__tests__/extract-perseus-data.test.ts +0 -1027
  30. package/src/__tests__/init.test.ts +0 -10
  31. package/src/__tests__/jipt-paragraphs.test.ts +0 -32
  32. package/src/__tests__/mock-asset-loading-widget.tsx +0 -53
  33. package/src/__tests__/mock-widget.tsx +0 -33
  34. package/src/__tests__/perseus-markdown.test.ts +0 -764
  35. package/src/__tests__/renderability.test.ts +0 -480
  36. package/src/__tests__/renderer-api.test.tsx +0 -302
  37. package/src/__tests__/renderer.test.tsx +0 -1816
  38. package/src/__tests__/server-item-renderer.test.tsx +0 -582
  39. package/src/__tests__/test-items/image-item.ts +0 -24
  40. package/src/__tests__/test-items/input-number-1-item.ts +0 -26
  41. package/src/__tests__/test-items/input-number-2-item.ts +0 -38
  42. package/src/__tests__/test-items/table-item.ts +0 -29
  43. package/src/__tests__/tex-wrangler.test.ts +0 -82
  44. package/src/__tests__/util.test.ts +0 -40
  45. package/src/__tests__/widget-container.test.tsx +0 -126
  46. package/src/__tests__/widgets.test.ts +0 -91
  47. package/src/a11y.ts +0 -45
  48. package/src/all-widgets.ts +0 -9
  49. package/src/article-renderer.tsx +0 -237
  50. package/src/asset-context.ts +0 -30
  51. package/src/basic-widgets.ts +0 -16
  52. package/src/components/__stories__/button-group.stories.tsx +0 -55
  53. package/src/components/__stories__/fixed-to-responsive.stories.tsx +0 -141
  54. package/src/components/__stories__/graph.stories.tsx +0 -28
  55. package/src/components/__stories__/graphie.stories.tsx +0 -33
  56. package/src/components/__stories__/hud.stories.tsx +0 -35
  57. package/src/components/__stories__/icon.stories.tsx +0 -63
  58. package/src/components/__stories__/image-loader.stories.tsx +0 -63
  59. package/src/components/__stories__/info-tip.stories.tsx +0 -31
  60. package/src/components/__stories__/inline-icon.stories.tsx +0 -40
  61. package/src/components/__stories__/input-with-examples.stories.tsx +0 -53
  62. package/src/components/__stories__/lint.stories.tsx +0 -86
  63. package/src/components/__stories__/math-input.stories.tsx +0 -43
  64. package/src/components/__stories__/math-output.stories.tsx +0 -23
  65. package/src/components/__stories__/multi-button-group.stories.tsx +0 -66
  66. package/src/components/__stories__/number-input.stories.tsx +0 -41
  67. package/src/components/__stories__/prop-check-box.stories.tsx +0 -65
  68. package/src/components/__stories__/range-input.stories.tsx +0 -29
  69. package/src/components/__stories__/simple-keypad-input.stories.tsx +0 -27
  70. package/src/components/__stories__/sortable.stories.tsx +0 -77
  71. package/src/components/__stories__/stub-tag-editor.stories.tsx +0 -45
  72. package/src/components/__stories__/svg-image.stories.tsx +0 -74
  73. package/src/components/__stories__/tex.stories.tsx +0 -23
  74. package/src/components/__stories__/text-input.stories.tsx +0 -33
  75. package/src/components/__stories__/text-list-editor.stories.tsx +0 -32
  76. package/src/components/__stories__/tooltip.stories.tsx +0 -52
  77. package/src/components/__stories__/zoomable-tex.stories.tsx +0 -41
  78. package/src/components/__stories__/zoomable.stories.tsx +0 -49
  79. package/src/components/__testdata__/sorter.testdata.ts +0 -19
  80. package/src/components/__tests__/__snapshots__/sortable.test.tsx.snap +0 -163
  81. package/src/components/__tests__/__snapshots__/sorter.test.tsx.snap +0 -265
  82. package/src/components/__tests__/math-input.test.tsx +0 -147
  83. package/src/components/__tests__/number-input.test.tsx +0 -128
  84. package/src/components/__tests__/sortable.test.tsx +0 -269
  85. package/src/components/__tests__/sorter.test.tsx +0 -114
  86. package/src/components/__tests__/zoomable.test.tsx +0 -399
  87. package/src/components/button-group.tsx +0 -130
  88. package/src/components/constants.ts +0 -14
  89. package/src/components/fixed-to-responsive.tsx +0 -169
  90. package/src/components/graph.tsx +0 -444
  91. package/src/components/graphie-classes.ts +0 -153
  92. package/src/components/graphie-movables.ts +0 -338
  93. package/src/components/graphie.tsx +0 -392
  94. package/src/components/highlighting/highlightable-content.tsx +0 -240
  95. package/src/components/highlighting/highlights.ts +0 -207
  96. package/src/components/highlighting/ranges.ts +0 -304
  97. package/src/components/highlighting/types.ts +0 -73
  98. package/src/components/highlighting/ui/highlight-renderer.tsx +0 -172
  99. package/src/components/highlighting/ui/highlight-set-renderer.tsx +0 -185
  100. package/src/components/highlighting/ui/highlight-tooltip.tsx +0 -141
  101. package/src/components/highlighting/ui/highlighting-ui.tsx +0 -143
  102. package/src/components/highlighting/ui/selection-tracker.tsx +0 -186
  103. package/src/components/highlighting/ui/types.ts +0 -21
  104. package/src/components/highlighting/ui/util.ts +0 -165
  105. package/src/components/highlighting/word-indexer.tsx +0 -152
  106. package/src/components/hud.tsx +0 -167
  107. package/src/components/icon.tsx +0 -141
  108. package/src/components/image-loader.tsx +0 -177
  109. package/src/components/info-tip/info-tip.tsx +0 -144
  110. package/src/components/info-tip.tsx +0 -39
  111. package/src/components/inline-icon.tsx +0 -60
  112. package/src/components/input-with-examples.tsx +0 -249
  113. package/src/components/lint.tsx +0 -459
  114. package/src/components/math-input.tsx +0 -474
  115. package/src/components/math-output.tsx +0 -134
  116. package/src/components/multi-button-group.tsx +0 -135
  117. package/src/components/number-input.tsx +0 -249
  118. package/src/components/prop-check-box.tsx +0 -79
  119. package/src/components/range-input.tsx +0 -60
  120. package/src/components/simple-keypad-input.tsx +0 -88
  121. package/src/components/sortable.tsx +0 -958
  122. package/src/components/stub-tag-editor.tsx +0 -53
  123. package/src/components/svg-image.tsx +0 -779
  124. package/src/components/tex.tsx +0 -67
  125. package/src/components/text-input.tsx +0 -129
  126. package/src/components/text-list-editor.tsx +0 -166
  127. package/src/components/tooltip.tsx +0 -410
  128. package/src/components/visibility-observer/__stories__/visibility-observer.stories.tsx +0 -141
  129. package/src/components/visibility-observer/create-visibility-observer.ts +0 -108
  130. package/src/components/visibility-observer/get-scroll-parent.ts +0 -46
  131. package/src/components/visibility-observer/visibility-observer.tsx +0 -124
  132. package/src/components/zoomable-tex.tsx +0 -78
  133. package/src/components/zoomable.tsx +0 -304
  134. package/src/components.ts +0 -15
  135. package/src/definition-context.tsx +0 -59
  136. package/src/dependencies.ts +0 -40
  137. package/src/error-boundary.tsx +0 -70
  138. package/src/extra-widgets.ts +0 -75
  139. package/src/hint-renderer.tsx +0 -151
  140. package/src/hints-renderer.tsx +0 -372
  141. package/src/icon-paths.ts +0 -119
  142. package/src/index.ts +0 -171
  143. package/src/init.ts +0 -56
  144. package/src/interaction-tracker.ts +0 -62
  145. package/src/interactive2/__tests__/movable-mock.ts +0 -43
  146. package/src/interactive2/__tests__/movable-point.test.ts +0 -144
  147. package/src/interactive2/__tests__/movable.test.ts +0 -157
  148. package/src/interactive2/interactive-util.test.ts +0 -93
  149. package/src/interactive2/interactive-util.ts +0 -159
  150. package/src/interactive2/movable-helper-methods.ts +0 -166
  151. package/src/interactive2/movable-line-options.ts +0 -419
  152. package/src/interactive2/movable-line.ts +0 -307
  153. package/src/interactive2/movable-point-options.ts +0 -212
  154. package/src/interactive2/movable-point.tsx +0 -585
  155. package/src/interactive2/movable-polygon-options.ts +0 -389
  156. package/src/interactive2/movable-polygon.ts +0 -360
  157. package/src/interactive2/movable.ts +0 -279
  158. package/src/interactive2/objective_.ts +0 -79
  159. package/src/interactive2/types.ts +0 -2
  160. package/src/interactive2/wrapped-defaults.ts +0 -87
  161. package/src/interactive2/wrapped-ellipse.ts +0 -94
  162. package/src/interactive2/wrapped-line.ts +0 -116
  163. package/src/interactive2/wrapped-path.ts +0 -51
  164. package/src/interactive2.ts +0 -70
  165. package/src/item-version.ts +0 -10
  166. package/src/jipt-paragraphs.ts +0 -49
  167. package/src/jquery.mobile.vmouse.js +0 -545
  168. package/src/loading-context.ts +0 -21
  169. package/src/logging/log.test.ts +0 -39
  170. package/src/logging/log.ts +0 -97
  171. package/src/mixins/__tests__/widget-prop-denylist.test.ts +0 -21
  172. package/src/mixins/changeable.ts +0 -133
  173. package/src/mixins/editor-jsonify.ts +0 -13
  174. package/src/mixins/widget-jsonify-deprecated.ts +0 -23
  175. package/src/mixins/widget-prop-denylist.ts +0 -48
  176. package/src/multi-items/__stories__/multi-renderer.stories.tsx +0 -83
  177. package/src/multi-items/__testdata__/multi-renderer.testdata.ts +0 -172
  178. package/src/multi-items/__tests__/__snapshots__/multi-renderer.test.tsx.snap +0 -1164
  179. package/src/multi-items/__tests__/items.test.ts +0 -240
  180. package/src/multi-items/__tests__/multi-renderer.test.tsx +0 -914
  181. package/src/multi-items/__tests__/prop-type-builders.test.ts +0 -133
  182. package/src/multi-items/__tests__/shapes.test.ts +0 -43
  183. package/src/multi-items/__tests__/trees.test.ts +0 -317
  184. package/src/multi-items/item-types.ts +0 -56
  185. package/src/multi-items/items.ts +0 -176
  186. package/src/multi-items/multi-renderer.tsx +0 -569
  187. package/src/multi-items/prop-type-builders.ts +0 -106
  188. package/src/multi-items/shape-types.ts +0 -52
  189. package/src/multi-items/shapes.ts +0 -52
  190. package/src/multi-items/tree-types.ts +0 -47
  191. package/src/multi-items/trees.ts +0 -360
  192. package/src/multi-items.ts +0 -75
  193. package/src/perseus-api.tsx +0 -188
  194. package/src/perseus-env.ts +0 -27
  195. package/src/perseus-error.ts +0 -17
  196. package/src/perseus-markdown.tsx +0 -405
  197. package/src/perseus-types.ts +0 -1366
  198. package/src/perseus-version.ts +0 -9
  199. package/src/question-paragraph.tsx +0 -34
  200. package/src/renderability.ts +0 -109
  201. package/src/renderer.tsx +0 -2000
  202. package/src/server-item-renderer.tsx +0 -495
  203. package/src/sigfigs.ts +0 -490
  204. package/src/styles/articles.less +0 -749
  205. package/src/styles/constants.ts +0 -94
  206. package/src/styles/global-constants.ts +0 -343
  207. package/src/styles/global-styles.ts +0 -306
  208. package/src/styles/khan-exercise.css +0 -1468
  209. package/src/styles/media-queries.ts +0 -47
  210. package/src/styles/perseus-renderer.less +0 -732
  211. package/src/styles/shared.ts +0 -130
  212. package/src/styles/util.less +0 -47
  213. package/src/styles/variables.boostrap.less +0 -277
  214. package/src/styles/variables.less +0 -56
  215. package/src/styles/variables.shared.less +0 -340
  216. package/src/styles/widgets/categorizer.less +0 -38
  217. package/src/styles/widgets/dropdown.less +0 -3
  218. package/src/styles/widgets/expression.less +0 -65
  219. package/src/styles/widgets/graded-group.less +0 -36
  220. package/src/styles/widgets/grapher.less +0 -12
  221. package/src/styles/widgets/group.less +0 -19
  222. package/src/styles/widgets/image.less +0 -54
  223. package/src/styles/widgets/interactive-graph.less +0 -64
  224. package/src/styles/widgets/label-image.less +0 -57
  225. package/src/styles/widgets/matcher.less +0 -8
  226. package/src/styles/widgets/matrix.less +0 -126
  227. package/src/styles/widgets/measurer.less +0 -20
  228. package/src/styles/widgets/orderer.less +0 -98
  229. package/src/styles/widgets/passage.less +0 -154
  230. package/src/styles/widgets/plotter.less +0 -31
  231. package/src/styles/widgets/radio.less +0 -58
  232. package/src/styles/widgets/reaction-diagram.less +0 -35
  233. package/src/styles/widgets/sequence.less +0 -5
  234. package/src/styles/widgets/sortable.less +0 -113
  235. package/src/styles/widgets/table.less +0 -57
  236. package/src/styles/widgets/unit.less +0 -15
  237. package/src/tex-wrangler.ts +0 -182
  238. package/src/translation-linter.ts +0 -126
  239. package/src/traversal.ts +0 -138
  240. package/src/types.ts +0 -513
  241. package/src/util/a11y.ts +0 -23
  242. package/src/util/answer-types.test.ts +0 -114
  243. package/src/util/answer-types.ts +0 -876
  244. package/src/util/colors.ts +0 -93
  245. package/src/util/drawing-transform.test.ts +0 -67
  246. package/src/util/drawing-transform.ts +0 -102
  247. package/src/util/extract-perseus-data.ts +0 -539
  248. package/src/util/fix-passage-refs.ts +0 -121
  249. package/src/util/geometry.test.ts +0 -75
  250. package/src/util/geometry.ts +0 -312
  251. package/src/util/graph-bounds.ts +0 -23
  252. package/src/util/graph-utils.ts +0 -15
  253. package/src/util/graphie.test.ts +0 -1277
  254. package/src/util/graphie.ts +0 -1566
  255. package/src/util/interactive.ts +0 -4172
  256. package/src/util/interval.ts +0 -4
  257. package/src/util/katex-preprocess.ts +0 -14
  258. package/src/util/math.test.ts +0 -11
  259. package/src/util/math.ts +0 -294
  260. package/src/util/mobile-native-utils.test.ts +0 -22
  261. package/src/util/mobile-native-utils.ts +0 -12
  262. package/src/util/parse-perseus-json.test.ts +0 -10
  263. package/src/util/parse-perseus-json.ts +0 -13
  264. package/src/util/react-render.tsx +0 -27
  265. package/src/util/register-all-widgets-for-testing.ts +0 -13
  266. package/src/util/scroll-utils.ts +0 -155
  267. package/src/util/sizing-utils.ts +0 -46
  268. package/src/util/tex.ts +0 -73
  269. package/src/util/url-utils.ts +0 -34
  270. package/src/util.graphie.test.ts +0 -45
  271. package/src/util.graphie.ts +0 -59
  272. package/src/util.ts +0 -966
  273. package/src/version.ts +0 -10
  274. package/src/widget-container.tsx +0 -199
  275. package/src/widgets/__stories__/categorizer.stories.tsx +0 -14
  276. package/src/widgets/__stories__/cs-program.stories.tsx +0 -14
  277. package/src/widgets/__stories__/definition.stories.tsx +0 -114
  278. package/src/widgets/__stories__/dropdown.stories.tsx +0 -14
  279. package/src/widgets/__stories__/explanation.stories.tsx +0 -18
  280. package/src/widgets/__stories__/expression.stories.tsx +0 -145
  281. package/src/widgets/__stories__/graded-group-set.stories.tsx +0 -31
  282. package/src/widgets/__stories__/graded-group.stories.tsx +0 -31
  283. package/src/widgets/__stories__/grapher.stories.tsx +0 -46
  284. package/src/widgets/__stories__/group.stories.tsx +0 -14
  285. package/src/widgets/__stories__/iframe.stories.tsx +0 -14
  286. package/src/widgets/__stories__/image.stories.tsx +0 -85
  287. package/src/widgets/__stories__/input-number.stories.tsx +0 -101
  288. package/src/widgets/__stories__/interaction.stories.tsx +0 -14
  289. package/src/widgets/__stories__/interactive-graph.stories.tsx +0 -60
  290. package/src/widgets/__stories__/label-image.stories.tsx +0 -130
  291. package/src/widgets/__stories__/matcher.stories.tsx +0 -14
  292. package/src/widgets/__stories__/matrix.stories.tsx +0 -14
  293. package/src/widgets/__stories__/number-line.stories.tsx +0 -14
  294. package/src/widgets/__stories__/numeric-input.stories.tsx +0 -105
  295. package/src/widgets/__stories__/orderer.stories.tsx +0 -18
  296. package/src/widgets/__stories__/passage-ref.stories.tsx +0 -33
  297. package/src/widgets/__stories__/passage.stories.tsx +0 -39
  298. package/src/widgets/__stories__/plotter.stories.tsx +0 -27
  299. package/src/widgets/__stories__/radio.stories.tsx +0 -86
  300. package/src/widgets/__stories__/random-widgets.stories.tsx +0 -59
  301. package/src/widgets/__stories__/sorter.stories.tsx +0 -14
  302. package/src/widgets/__stories__/test-keypad-context-wrapper.tsx +0 -66
  303. package/src/widgets/__stories__/unit.stories.tsx +0 -28
  304. package/src/widgets/__stories__/video-transcript-link.stories.tsx +0 -23
  305. package/src/widgets/__stories__/video.stories.tsx +0 -22
  306. package/src/widgets/__testdata__/base-radio.testdata.ts +0 -21
  307. package/src/widgets/__testdata__/categorizer.testdata.ts +0 -89
  308. package/src/widgets/__testdata__/cs-program.testdata.ts +0 -27
  309. package/src/widgets/__testdata__/dropdown.testdata.ts +0 -71
  310. package/src/widgets/__testdata__/explanation.testdata.ts +0 -85
  311. package/src/widgets/__testdata__/expression.testdata.ts +0 -198
  312. package/src/widgets/__testdata__/graded-group-set.testdata.ts +0 -356
  313. package/src/widgets/__testdata__/graded-group.testdata.ts +0 -77
  314. package/src/widgets/__testdata__/grapher.testdata.ts +0 -374
  315. package/src/widgets/__testdata__/group.testdata.ts +0 -153
  316. package/src/widgets/__testdata__/iframe.testdata.ts +0 -35
  317. package/src/widgets/__testdata__/image.testdata.ts +0 -36
  318. package/src/widgets/__testdata__/input-number.testdata.ts +0 -72
  319. package/src/widgets/__testdata__/interaction.testdata.ts +0 -262
  320. package/src/widgets/__testdata__/interactive-graph-random.testdata.ts +0 -269
  321. package/src/widgets/__testdata__/interactive-graph.testdata.ts +0 -644
  322. package/src/widgets/__testdata__/label-image.testdata.ts +0 -308
  323. package/src/widgets/__testdata__/matcher.testdata.ts +0 -33
  324. package/src/widgets/__testdata__/matrix.testdata.ts +0 -28
  325. package/src/widgets/__testdata__/number-line.testdata.ts +0 -30
  326. package/src/widgets/__testdata__/numeric-input.testdata.ts +0 -265
  327. package/src/widgets/__testdata__/orderer.testdata.ts +0 -123
  328. package/src/widgets/__testdata__/passage-ref.testdata.ts +0 -101
  329. package/src/widgets/__testdata__/passage.testdata.ts +0 -95
  330. package/src/widgets/__testdata__/plotter.testdata.ts +0 -36
  331. package/src/widgets/__testdata__/radio.testdata.ts +0 -324
  332. package/src/widgets/__testdata__/randomizers.ts +0 -51
  333. package/src/widgets/__testdata__/sorter.testdata.ts +0 -19
  334. package/src/widgets/__testdata__/video.testdata.ts +0 -61
  335. package/src/widgets/__tests__/__snapshots__/categorizer.test.ts.snap +0 -940
  336. package/src/widgets/__tests__/__snapshots__/cs-program.test.ts.snap +0 -67
  337. package/src/widgets/__tests__/__snapshots__/definition.test.ts.snap +0 -97
  338. package/src/widgets/__tests__/__snapshots__/dropdown.test.ts.snap +0 -133
  339. package/src/widgets/__tests__/__snapshots__/explanation.test.ts.snap +0 -425
  340. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt.test.ts.snap +0 -729
  341. package/src/widgets/__tests__/__snapshots__/graded-group-set.test.ts.snap +0 -339
  342. package/src/widgets/__tests__/__snapshots__/graded-group.test.ts.snap +0 -801
  343. package/src/widgets/__tests__/__snapshots__/grapher.test.ts.snap +0 -2673
  344. package/src/widgets/__tests__/__snapshots__/group.test.tsx.snap +0 -1211
  345. package/src/widgets/__tests__/__snapshots__/iframe.test.ts.snap +0 -61
  346. package/src/widgets/__tests__/__snapshots__/image.test.ts.snap +0 -378
  347. package/src/widgets/__tests__/__snapshots__/input-number.test.ts.snap +0 -495
  348. package/src/widgets/__tests__/__snapshots__/interaction.test.ts.snap +0 -292
  349. package/src/widgets/__tests__/__snapshots__/interactive-graph.test.ts.snap +0 -2889
  350. package/src/widgets/__tests__/__snapshots__/matcher.test.tsx.snap +0 -883
  351. package/src/widgets/__tests__/__snapshots__/matrix.test.ts.snap +0 -514
  352. package/src/widgets/__tests__/__snapshots__/number-line.test.ts.snap +0 -814
  353. package/src/widgets/__tests__/__snapshots__/numeric-input.test.ts.snap +0 -470
  354. package/src/widgets/__tests__/__snapshots__/orderer.test.ts.snap +0 -243
  355. package/src/widgets/__tests__/__snapshots__/passage-ref.test.ts.snap +0 -434
  356. package/src/widgets/__tests__/__snapshots__/passage.test.tsx.snap +0 -1956
  357. package/src/widgets/__tests__/__snapshots__/plotter.test.tsx.snap +0 -827
  358. package/src/widgets/__tests__/__snapshots__/radio.test.ts.snap +0 -4237
  359. package/src/widgets/__tests__/__snapshots__/unit.test.tsx.snap +0 -20
  360. package/src/widgets/__tests__/__snapshots__/video.test.ts.snap +0 -153
  361. package/src/widgets/__tests__/categorizer.test.ts +0 -203
  362. package/src/widgets/__tests__/cs-program.test.ts +0 -43
  363. package/src/widgets/__tests__/definition.test.ts +0 -136
  364. package/src/widgets/__tests__/dropdown.test.ts +0 -105
  365. package/src/widgets/__tests__/explanation.test.ts +0 -198
  366. package/src/widgets/__tests__/expression-mobile.test.tsx +0 -363
  367. package/src/widgets/__tests__/expression.test.tsx +0 -484
  368. package/src/widgets/__tests__/graded-group-set-jipt.test.ts +0 -30
  369. package/src/widgets/__tests__/graded-group-set.test.ts +0 -288
  370. package/src/widgets/__tests__/graded-group.test.ts +0 -232
  371. package/src/widgets/__tests__/grapher.cypress.ts +0 -576
  372. package/src/widgets/__tests__/grapher.test.ts +0 -33
  373. package/src/widgets/__tests__/group.test.tsx +0 -510
  374. package/src/widgets/__tests__/iframe.test.ts +0 -45
  375. package/src/widgets/__tests__/image.test.ts +0 -62
  376. package/src/widgets/__tests__/input-number.test.ts +0 -354
  377. package/src/widgets/__tests__/interaction.test.ts +0 -31
  378. package/src/widgets/__tests__/interactive-graph.cypress.ts +0 -815
  379. package/src/widgets/__tests__/interactive-graph.test.ts +0 -106
  380. package/src/widgets/__tests__/matcher.test.tsx +0 -176
  381. package/src/widgets/__tests__/matrix.test.ts +0 -82
  382. package/src/widgets/__tests__/number-line.test.ts +0 -79
  383. package/src/widgets/__tests__/numeric-input.test.ts +0 -575
  384. package/src/widgets/__tests__/orderer.test.ts +0 -73
  385. package/src/widgets/__tests__/passage-ref.test.ts +0 -168
  386. package/src/widgets/__tests__/passage.test.tsx +0 -224
  387. package/src/widgets/__tests__/plotter.test.tsx +0 -69
  388. package/src/widgets/__tests__/radio/base-radio.test.tsx +0 -470
  389. package/src/widgets/__tests__/radio/choice-icon.test.tsx +0 -90
  390. package/src/widgets/__tests__/radio/choice.test.tsx +0 -310
  391. package/src/widgets/__tests__/radio/focus-ring.test.tsx +0 -22
  392. package/src/widgets/__tests__/radio/option-status.test.tsx +0 -77
  393. package/src/widgets/__tests__/radio.test.ts +0 -807
  394. package/src/widgets/__tests__/randomizers.test.ts +0 -53
  395. package/src/widgets/__tests__/renderQuestion.tsx +0 -101
  396. package/src/widgets/__tests__/unit.test.tsx +0 -168
  397. package/src/widgets/__tests__/video.test.ts +0 -69
  398. package/src/widgets/categorizer.tsx +0 -344
  399. package/src/widgets/cs-program.tsx +0 -227
  400. package/src/widgets/definition.tsx +0 -121
  401. package/src/widgets/deprecated-standin/__stories__/deprecated-standin.stories.tsx +0 -30
  402. package/src/widgets/deprecated-standin/__tests__/__snapshots__/deprecated-standin.test.ts.snap +0 -70
  403. package/src/widgets/deprecated-standin/__tests__/deprecated-standin.test.ts +0 -93
  404. package/src/widgets/deprecated-standin/index.tsx +0 -58
  405. package/src/widgets/dropdown.tsx +0 -142
  406. package/src/widgets/explanation.tsx +0 -317
  407. package/src/widgets/expression.tsx +0 -707
  408. package/src/widgets/graded-group-answer-bar.tsx +0 -142
  409. package/src/widgets/graded-group-set.tsx +0 -338
  410. package/src/widgets/graded-group.tsx +0 -471
  411. package/src/widgets/grapher/util.tsx +0 -799
  412. package/src/widgets/grapher.tsx +0 -660
  413. package/src/widgets/group.tsx +0 -195
  414. package/src/widgets/iframe.tsx +0 -195
  415. package/src/widgets/image.tsx +0 -312
  416. package/src/widgets/input-number.tsx +0 -412
  417. package/src/widgets/interaction/arrow-picker.tsx +0 -38
  418. package/src/widgets/interaction/color-picker.tsx +0 -69
  419. package/src/widgets/interaction/dash-picker.tsx +0 -45
  420. package/src/widgets/interaction/element-container.tsx +0 -100
  421. package/src/widgets/interaction.tsx +0 -826
  422. package/src/widgets/interactive-graph.tsx +0 -2597
  423. package/src/widgets/label-image/__stories__/answer-choices.stories.tsx +0 -91
  424. package/src/widgets/label-image/__stories__/answer-pill.stories.tsx +0 -62
  425. package/src/widgets/label-image/__stories__/marker.stories.tsx +0 -171
  426. package/src/widgets/label-image/__tests__/answer-pill.test.tsx +0 -75
  427. package/src/widgets/label-image/__tests__/hide-answers-toggle.test.tsx +0 -39
  428. package/src/widgets/label-image/answer-choices.tsx +0 -88
  429. package/src/widgets/label-image/answer-pill.tsx +0 -103
  430. package/src/widgets/label-image/hide-answers-toggle.tsx +0 -40
  431. package/src/widgets/label-image/marker.tsx +0 -268
  432. package/src/widgets/label-image/strings.ts +0 -5
  433. package/src/widgets/label-image/types.ts +0 -27
  434. package/src/widgets/label-image.test.ts +0 -928
  435. package/src/widgets/label-image.tsx +0 -819
  436. package/src/widgets/lights-puzzle.tsx +0 -359
  437. package/src/widgets/matcher.tsx +0 -298
  438. package/src/widgets/matrix.tsx +0 -634
  439. package/src/widgets/measurer.tsx +0 -222
  440. package/src/widgets/molecule/__tests__/layout.test.ts +0 -162
  441. package/src/widgets/molecule/__tests__/parser.test.ts +0 -155
  442. package/src/widgets/molecule/molecule-drawing.ts +0 -173
  443. package/src/widgets/molecule/molecule-layout.ts +0 -362
  444. package/src/widgets/molecule/smiles-parser.ts +0 -296
  445. package/src/widgets/molecule.tsx +0 -172
  446. package/src/widgets/number-line.tsx +0 -862
  447. package/src/widgets/numeric-input.tsx +0 -576
  448. package/src/widgets/orderer.tsx +0 -786
  449. package/src/widgets/passage/__tests__/passage-markdown.test.ts +0 -376
  450. package/src/widgets/passage/passage-markdown.tsx +0 -475
  451. package/src/widgets/passage-ref-target.tsx +0 -76
  452. package/src/widgets/passage-ref.tsx +0 -213
  453. package/src/widgets/passage.tsx +0 -582
  454. package/src/widgets/plotter.tsx +0 -1194
  455. package/src/widgets/radio/__stories__/base-radio.stories.tsx +0 -196
  456. package/src/widgets/radio/__stories__/choice-icon.stories.tsx +0 -166
  457. package/src/widgets/radio/__stories__/choice-none-above.stories.tsx +0 -40
  458. package/src/widgets/radio/__stories__/choice.stories.tsx +0 -137
  459. package/src/widgets/radio/__stories__/focus-ring.stories.tsx +0 -43
  460. package/src/widgets/radio/__stories__/option-status.stories.tsx +0 -103
  461. package/src/widgets/radio/base-radio.tsx +0 -511
  462. package/src/widgets/radio/choice-a11y-text.ts +0 -61
  463. package/src/widgets/radio/choice-icon/choice-icon.tsx +0 -218
  464. package/src/widgets/radio/choice-icon/cross-out-line.tsx +0 -51
  465. package/src/widgets/radio/choice-icon/shared-styles.ts +0 -15
  466. package/src/widgets/radio/choice-none-above.tsx +0 -56
  467. package/src/widgets/radio/choice.jsx.fixture.ts +0 -45
  468. package/src/widgets/radio/choice.tsx +0 -359
  469. package/src/widgets/radio/focus-ring.tsx +0 -57
  470. package/src/widgets/radio/option-status.tsx +0 -98
  471. package/src/widgets/radio/util.test.ts +0 -18
  472. package/src/widgets/radio/util.ts +0 -31
  473. package/src/widgets/radio/widget.tsx +0 -520
  474. package/src/widgets/radio.ts +0 -157
  475. package/src/widgets/reaction-diagram.tsx +0 -144
  476. package/src/widgets/sequence.tsx +0 -142
  477. package/src/widgets/simulator.tsx +0 -829
  478. package/src/widgets/sorter.tsx +0 -155
  479. package/src/widgets/table.tsx +0 -393
  480. package/src/widgets/unit.tsx +0 -374
  481. package/src/widgets/video-transcript-link.test.tsx +0 -213
  482. package/src/widgets/video-transcript-link.tsx +0 -84
  483. package/src/widgets/video.tsx +0 -135
  484. package/src/widgets.ts +0 -489
  485. package/src/zoom.ts +0 -555
  486. package/tsconfig-build.json +0 -26
  487. package/tsconfig-build.tsbuildinfo +0 -1
package/src/renderer.tsx DELETED
@@ -1,2000 +0,0 @@
1
- /* eslint-disable @khanacademy/ts-no-error-suppressions */
2
- /* eslint-disable react/no-unsafe */
3
- import * as PerseusLinter from "@khanacademy/perseus-linter";
4
- import {entries} from "@khanacademy/wonder-stuff-core";
5
- import classNames from "classnames";
6
- import $ from "jquery";
7
- import * as React from "react";
8
- import ReactDOM from "react-dom";
9
- import _ from "underscore";
10
-
11
- import AssetContext from "./asset-context";
12
- import SvgImage from "./components/svg-image";
13
- import TeX from "./components/tex";
14
- import Zoomable from "./components/zoomable";
15
- import ZoomableTeX from "./components/zoomable-tex";
16
- import {DefinitionProvider} from "./definition-context";
17
- import {getDependencies} from "./dependencies";
18
- import ErrorBoundary from "./error-boundary";
19
- import InteractionTracker from "./interaction-tracker";
20
- import Objective from "./interactive2/objective_";
21
- import JiptParagraphs from "./jipt-paragraphs";
22
- import {Errors, Log} from "./logging/log";
23
- import {ClassNames as ApiClassNames, ApiOptions} from "./perseus-api";
24
- import {PerseusError} from "./perseus-error";
25
- import PerseusMarkdown from "./perseus-markdown";
26
- import QuestionParagraph from "./question-paragraph";
27
- import TranslationLinter from "./translation-linter";
28
- import Util from "./util";
29
- import preprocessTex from "./util/katex-preprocess";
30
- import WidgetContainer from "./widget-container";
31
- import * as Widgets from "./widgets";
32
-
33
- import type {DependenciesContext} from "./dependencies";
34
- import type {
35
- PerseusRenderer,
36
- PerseusWidget,
37
- PerseusWidgetOptions,
38
- } from "./perseus-types";
39
- import type {
40
- APIOptions,
41
- APIOptionsWithDefaults,
42
- FilterCriterion,
43
- FocusPath,
44
- PerseusScore,
45
- PerseusWidgetMap,
46
- WidgetProps,
47
- } from "./types";
48
- import type {LinterContextProps} from "@khanacademy/perseus-linter";
49
-
50
- import "./styles/perseus-renderer.less";
51
-
52
- const {mapObject, mapObjectFromArray} = Objective;
53
-
54
- const rContainsNonWhitespace = /\S/;
55
- const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
56
-
57
- const noopOnRender = () => {};
58
-
59
- const SHOULD_CLEAR_WIDGETS_PROP_LIST = ["content", "problemNum", "widgets"];
60
-
61
- const makeContainerId = (id: string) => "container:" + id;
62
-
63
- // Check if one focus path / id path is a prefix of another
64
- // The focus path null will never be a prefix of any non-null
65
- // path, since it represents no focus.
66
- // Otherwise, prefix is calculated by whether every array
67
- // element in the prefix is present in the same position in the
68
- // wholeArray path.
69
- const isIdPathPrefix = function (
70
- prefixArray: FocusPath,
71
- wholeArray: FocusPath,
72
- ) {
73
- if (prefixArray === null || wholeArray === null) {
74
- return prefixArray === wholeArray;
75
- }
76
-
77
- // @ts-expect-error - TS2345 - Argument of type 'readonly string[] | undefined' is not assignable to parameter of type 'Collection<any>'.
78
- return _.every(prefixArray, (elem: string, i: number) => {
79
- if (wholeArray != null) {
80
- return _.isEqual(elem, wholeArray[i]);
81
- }
82
- });
83
- };
84
-
85
- type WidgetState = {
86
- isMobile?: boolean;
87
- inTable?: boolean;
88
- key?: number;
89
- paragraphIndex?: number;
90
- foundFullWidth?: boolean;
91
- baseElements?: any;
92
- };
93
-
94
- type SetWidgetPropsFn = (
95
- id: string,
96
- newProps: any,
97
- cb: () => boolean,
98
- // Widgets can call `onChange` with `silent` set to `true` to prevent
99
- // interaction events from being triggered in listeners.
100
- silent?: boolean,
101
- ) => void;
102
-
103
- // The return type for getUserInput. Widgets have full control of what is
104
- // returned so it's not easily typed (some widgets return a scalar (string),
105
- // some return a custom-built object, some delegate to
106
- // `WidgetJsonifyDeprecated` which returns an object containing widget props
107
- // (filtered by deprecated keys)).
108
- type WidgetUserInput = any;
109
-
110
- type SerializedState = {
111
- [id: string]: any;
112
- };
113
-
114
- export type Widget = {
115
- focus: () =>
116
- | {
117
- id: string;
118
- path: FocusPath;
119
- }
120
- | boolean;
121
- getDOMNodeForPath: (path: FocusPath) => Element | Text | null;
122
- deselectIncorrectSelectedChoices?: () => void;
123
- restoreSerializedState: (props: any, callback: () => void) => any;
124
- // TODO(jeremy): I think this return value is wrong. The widget
125
- // getSerializedState should just return _its_ serialized state, not a
126
- // key/value list of all widget states (i think!)
127
- // Returns widget state that can be passed back to `restoreSerializedState`
128
- // to put the widget back into exactly the same state. If the widget does
129
- // not implement this function, the renderer simply returns all of the
130
- // widget's props.
131
- getSerializedState?: () => SerializedState; // SUSPECT,
132
- getGrammarTypeForPath: (path: FocusPath) => string;
133
- blurInputPath?: (path: FocusPath) => null;
134
- focusInputPath?: (path: FocusPath) => null;
135
- getInputPaths?: () => ReadonlyArray<FocusPath>;
136
- setInputValue?: (
137
- path: FocusPath,
138
- newValue: string,
139
- // TODO(jeremy): I think this is actually a callback
140
- focus?: () => unknown,
141
- ) => void;
142
- getUserInput?: () => WidgetUserInput | null | undefined;
143
- simpleValidate?: (
144
- options?: any,
145
- onOutputError?: (
146
- widgetId: any,
147
- value: string,
148
- message?: string | null | undefined,
149
- ) => unknown | null | undefined,
150
- ) => PerseusScore;
151
- showRationalesForCurrentlySelectedChoices?: (options?: any) => void;
152
- examples?: () => ReadonlyArray<string>;
153
- };
154
-
155
- type Props = Partial<React.ContextType<typeof DependenciesContext>> & {
156
- apiOptions?: APIOptions;
157
- alwaysUpdate?: boolean;
158
- findExternalWidgets: any;
159
- highlightedWidgets?: ReadonlyArray<any>;
160
- images: PerseusRenderer["images"];
161
- keypadElement?: any; // TODO(kevinb): add proper types,
162
- onInteractWithWidget: (id: string) => void;
163
- onRender: (node?: any) => void;
164
- problemNum?: number;
165
- questionCompleted?: boolean;
166
- reviewMode?: boolean | null | undefined;
167
- content: PerseusRenderer["content"];
168
- serializedState?: any;
169
- // Callback which is called when serialized state changes with the new
170
- // serialized state.
171
- onSerializedStateUpdated: (serializedState: {
172
- [key: string]: any;
173
- }) => unknown;
174
- // If linterContext.highlightLint is true, then content will be passed
175
- // to the linter and any warnings will be highlighted in the rendered
176
- // output.
177
- linterContext: LinterContextProps;
178
- legacyPerseusLint?: ReadonlyArray<string>;
179
- widgets: PerseusRenderer["widgets"];
180
- // Skip adding paragraph class
181
- inline?: boolean;
182
- };
183
-
184
- type State = {
185
- translationLintErrors: ReadonlyArray<string>;
186
- widgetInfo: Readonly<{
187
- [id: string]: PerseusWidget | null | undefined;
188
- }>;
189
- widgetProps: Readonly<{
190
- [id: string]: any | null | undefined;
191
- }>;
192
- jiptContent: any;
193
- lastUsedWidgetId: string | null | undefined;
194
- };
195
-
196
- type Context = LinterContextProps & {
197
- content: string;
198
- widgets: {
199
- [id: string]: any;
200
- };
201
- // This is inexact because LinterContextProps is inexact
202
- };
203
-
204
- type DefaultProps = {
205
- alwaysUpdate: Props["alwaysUpdate"];
206
- content: Props["content"];
207
- findExternalWidgets: Props["findExternalWidgets"];
208
- highlightedWidgets: Props["highlightedWidgets"];
209
- images: Props["images"];
210
- linterContext: Props["linterContext"];
211
- onInteractWithWidget: Props["onInteractWithWidget"];
212
- onRender: Props["onRender"];
213
- onSerializedStateUpdated: Props["onSerializedStateUpdated"];
214
- questionCompleted: Props["questionCompleted"];
215
- reviewMode: Props["reviewMode"];
216
- serializedState: Props["serializedState"];
217
- widgets: Props["widgets"];
218
- };
219
-
220
- class Renderer extends React.Component<Props, State> {
221
- _currentFocus: FocusPath | null | undefined;
222
- // @ts-expect-error - TS2564 - Property '_foundTextNodes' has no initializer and is not definitely assigned in the constructor.
223
- _foundTextNodes: boolean;
224
- // @ts-expect-error - TS2564 - Property '_interactionTrackers' has no initializer and is not definitely assigned in the constructor.
225
- _interactionTrackers: {
226
- [id: string]: InteractionTracker<any>;
227
- };
228
- // @ts-expect-error - TS2564 - Property '_isMounted' has no initializer and is not definitely assigned in the constructor.
229
- _isMounted: boolean;
230
- // @ts-expect-error - TS2564 - Property '_isTwoColumn' has no initializer and is not definitely assigned in the constructor.
231
- _isTwoColumn: boolean;
232
-
233
- // The i18n linter.
234
- _translationLinter: TranslationLinter;
235
-
236
- _widgetContainers: Map<string, WidgetContainer> = new Map();
237
-
238
- lastRenderedMarkdown: React.ReactNode;
239
- // @ts-expect-error - TS2564 - Property 'reuseMarkdown' has no initializer and is not definitely assigned in the constructor.
240
- reuseMarkdown: boolean;
241
- // @ts-expect-error - TS2564 - Property 'translationIndex' has no initializer and is not definitely assigned in the constructor.
242
- translationIndex: number;
243
- // @ts-expect-error - TS2564 - Property 'widgetIds' has no initializer and is not definitely assigned in the constructor.
244
- widgetIds: Array<string>;
245
-
246
- static defaultProps: DefaultProps = {
247
- content: "",
248
- widgets: {},
249
- images: {},
250
- highlightedWidgets: [],
251
- questionCompleted: false,
252
- // onRender may be called multiple times per render, for example
253
- // if there are multiple images or TeX pieces within `content`.
254
- // It is a good idea to debounce any functions passed here.
255
- onRender: noopOnRender,
256
- onInteractWithWidget: function () {},
257
- findExternalWidgets: () => [],
258
- alwaysUpdate: false,
259
- reviewMode: false,
260
- serializedState: null,
261
- onSerializedStateUpdated: () => {},
262
- linterContext: PerseusLinter.linterContextDefault,
263
- };
264
-
265
- constructor(props: Props) {
266
- super(props);
267
- this._translationLinter = new TranslationLinter();
268
-
269
- this.state = {
270
- jiptContent: null,
271
-
272
- // TranslationLinter is async and currently does not contain a
273
- // location. This is a list of error strings TranslationLinter
274
- // detected on its last run.
275
- translationLintErrors: [],
276
-
277
- // The ID of the last widget the user interacted with. We'll
278
- // use this to set the `isLastUsedWidget` flag on the
279
- // corresponding widget.
280
- lastUsedWidgetId: null,
281
-
282
- ...this._getInitialWidgetState(props),
283
- };
284
- }
285
-
286
- componentDidMount() {
287
- this._isMounted = true;
288
-
289
- // figure out why we're passing an empty object
290
- // @ts-expect-error - TS2345 - Argument of type '{}' is not assignable to parameter of type 'Props'.
291
- this.handleRender({});
292
- this._currentFocus = null;
293
-
294
- // TODO(emily): actually make the serializedState prop work like a
295
- // controlled prop, instead of manually calling .restoreSerializedState
296
- // at the right times.
297
- if (this.props.serializedState) {
298
- this.restoreSerializedState(this.props.serializedState);
299
- }
300
-
301
- if (this.props.linterContext.highlightLint) {
302
- // Get i18n lint errors asynchronously. If there are lint errors,
303
- // this component will be rerendered.
304
- this._translationLinter.runLinter(
305
- this.props.content,
306
- this.handletranslationLintErrors,
307
- );
308
- }
309
- }
310
-
311
- UNSAFE_componentWillReceiveProps(nextProps: Props) {
312
- if (
313
- !_.isEqual(
314
- _.pick(this.props, SHOULD_CLEAR_WIDGETS_PROP_LIST),
315
- _.pick(nextProps, SHOULD_CLEAR_WIDGETS_PROP_LIST),
316
- )
317
- ) {
318
- this.setState(this._getInitialWidgetState(nextProps));
319
- }
320
- }
321
-
322
- shouldComponentUpdate(nextProps: Props, nextState: State): any | boolean {
323
- // QUESTION(jeremy): Shouldn't we look at `nextProps` here? Otherwise
324
- // we're always looking "one render behind".
325
- if (this.props.alwaysUpdate) {
326
- // TOTAL hacks so that findWidgets doesn't break
327
- // when one widget updates without the other.
328
- // See passage-refs inside radios, which was why
329
- // this was introduced.
330
- // I'm sorry!
331
- // TODO(aria): cry
332
- //
333
- // HACK(djf): I've also set this alwaysUpdate property from
334
- // async-renderer.jsx in the manticore-package. I'm doing this
335
- // to work around an infinite loop of some sort that started
336
- // happening in manticore after the React 16 update. After
337
- // clicking around in the manticore exercise editor (for example)
338
- // the UI would freeze up, and the debugger would show that
339
- // we were always deep in a recursion on the propsChanged line
340
- // below. There is some kind of timing issue causing some kind
341
- // of infinite loop, but by avoiding the time-consuming deep
342
- // equal comparisons on our props (which are often huge) I can
343
- // no longer reproduce the bug.
344
- // TODO(djf): Remove this comment
345
- // https://khanacademy.atlassian.net/browse/CP-834 is resolved.
346
- return true;
347
- }
348
- const stateChanged = !_.isEqual(this.state, nextState);
349
- const propsChanged = !_.isEqual(this.props, nextProps);
350
- return propsChanged || stateChanged;
351
- }
352
-
353
- UNSAFE_componentWillUpdate(nextProps: Props, nextState: State) {
354
- const oldJipt = this.shouldRenderJiptPlaceholder(
355
- this.props,
356
- this.state,
357
- );
358
- const newJipt = this.shouldRenderJiptPlaceholder(nextProps, nextState);
359
- const oldContent = this.getContent(this.props, this.state);
360
- const newContent = this.getContent(nextProps, nextState);
361
- const oldHighlightedWidgets = this.props.highlightedWidgets;
362
- const newHighlightedWidgets = nextProps.highlightedWidgets;
363
-
364
- // TODO(jared): This seems to be a perfect overlap with
365
- // "shouldComponentUpdate" -- can we just remove this
366
- // componentWillUpdate and the reuseMarkdown attr?
367
- this.reuseMarkdown =
368
- !oldJipt &&
369
- !newJipt &&
370
- oldContent === newContent &&
371
- _.isEqual(
372
- this.state.translationLintErrors,
373
- nextState.translationLintErrors,
374
- ) &&
375
- // If we are running the linter then we need to know when
376
- // widgets have changed because we need for force the linter to
377
- // run when that happens. Note: don't do identity comparison here:
378
- // it can cause frequent re-renders that break MathJax somehow
379
- (!this.props.linterContext.highlightLint ||
380
- _.isEqual(this.props.widgets, nextProps.widgets)) &&
381
- // If the linter is turned on or off, we have to rerender
382
- this.props.linterContext.highlightLint ===
383
- nextProps.linterContext.highlightLint &&
384
- // yes, this is identity array comparison, but these are passed
385
- // in from state in the item-renderer, so they should be
386
- // identity equal unless something changed, and it's expensive
387
- // to loop through them to look for differences.
388
- // Technically, we could reuse the markdown when this changes,
389
- // but to do that we'd have to do more expensive checking of
390
- // whether a widget should be highlighted in the common case
391
- // where this array hasn't changed, so we just redo the whole
392
- // render if this changed
393
- oldHighlightedWidgets === newHighlightedWidgets;
394
- }
395
-
396
- componentDidUpdate(prevProps: Props, prevState: State) {
397
- this.handleRender(prevProps);
398
- // We even do this if we did reuse the markdown because
399
- // we might need to update the widget props on this render,
400
- // even though we have the same widgets.
401
- // WidgetContainers don't update their widgets' props when
402
- // they are re-rendered, so even if they've been
403
- // re-rendered we need to call these methods on them.
404
- _.each(this.widgetIds, (id) => {
405
- const container = this._widgetContainers.get(makeContainerId(id));
406
- container && container.replaceWidgetProps(this.getWidgetProps(id));
407
- });
408
-
409
- if (
410
- this.props.serializedState &&
411
- !_.isEqual(this.props.serializedState, this.getSerializedState())
412
- ) {
413
- this.restoreSerializedState(this.props.serializedState);
414
- }
415
-
416
- if (this.props.linterContext.highlightLint) {
417
- // Get i18n lint errors asynchronously. If lint errors have changed
418
- // since the last run, this component will be rerendered.
419
- this._translationLinter.runLinter(
420
- this.props.content,
421
- this.handletranslationLintErrors,
422
- );
423
- }
424
- }
425
-
426
- componentWillUnmount() {
427
- // Clean out the list of widgetIds when unmounting, as this list is
428
- // meant to be consistent with the refs controlled by the renderer, and
429
- // refs are also cleared out during unmounting.
430
- // (This may not be totally necessary, but mobile clients have been
431
- // seeing JS errors due to an inconsistency between the list of
432
- // widgetIds and the child refs of the renderer.
433
- // See: https://phabricator.khanacademy.org/D32420.)
434
- this.widgetIds = [];
435
-
436
- if (this.translationIndex != null) {
437
- // NOTE(jeremy): Since the translationIndex is simply the array
438
- // index of each renderer, we can't remove Renderers from this
439
- // list, rather, we simply null out the entry (which means that
440
- // this array's growth is unbounded until a page reload).
441
- getDependencies().rendererTranslationComponents.removeComponentAtIndex(
442
- this.translationIndex,
443
- );
444
- }
445
-
446
- this._isMounted = false;
447
- }
448
-
449
- getApiOptions: () => APIOptionsWithDefaults = () => {
450
- return {
451
- ...ApiOptions.defaults,
452
- ...this.props.apiOptions,
453
- };
454
- };
455
-
456
- _getInitialWidgetState: (props: Props) => {
457
- widgetInfo: State["widgetInfo"];
458
- widgetProps: State["widgetProps"];
459
- } = (props: Props) => {
460
- const allWidgetInfo = this._getAllWidgetsInfo(props);
461
- return {
462
- widgetInfo: allWidgetInfo,
463
- widgetProps: this._getAllWidgetsStartProps(allWidgetInfo, props),
464
- };
465
- };
466
-
467
- // @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
468
- _getAllWidgetsInfo: (props: Props) => {
469
- [key: string]: PerseusWidget;
470
- } = (props: Props) => {
471
- // @ts-expect-error - TS2345 - Argument of type '{ [key: string]: PerseusWidget; }' is not assignable to parameter of type 'Partial<Record<string, CategorizerWidget>>'.
472
- return mapObject(props.widgets, (widgetInfo, widgetId) => {
473
- if (!widgetInfo.type || !widgetInfo.alignment) {
474
- const newValues: Record<string, any> = {};
475
-
476
- if (!widgetInfo.type) {
477
- newValues.type = widgetId.split(" ")[0];
478
- }
479
- if (!widgetInfo.alignment) {
480
- newValues.alignment = "default";
481
- }
482
-
483
- widgetInfo = _.extend({}, widgetInfo, newValues);
484
- }
485
- return Widgets.upgradeWidgetInfoToLatestVersion(widgetInfo);
486
- });
487
- };
488
-
489
- _getAllWidgetsStartProps: (
490
- allWidgetInfo: PerseusWidgetMap,
491
- props: Props,
492
- ) => PerseusWidgetMap = (allWidgetInfo, props) => {
493
- const {apiOptions, problemNum} = props;
494
- const widgetsStartProps: PerseusWidgetMap = {};
495
- entries(allWidgetInfo).forEach(([key, widgetInfo]) => {
496
- widgetsStartProps[key] = Widgets.getRendererPropsForWidgetInfo(
497
- widgetInfo,
498
- problemNum,
499
- );
500
- });
501
-
502
- // Call the onWidgetStartProps callback if it exists
503
- apiOptions?.onWidgetStartProps?.(widgetsStartProps);
504
-
505
- return widgetsStartProps;
506
- };
507
-
508
- // This is only used in _getWidgetInfo as a fallback if widgetId
509
- // doesn't exist in this.state.widgetInfo. It doesn't get run as
510
- // part of the happy path.
511
- // TODO(LP-10713): Refactor how we handle widgetIds that don't exist
512
- // in this.state.widgetInfo.
513
- _getDefaultWidgetInfo: (widgetId: string) => any = (widgetId: string) => {
514
- const widgetIdParts = Util.rTypeFromWidgetId.exec(widgetId);
515
- if (widgetIdParts == null) {
516
- // We should probably return null here since there's no
517
- // widget with the given id.
518
- // NOTE(jeremy): Further, the widgetId we were given does not even
519
- // look like a widget ID (ie. `widget-type \d+`). I can't figure
520
- // out how to trigger this line of code though.
521
- /* c8 ignore next line */
522
- return {};
523
- }
524
- return {
525
- type: widgetIdParts[1],
526
- graded: true,
527
- options: {},
528
- };
529
- };
530
-
531
- _getWidgetInfo: (widgetId: string) => PerseusWidget = (
532
- widgetId: string,
533
- ): PerseusWidget => {
534
- return (
535
- this.state.widgetInfo[widgetId] ||
536
- this._getDefaultWidgetInfo(widgetId)
537
- );
538
- };
539
-
540
- renderWidget: (
541
- impliedType: string,
542
- id: string,
543
- state: WidgetState,
544
- ) => null | React.ReactNode = (
545
- impliedType: string,
546
- id: string,
547
- state: WidgetState,
548
- ) => {
549
- const widgetInfo = this.state.widgetInfo[id];
550
-
551
- if (widgetInfo && widgetInfo.alignment === "full-width") {
552
- state.foundFullWidth = true;
553
- }
554
-
555
- if (widgetInfo) {
556
- const type = (widgetInfo && widgetInfo.type) || impliedType;
557
- const shouldHighlight = _.contains(
558
- // @ts-expect-error - TS2345 - Argument of type 'readonly any[] | undefined' is not assignable to parameter of type 'Collection<any>'.
559
- this.props.highlightedWidgets,
560
- id,
561
- );
562
-
563
- // By this point we should have no duplicates, which are
564
- // filtered out in this.render(), so we shouldn't have to
565
- // worry about using this widget key and ref:
566
- return (
567
- <WidgetContainer
568
- key={makeContainerId(id)}
569
- id={id}
570
- ref={(node) => {
571
- const containerId = makeContainerId(id);
572
- if (node != null) {
573
- this._widgetContainers.set(containerId, node);
574
- } else {
575
- this._widgetContainers.delete(containerId);
576
- }
577
- }}
578
- type={type}
579
- initialProps={this.getWidgetProps(id)}
580
- shouldHighlight={shouldHighlight}
581
- linterContext={PerseusLinter.pushContextStack(
582
- this.props.linterContext,
583
- "widget",
584
- )}
585
- />
586
- );
587
- }
588
- return null;
589
- };
590
-
591
- getWidgetProps: (id: string) => WidgetProps<any, PerseusWidgetOptions> = (
592
- id,
593
- ) => {
594
- const apiOptions = this.getApiOptions();
595
- const widgetProps = this.state.widgetProps[id] || {};
596
-
597
- // The widget needs access to its "rubric" at all times when in review
598
- // mode (which is really just part of its widget info).
599
- let reviewModeRubric = null;
600
- const widgetInfo = this.state.widgetInfo[id];
601
- if (this.props.reviewMode && widgetInfo) {
602
- reviewModeRubric = widgetInfo.options;
603
- }
604
-
605
- if (!this._interactionTrackers) {
606
- this._interactionTrackers = {};
607
- }
608
-
609
- let interactionTracker = this._interactionTrackers[id];
610
- if (!interactionTracker) {
611
- interactionTracker = this._interactionTrackers[id] =
612
- new InteractionTracker(
613
- apiOptions.trackInteraction,
614
- // @ts-expect-error - TS2345 - Argument of type 'string | null | undefined' is not assignable to parameter of type 'string'.
615
- widgetInfo && widgetInfo.type,
616
- id,
617
- // @ts-expect-error - TS2345 - Argument of type 'string | null | undefined' is not assignable to parameter of type 'string'.
618
- Widgets.getTracking(widgetInfo && widgetInfo.type),
619
- );
620
- }
621
-
622
- return {
623
- ...widgetProps,
624
- widgetId: id,
625
- alignment: widgetInfo && widgetInfo.alignment,
626
- // When determining if a widget is static, we verify that the widget is not an
627
- // exercise question by verifying that it has no problem number.
628
- static: widgetInfo && widgetInfo.static && !this.props.problemNum,
629
- problemNum: this.props.problemNum,
630
- apiOptions: this.getApiOptions(),
631
- keypadElement: this.props.keypadElement,
632
- questionCompleted: this.props.questionCompleted,
633
- onFocus: _.partial(this._onWidgetFocus, id),
634
- onBlur: _.partial(this._onWidgetBlur, id),
635
- findWidgets: this.findWidgets,
636
- reviewModeRubric: reviewModeRubric,
637
- onChange: (newProps, cb, silent = false) => {
638
- this._setWidgetProps(id, newProps, cb, silent);
639
- },
640
- trackInteraction: interactionTracker.track,
641
- isLastUsedWidget: id === this.state.lastUsedWidgetId,
642
- };
643
- };
644
-
645
- /**
646
- * Serializes the questions state so it can be recovered.
647
- *
648
- * The return value of this function can be sent to the
649
- * `restoreSerializedState` method to restore this state.
650
- *
651
- * If an instance of widgetProps is passed in, it generates the serialized
652
- * state from that instead of the current widget props.
653
- */
654
- getSerializedState: (widgetProps?: any) => {
655
- [id: string]: any;
656
- } = (
657
- widgetProps: any,
658
- ): {
659
- [id: string]: any;
660
- } => {
661
- return mapObject(
662
- widgetProps || this.state.widgetProps,
663
- (props, widgetId) => {
664
- const widget = this.getWidgetInstance(widgetId);
665
- if (widget && widget.getSerializedState) {
666
- return widget.getSerializedState();
667
- }
668
- return props;
669
- },
670
- );
671
- };
672
-
673
- restoreSerializedState: (
674
- serializedState: SerializedState,
675
- callback?: () => void,
676
- ) => void = (serializedState: SerializedState, callback?: () => void) => {
677
- // Do some basic validation on the serialized state (just make sure the
678
- // widget IDs are what we expect).
679
- const serializedWidgetIds = _.keys(serializedState);
680
- const widgetPropIds = _.keys(this.state.widgetProps);
681
-
682
- // If the two lists of IDs match (ignoring order)
683
- if (
684
- serializedWidgetIds.length !== widgetPropIds.length ||
685
- _.intersection(serializedWidgetIds, widgetPropIds).length !==
686
- serializedWidgetIds.length
687
- ) {
688
- Log.error(
689
- "Refusing to restore bad serialized state:",
690
- Errors.Internal,
691
- {
692
- loggedMetadata: {
693
- serializedState: JSON.stringify(serializedState),
694
- currentProps: JSON.stringify(this.state.widgetProps),
695
- },
696
- },
697
- );
698
- return;
699
- }
700
-
701
- // We want to wait until any children widgets who have a
702
- // restoreSerializedState function also call their own callbacks before
703
- // we declare that the operation is finished.
704
- let numCallbacks = 1;
705
- const fireCallback = () => {
706
- --numCallbacks;
707
- if (callback && numCallbacks === 0) {
708
- callback();
709
- }
710
- };
711
-
712
- this.setState(
713
- {
714
- widgetProps: mapObject(serializedState, (props, widgetId) => {
715
- const widget = this.getWidgetInstance(widgetId);
716
- if (widget && widget.restoreSerializedState) {
717
- // Note that we probably can't call
718
- // `this.change()/this.props.onChange()` in this
719
- // function, so we take the return value and use
720
- // that as props if necessary so that
721
- // `restoreSerializedState` in a widget can
722
- // change the props as well as state.
723
- // If a widget has no props to change, it can
724
- // safely return null.
725
- ++numCallbacks;
726
- const restoreResult = widget.restoreSerializedState(
727
- props,
728
- fireCallback,
729
- );
730
- return _.extend(
731
- {},
732
- this.state.widgetProps[widgetId],
733
- restoreResult,
734
- );
735
- }
736
- return props;
737
- }),
738
- },
739
- () => {
740
- // Wait until all components have rendered. In React 16 setState
741
- // callback fires immediately after this componentDidUpdate, and
742
- // there is no guarantee that parent/siblings components have
743
- // finished rendering.
744
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API
745
- // eslint-disable-next-line no-restricted-syntax
746
- setTimeout(fireCallback, 0);
747
- },
748
- );
749
- };
750
-
751
- /**
752
- * Tell each of the radio widgets to show rationales for each of the
753
- * currently selected choices inside of them. If the widget is correct, it
754
- * shows rationales for all of the choices. This also disables interaction
755
- * with the choices that we show rationales for.
756
- */
757
- showRationalesForCurrentlySelectedChoices: () => void = () => {
758
- Object.keys(this.props.widgets).forEach((widgetId) => {
759
- const widget = this.getWidgetInstance(widgetId);
760
- if (widget && widget.showRationalesForCurrentlySelectedChoices) {
761
- widget.showRationalesForCurrentlySelectedChoices(
762
- this._getWidgetInfo(widgetId).options,
763
- );
764
- }
765
- });
766
- };
767
-
768
- /**
769
- * Tells each of the radio widgets to deselect any of the incorrect choices
770
- * that are currently selected (leaving correct choices still selected).
771
- */
772
- deselectIncorrectSelectedChoices: () => void = () => {
773
- // TODO(emily): this has the exact same structure as
774
- // showRationalesForCurrentlySelectedChoices above. Maybe DRY this up.
775
- Object.keys(this.props.widgets).forEach((widgetId) => {
776
- const widget = this.getWidgetInstance(widgetId);
777
- if (widget && widget.deselectIncorrectSelectedChoices) {
778
- widget.deselectIncorrectSelectedChoices();
779
- }
780
- });
781
- };
782
-
783
- /**
784
- * Allows inter-widget communication.
785
- *
786
- * This function yields this Renderer's own internal widgets, and it's used
787
- * in two places.
788
- *
789
- * First, we expose our own internal widgets to each other by giving them
790
- * a `findWidgets` function that, in turn, calls this function.
791
- *
792
- * Second, we expose our own internal widgets to this Renderer's parent,
793
- * by allowing it to call this function directly. That way, it can hook us
794
- * up to other Renderers on the page, by writing a `findExternalWidgets`
795
- * prop that calls each other Renderer's `findInternalWidgets` function.
796
- *
797
- * Takes a `filterCriterion` on which widgets to return.
798
- * `filterCriterion` can be one of:
799
- * * A string widget id
800
- * * A string widget type
801
- * * a function from (id, widgetInfo, widgetComponent) to true or false
802
- *
803
- * Returns an array of the matching widget components.
804
- *
805
- * If you need to do logic with more than the components, it is possible
806
- * to do such logic inside the filter, rather than on the result array.
807
- *
808
- * See the passage-ref widget for an example.
809
- *
810
- * "Remember: abilities are not inherently good or evil, it's how you use
811
- * them." ~ Kyle Katarn
812
- * Please use this one with caution.
813
- */
814
- findInternalWidgets: (
815
- filterCriterion: FilterCriterion,
816
- ) => ReadonlyArray<Widget | null | undefined> = (
817
- filterCriterion: FilterCriterion,
818
- ) => {
819
- let filterFunc;
820
- // Convenience filters:
821
- // "interactive-graph 3" will give you [[interactive-graph 3]]
822
- // "interactive-graph" will give you all interactive-graphs
823
- if (typeof filterCriterion === "string") {
824
- if (filterCriterion.indexOf(" ") !== -1) {
825
- const widgetId = filterCriterion;
826
- filterFunc = (
827
- id: string,
828
- widgetInfo: PerseusWidget,
829
- widget?: Widget | null,
830
- ) => id === widgetId;
831
- } else {
832
- const widgetType = filterCriterion;
833
- filterFunc = (
834
- id: string,
835
- widgetInfo: PerseusWidget,
836
- widget?: Widget | null,
837
- ) => {
838
- return widgetInfo.type === widgetType;
839
- };
840
- }
841
- } else {
842
- filterFunc = filterCriterion;
843
- }
844
-
845
- const results: ReadonlyArray<Widget | null | undefined> = this.widgetIds
846
- .filter((id: string) => {
847
- const widgetInfo = this._getWidgetInfo(id);
848
- const widget = this.getWidgetInstance(id);
849
- return filterFunc(id, widgetInfo, widget);
850
- })
851
- .map(this.getWidgetInstance);
852
-
853
- return results;
854
- };
855
-
856
- /**
857
- * Allows inter-widget communication.
858
- *
859
- * Includes both widgets internal to this Renderer, and external widgets
860
- * exposed by the `findExternalWidgets` prop.
861
- *
862
- * See `findInteralWidgets` for more information.
863
- */
864
- findWidgets: (filterCriterion: FilterCriterion) => any = (
865
- filterCriterion: FilterCriterion,
866
- ) => {
867
- return [
868
- ...this.findInternalWidgets(filterCriterion),
869
- ...this.props.findExternalWidgets(filterCriterion),
870
- ];
871
- };
872
-
873
- getWidgetInstance: (id: string) => Widget | null | undefined = (id) => {
874
- const ref = this._widgetContainers.get(makeContainerId(id));
875
- if (!ref) {
876
- return null;
877
- }
878
- return ref.getWidget();
879
- };
880
-
881
- _onWidgetFocus: (id: string, focusPath?: ReadonlyArray<string>) => void = (
882
- id: string,
883
- focusPath: ReadonlyArray<string> = [],
884
- ) => {
885
- if (!_.isArray(focusPath)) {
886
- throw new PerseusError(
887
- "widget props.onFocus focusPath must be an Array, " +
888
- "but was" +
889
- JSON.stringify(focusPath),
890
- Errors.Internal,
891
- );
892
- }
893
- this._setCurrentFocus([id].concat(focusPath));
894
- };
895
-
896
- _onWidgetBlur: (id: string, blurPath: FocusPath) => void = (
897
- id: string,
898
- blurPath: FocusPath,
899
- ) => {
900
- const blurringFocusPath = this._currentFocus;
901
-
902
- // Failsafe: abort if ID is different, because focus probably happened
903
- // before blur
904
- // @ts-expect-error - TS2769 - No overload matches this call.
905
- const fullPath = [id].concat(blurPath);
906
- if (!_.isEqual(fullPath, blurringFocusPath)) {
907
- return;
908
- }
909
-
910
- // Wait until after any new focus events fire this tick before
911
- // declaring that nothing is focused.
912
- // If a different widget was focused, we'll see an onBlur event
913
- // now, but then an onFocus event on a different element before
914
- // this callback is executed
915
- _.defer(() => {
916
- if (_.isEqual(this._currentFocus, blurringFocusPath)) {
917
- this._setCurrentFocus(null);
918
- }
919
- });
920
- };
921
-
922
- getContent: (props: Props, state: State) => any = (
923
- props: Props,
924
- state: State,
925
- ) => {
926
- return state.jiptContent || props.content;
927
- };
928
-
929
- shouldRenderJiptPlaceholder: (props: Props, state: State) => boolean = (
930
- props: Props,
931
- state: State,
932
- ): boolean => {
933
- // TODO(aria): Pass this in via webapp as an apiOption
934
- return (
935
- getDependencies().JIPT.useJIPT &&
936
- state.jiptContent == null &&
937
- props.content.indexOf("crwdns") !== -1
938
- );
939
- };
940
-
941
- replaceJiptContent: (content: string, paragraphIndex: number) => void = (
942
- content: string,
943
- paragraphIndex: number,
944
- ) => {
945
- if (paragraphIndex == null) {
946
- // we're not translating paragraph-wise; replace the whole content
947
- // (we could also theoretically check for apiOptions.isArticle
948
- // here, which is what causes paragraphIndex to not be null)
949
- this.setState({
950
- jiptContent: content,
951
- });
952
- } else {
953
- // This is the same regex we use in perseus/translate.py to find
954
- // code blocks. We use it to count entire code blocks as
955
- // paragraphs.
956
- const codeFenceRegex =
957
- /^\s*(`{3,}|~{3,})\s*(\S+)?\s*\n([\s\S]+?)\s*\1\s*$/;
958
-
959
- if (codeFenceRegex.test(content)) {
960
- // If a paragraph is a code block, we're going to treat it as a
961
- // single paragraph even if it has double-newlines in it, so
962
- // skip the next two checks.
963
- } else if (/\S\n\s*\n\S/.test(content)) {
964
- // Our "render the exact same QuestionParagraphs each time"
965
- // strategy will fail if we allow translating a paragraph
966
- // to more than one paragraph. This hack renders as a single
967
- // paragraph and lets the translator know to not use \n\n,
968
- // hopefully. We can't wait for linting because we can't
969
- // safely render the node.
970
- // TODO(aria): Check for the max number of backticks or tildes
971
- // in the content, and just render a red code block of the
972
- // content here instead?
973
- content =
974
- "$\\large{\\red{\\text{Please translate each " +
975
- "paragraph to a single paragraph.}}}$";
976
- } else if (/^\s*$/.test(content)) {
977
- // We similarly can't have an all-whitespace paragraph, or
978
- // we will parse it as the closing of the previous paragraph
979
- content =
980
- "$\\large{\\red{\\text{Translated paragraph is " +
981
- "currently empty}}}$";
982
- }
983
- // Split the paragraphs; we have to use getContent() in case
984
- // nothing has been translated yet (in which case we just have
985
- // this.props.content)
986
- const allContent = this.getContent(this.props, this.state);
987
- const paragraphs = JiptParagraphs.parseToArray(allContent);
988
- paragraphs[paragraphIndex] = content;
989
- this.setState({
990
- jiptContent: JiptParagraphs.joinFromArray(paragraphs),
991
- });
992
- }
993
- };
994
-
995
- // wrap top-level elements in a QuestionParagraph, mostly
996
- // for appropriate spacing and other css
997
- // @ts-expect-error - TS2322 - Type '(ast: any, state: WidgetState) => never[] | JSX.Element' is not assignable to type '(ast: any, state: WidgetState) => ReactElement<any, string | JSXElementConstructor<any>>'.
998
- outputMarkdown: (ast: any, state: WidgetState) => React.ReactElement = (
999
- ast: any,
1000
- state: WidgetState,
1001
- ) => {
1002
- if (_.isArray(ast)) {
1003
- // This is duplicated from simple-markdown
1004
- // TODO(aria): Don't duplicate this logic
1005
- const oldKey = state.key;
1006
- const result = [];
1007
-
1008
- // map nestedOutput over the ast, except group any text
1009
- // nodes together into a single string output.
1010
- // NOTE(aria): These are never strings--always QuestionParagraphs
1011
- // TODO(aria): We probably don't need this string logic here.
1012
- let lastWasString = false;
1013
- for (let i = 0; i < ast.length; i++) {
1014
- state.key = i;
1015
- state.paragraphIndex = i;
1016
- const nodeOut = this.outputMarkdown(ast[i], state);
1017
- const isString = typeof nodeOut === "string";
1018
- // NOTE(jeremy): As far as I can tell, this if is _never_
1019
- // reached. As noted above, these are always QuestionParagraphs
1020
- // now.
1021
- /* c8 ignore if */
1022
- if (typeof nodeOut === "string" && lastWasString) {
1023
- // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'never'.
1024
- result[result.length - 1] += nodeOut;
1025
- } else {
1026
- // @ts-expect-error - TS2345 - Argument of type 'ReactElement<any, string | JSXElementConstructor<any>>' is not assignable to parameter of type 'never'.
1027
- result.push(nodeOut);
1028
- }
1029
- lastWasString = isString;
1030
- }
1031
-
1032
- state.key = oldKey;
1033
- return result;
1034
- }
1035
- // !!! WARNING: Mutative hacks! mutates `this._foundTextNodes`:
1036
- // because I wrote a bad interface to simple-markdown.js' `output`
1037
- this._foundTextNodes = false;
1038
- state.foundFullWidth = false;
1039
- const output = this.outputNested(ast, state);
1040
-
1041
- // In Jipt-land, we need to render the exact same outer
1042
- // QuestionParagraph nodes always. This means the number of
1043
- // paragraphs needs to stay the same, and we can't modify
1044
- // the classnames on the QuestionParagraphs or we'll destroy
1045
- // the crowdin classnames. So we just only use the
1046
- // 'paragraph' classname from the QuestionParagraph.
1047
- // If this becomes a problem it would be easy to fix by wrapping
1048
- // the nodes in an extra layer (hopefully only for jipt) that
1049
- // handles the jipt classnames, and let this layer handle the
1050
- // dynamic classnames.
1051
- // We can't render the classes the first time and leave them
1052
- // the same because we don't know at the time of the first
1053
- // render whether they are full-bleed or centered, since they
1054
- // only contain crowdin IDs like `crwdns:972384209:0...`
1055
- let className;
1056
- if (this.translationIndex != null) {
1057
- className = null;
1058
- } else {
1059
- className = classNames({
1060
- "perseus-paragraph-centered": !this._foundTextNodes,
1061
- // There is only one node being rendered,
1062
- // and it's a full-width widget.
1063
- "perseus-paragraph-full-width":
1064
- state.foundFullWidth && ast.content.length === 1,
1065
- });
1066
- }
1067
-
1068
- return (
1069
- <QuestionParagraph
1070
- key={state.key}
1071
- className={className}
1072
- translationIndex={this.translationIndex}
1073
- paragraphIndex={state.paragraphIndex}
1074
- inline={this.props.inline}
1075
- >
1076
- <ErrorBoundary>{output}</ErrorBoundary>
1077
- </QuestionParagraph>
1078
- );
1079
- };
1080
-
1081
- // output non-top-level nodes or arrays
1082
- outputNested: (ast: any, state: WidgetState) => React.ReactElement = (
1083
- ast: any,
1084
- state: WidgetState,
1085
- ) => {
1086
- if (_.isArray(ast)) {
1087
- // This is duplicated from simple-markdown
1088
- // TODO(aria): Don't duplicate this logic
1089
- const oldKey = state.key;
1090
- const result: Array<string | React.ReactNode> = [];
1091
-
1092
- // map nestedOutput over the ast, except group any text
1093
- // nodes together into a single string output.
1094
- let lastWasString = false;
1095
- for (let i = 0; i < ast.length; i++) {
1096
- state.key = i;
1097
- const nodeOut = this.outputNested(ast[i], state);
1098
- const isString = typeof nodeOut === "string";
1099
- if (typeof nodeOut === "string" && lastWasString) {
1100
- /**
1101
- * We know that last was string, but TypeScript can't see this
1102
- * refinement.
1103
- */
1104
- result[result.length - 1] += nodeOut;
1105
- } else {
1106
- result.push(nodeOut);
1107
- }
1108
- lastWasString = isString;
1109
- }
1110
-
1111
- state.key = oldKey;
1112
- return result;
1113
- }
1114
- return this.outputNode(ast, this.outputNested, state);
1115
- };
1116
-
1117
- // output individual AST nodes [not arrays]
1118
- outputNode: (
1119
- node: any,
1120
- nestedOutput: any,
1121
- state: WidgetState,
1122
- ) =>
1123
- | any
1124
- | null
1125
- | React.ReactElement<React.ComponentProps<"div">>
1126
- | React.ReactElement<React.ComponentProps<"span">>
1127
- | React.ReactNode = (
1128
- node: any,
1129
- nestedOutput: any,
1130
- state: WidgetState,
1131
- ) => {
1132
- const apiOptions = this.getApiOptions();
1133
- const imagePlaceholder = apiOptions.imagePlaceholder;
1134
-
1135
- if (node.type === "widget") {
1136
- const widgetPlaceholder = apiOptions.widgetPlaceholder;
1137
-
1138
- if (widgetPlaceholder) {
1139
- return widgetPlaceholder;
1140
- }
1141
- // Widgets can contain text nodes, so we don't center them with
1142
- // markdown magic here.
1143
- // Instead, we center them with css magic in articles.less
1144
- // /cry(aria)
1145
- this._foundTextNodes = true;
1146
-
1147
- if (_.contains(this.widgetIds, node.id)) {
1148
- // We don't want to render a duplicate widget key/ref,
1149
- // as this causes problems with react (for obvious
1150
- // reasons). Instead we just notify the
1151
- // hopefully-content-creator that they need to change the
1152
- // widget id.
1153
- return (
1154
- <span key={state.key} className="renderer-widget-error">
1155
- {[
1156
- "Widget [[",
1157
- "☃",
1158
- " ",
1159
- node.id,
1160
- "]] already exists.",
1161
- ].join("")}
1162
- </span>
1163
- );
1164
- }
1165
- this.widgetIds.push(node.id);
1166
- return this.renderWidget(node.widgetType, node.id, state);
1167
- }
1168
- if (node.type === "blockMath") {
1169
- // We render math here instead of in perseus-markdown.jsx
1170
- // because we need to pass it our onRender callback.
1171
- const content = preprocessTex(node.content);
1172
-
1173
- const innerStyle = {
1174
- // HACK(benkomalo): we only want horizontal scrolling, but
1175
- // overflowX: 'auto' causes a vertical scrolling scrollbar
1176
- // as well, despite the parent and child elements having
1177
- // the exact same height. Force it to not scroll by
1178
- // applying overflowY: 'hidden'
1179
- overflowX: "auto",
1180
- overflowY: "hidden",
1181
-
1182
- // HACK(kevinb): overflowY: 'hidden' inadvertently clips the
1183
- // top and bottom of some fractions. We add padding to the
1184
- // top and bottom to avoid the clipping and then correct for
1185
- // the padding by adding equal but opposite margins.
1186
- paddingTop: 10,
1187
- paddingBottom: 10,
1188
- marginTop: -10,
1189
- marginBottom: -10,
1190
- } as const;
1191
-
1192
- if (apiOptions.isMobile) {
1193
- // The style for the body of articles and exercises on mobile is
1194
- // to have a 16px margin. When a user taps to zoom math we'd
1195
- // like the math to extend all the way to the edge of the page/
1196
- // To achieve this affect we nest the Zoomable component in two
1197
- // nested divs. The outer div has a negative margin to
1198
- // counteract the margin on main perseus container. The inner
1199
- // div adds the margin back as padding so that when the math is
1200
- // scaled out it's inset from the edge of the page. When the
1201
- // TeX component is full size it will extend to the edge of the
1202
- // page if it's larger than the page.
1203
- //
1204
- // TODO(kevinb) automatically determine the margin size
1205
- const margin = 16;
1206
- const outerStyle = {
1207
- marginLeft: -margin,
1208
- marginRight: -margin,
1209
- } as const;
1210
- const horizontalPadding = {
1211
- paddingLeft: margin,
1212
- paddingRight: margin,
1213
- } as const;
1214
-
1215
- const mobileInnerStyle = {
1216
- ...innerStyle,
1217
- ...styles.mobileZoomableParentFix,
1218
- } as const;
1219
-
1220
- return (
1221
- <div
1222
- key={state.key}
1223
- className="perseus-block-math"
1224
- style={outerStyle}
1225
- >
1226
- <ErrorBoundary>
1227
- <div
1228
- className="perseus-block-math-inner"
1229
- style={{
1230
- ...mobileInnerStyle,
1231
- ...horizontalPadding,
1232
- }}
1233
- >
1234
- <ZoomableTeX>{content}</ZoomableTeX>
1235
- </div>
1236
- </ErrorBoundary>
1237
- </div>
1238
- );
1239
- }
1240
- return (
1241
- <div key={state.key} className="perseus-block-math">
1242
- <ErrorBoundary>
1243
- <div
1244
- className="perseus-block-math-inner"
1245
- style={innerStyle}
1246
- >
1247
- <AssetContext.Consumer>
1248
- {({setAssetStatus}) => (
1249
- <TeX setAssetStatus={setAssetStatus}>
1250
- {content}
1251
- </TeX>
1252
- )}
1253
- </AssetContext.Consumer>
1254
- </div>
1255
- </ErrorBoundary>
1256
- </div>
1257
- );
1258
- }
1259
- if (node.type === "math") {
1260
- // Replace uses of \begin{align}...\end{align} which KaTeX doesn't
1261
- // support (yet) with \begin{aligned}...\end{aligned} which renders
1262
- // the same is supported by KaTeX. It does the same for align*.
1263
- // TODO(kevinb) update content to use aligned instead of align.
1264
- const tex = node.content.replace(/\{align[*]?\}/g, "{aligned}");
1265
-
1266
- // We render math here instead of in perseus-markdown.jsx
1267
- // because we need to pass it our onRender callback.
1268
- return (
1269
- <span
1270
- key={state.key}
1271
- style={{
1272
- // If math is directly next to text, don't let it
1273
- // wrap to the next line
1274
- whiteSpace: "nowrap",
1275
- }}
1276
- >
1277
- <ErrorBoundary>
1278
- {/* We add extra empty spans around the math to make it not
1279
- wrap (I don't know why this works, but it does) */}
1280
- <span />
1281
- <AssetContext.Consumer>
1282
- {({setAssetStatus}) => (
1283
- <TeX
1284
- onRender={this.props.onRender}
1285
- setAssetStatus={setAssetStatus}
1286
- >
1287
- {tex}
1288
- </TeX>
1289
- )}
1290
- </AssetContext.Consumer>
1291
- <span />
1292
- </ErrorBoundary>
1293
- </span>
1294
- );
1295
- }
1296
- if (node.type === "image") {
1297
- if (imagePlaceholder) {
1298
- return imagePlaceholder;
1299
- }
1300
-
1301
- // We need to add width and height to images from our
1302
- // props.images mapping.
1303
-
1304
- // We do a _.has check here to avoid weird things like
1305
- // 'toString' or '__proto__' as a url.
1306
- const extraAttrs = _.has(this.props.images, node.target)
1307
- ? this.props.images[node.target]
1308
- : null;
1309
-
1310
- // The width of a table column is determined by the widest table
1311
- // cell within that column, but responsive images constrain
1312
- // themselves to the width of their parent containers. Thus,
1313
- // responsive images don't do very well within tables. To avoid
1314
- // haphazard sizing, simply make images within tables unresponsive.
1315
- // TODO(alex): Make tables themselves responsive.
1316
- const responsive = !state.inTable;
1317
- return (
1318
- <ErrorBoundary key={state.key}>
1319
- <AssetContext.Consumer>
1320
- {({setAssetStatus}) => (
1321
- <SvgImage
1322
- setAssetStatus={setAssetStatus}
1323
- // @ts-expect-error - TS2322 - Type 'string | null | undefined' is not assignable to type 'string | undefined'.
1324
- src={PerseusMarkdown.sanitizeUrl(node.target)}
1325
- alt={node.alt}
1326
- title={node.title}
1327
- responsive={responsive}
1328
- onUpdate={this.props.onRender}
1329
- zoomToFullSizeOnMobile={
1330
- apiOptions.isMobile && apiOptions.isArticle
1331
- }
1332
- {...extraAttrs}
1333
- />
1334
- )}
1335
- </AssetContext.Consumer>
1336
- </ErrorBoundary>
1337
- );
1338
- }
1339
- if (node.type === "columns") {
1340
- // Note that we have two columns. This is so we can put
1341
- // a className on the outer renderer content for SAT.
1342
- // TODO(aria): See if there is a better way we can do
1343
- // things like this
1344
- this._isTwoColumn = true;
1345
- // but then render normally:
1346
- return (
1347
- <ErrorBoundary key={state.key}>
1348
- {PerseusMarkdown.ruleOutput(node, nestedOutput, state)}
1349
- </ErrorBoundary>
1350
- );
1351
- }
1352
- if (node.type === "text") {
1353
- if (rContainsNonWhitespace.test(node.content)) {
1354
- this._foundTextNodes = true;
1355
- }
1356
-
1357
- // Used by the translator portal to replace image URLs with
1358
- // placeholders, see preprocessWidgets in manticore-utils.js
1359
- // for more details.
1360
- if (imagePlaceholder && rImageURL.test(node.content)) {
1361
- return imagePlaceholder;
1362
- }
1363
- return node.content;
1364
- }
1365
- if (node.type === "table" || node.type === "titledTable") {
1366
- const output = PerseusMarkdown.ruleOutput(node, nestedOutput, {
1367
- ...state,
1368
- isMobile: apiOptions.isMobile,
1369
- inTable: true,
1370
- });
1371
-
1372
- if (!apiOptions.isMobile) {
1373
- return output;
1374
- }
1375
-
1376
- const margin = 16;
1377
- const outerStyle = {
1378
- marginLeft: -margin,
1379
- marginRight: -margin,
1380
- } as const;
1381
- const innerStyle = {
1382
- paddingLeft: 0,
1383
- paddingRight: 0,
1384
- } as const;
1385
- const mobileInnerStyle = {
1386
- ...innerStyle,
1387
- ...styles.mobileZoomableParentFix,
1388
- } as const;
1389
-
1390
- const wrappedOutput = (
1391
- <div style={{...mobileInnerStyle, overflowX: "auto"}}>
1392
- <ErrorBoundary>
1393
- <Zoomable animateHeight={true}>{output}</Zoomable>
1394
- </ErrorBoundary>
1395
- </div>
1396
- );
1397
-
1398
- // TODO(benkomalo): how should we deal with tappable items inside
1399
- // of tables?
1400
- return <div style={outerStyle}>{wrappedOutput}</div>;
1401
- }
1402
- // If it's a "normal" or "simple" markdown node, just
1403
- // output it using its output rule.
1404
- return (
1405
- <ErrorBoundary key={state.key}>
1406
- {PerseusMarkdown.ruleOutput(node, nestedOutput, state)}
1407
- </ErrorBoundary>
1408
- );
1409
- };
1410
-
1411
- handleRender: (prevProps: Props) => void = (prevProps: Props) => {
1412
- const onRender = this.props.onRender;
1413
- const oldOnRender = prevProps.onRender;
1414
-
1415
- // In the common case of no callback specified, avoid this work.
1416
- if (onRender !== noopOnRender || oldOnRender !== noopOnRender) {
1417
- // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'find' does not exist on type 'JQueryStatic'.
1418
- const $images = $(ReactDOM.findDOMNode(this)).find("img");
1419
-
1420
- // Fire callback on image load...
1421
- if (oldOnRender !== noopOnRender) {
1422
- $images.off("load", oldOnRender);
1423
- }
1424
-
1425
- if (onRender !== noopOnRender) {
1426
- $images.on("load", onRender);
1427
- }
1428
- }
1429
-
1430
- // ...as well as right now (non-image, non-TeX or image from cache)
1431
- onRender();
1432
- };
1433
-
1434
- // Sets the current focus path
1435
- // If the new focus path is not a prefix of the old focus path,
1436
- // we send an onChangeFocus event back to our parent.
1437
- _setCurrentFocus: (path: FocusPath) => void = (path: FocusPath) => {
1438
- const apiOptions = this.getApiOptions();
1439
-
1440
- // We don't do this when the new path is a prefix because
1441
- // that prefix is already focused (we're just in a more specific
1442
- // area of it). This makes it safe to call _setCurrentFocus
1443
- // whenever a widget is interacted with--we won't wipe out
1444
- // our focus state if we are already focused on a subpart
1445
- // of that widget (i.e. a transformation NumberInput inside
1446
- // of a transformer widget).
1447
- if (!isIdPathPrefix(path, this._currentFocus)) {
1448
- const prevFocus = this._currentFocus;
1449
-
1450
- if (prevFocus) {
1451
- this.blurPath(prevFocus);
1452
- }
1453
-
1454
- this._currentFocus = path;
1455
- apiOptions.onFocusChange(this._currentFocus, prevFocus);
1456
- }
1457
- };
1458
-
1459
- focus: () => boolean | null | undefined = () => {
1460
- let id;
1461
- let focusResult;
1462
- for (let i = 0; i < this.widgetIds.length; i++) {
1463
- const widgetId = this.widgetIds[i];
1464
- const widget = this.getWidgetInstance(widgetId);
1465
- const widgetFocusResult = widget && widget.focus && widget.focus();
1466
- if (widgetFocusResult) {
1467
- id = widgetId;
1468
- focusResult = widgetFocusResult;
1469
- break;
1470
- }
1471
- }
1472
-
1473
- if (id) {
1474
- // reconstruct a {path, element} focus object
1475
- let path;
1476
- if (typeof focusResult === "object") {
1477
- // TODO(jeremy): I am 99% sure this path is no longer possible.
1478
- // In D10274, focus management sometimes returned an object
1479
- // with a `path` and `element` key. But later in D11387 and
1480
- // D13664 things have been changed and seemingly removed the
1481
- // object return value.
1482
- // The result of focus was a {path, id} object itself
1483
- path = [id].concat(focusResult.path || []);
1484
- Log.error(
1485
- "Renderer received a focus result of type 'object' " +
1486
- "instead of the expected type 'boolean'",
1487
- Errors.Internal,
1488
- {
1489
- loggedMetadata: {
1490
- focusResult: JSON.stringify(focusResult),
1491
- currentProps: JSON.stringify(
1492
- this.state.widgetProps,
1493
- ),
1494
- },
1495
- },
1496
- );
1497
- } else {
1498
- // The result of focus was true or the like; just
1499
- // construct a root focus object
1500
- path = [id];
1501
- }
1502
-
1503
- this._setCurrentFocus(path);
1504
- return true;
1505
- }
1506
- };
1507
-
1508
- getDOMNodeForPath: (path: FocusPath) => Element | Text | null | undefined =
1509
- (path: FocusPath) => {
1510
- // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
1511
- const widgetId = _.first(path);
1512
- // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
1513
- const interWidgetPath = _.rest(path);
1514
-
1515
- // Widget handles parsing of the interWidgetPath. If the path is empty
1516
- // beyond the widgetID, as a special case we just return the widget's
1517
- // DOM node.
1518
- const widget = this.getWidgetInstance(widgetId);
1519
- const getNode = widget && widget.getDOMNodeForPath;
1520
- if (getNode) {
1521
- return getNode(interWidgetPath);
1522
- }
1523
- if (interWidgetPath.length === 0) {
1524
- // @ts-expect-error - TS2345 - Argument of type 'Widget | null | undefined' is not assignable to parameter of type 'ReactInstance | null | undefined'.
1525
- return ReactDOM.findDOMNode(widget);
1526
- }
1527
- };
1528
-
1529
- getGrammarTypeForPath: (path: FocusPath) => string | null | undefined = (
1530
- path: FocusPath,
1531
- ) => {
1532
- // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
1533
- const widgetId = _.first(path);
1534
- // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
1535
- const interWidgetPath = _.rest(path);
1536
-
1537
- const widget = this.getWidgetInstance(widgetId);
1538
- if (widget && widget.getGrammarTypeForPath) {
1539
- return widget.getGrammarTypeForPath(interWidgetPath);
1540
- }
1541
- };
1542
-
1543
- getInputPaths: () => ReadonlyArray<FocusPath> = () => {
1544
- const inputPaths: Array<FocusPath> = [];
1545
- _.each(this.widgetIds, (widgetId: string) => {
1546
- const widget = this.getWidgetInstance(widgetId);
1547
- if (widget && widget.getInputPaths) {
1548
- // Grab all input paths and add widgetID to the front
1549
- const widgetInputPaths = widget.getInputPaths();
1550
- // Prefix paths with their widgetID and add to collective
1551
- // list of paths.
1552
- // @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
1553
- _.each(widgetInputPaths, (inputPath: string) => {
1554
- const relativeInputPath = [widgetId].concat(inputPath);
1555
- inputPaths.push(relativeInputPath);
1556
- });
1557
- }
1558
- });
1559
-
1560
- return inputPaths;
1561
- };
1562
-
1563
- focusPath: (path: FocusPath) => void = (path: FocusPath) => {
1564
- // No need to focus if it's already focused
1565
- if (_.isEqual(this._currentFocus, path)) {
1566
- return;
1567
- }
1568
- if (this._currentFocus) {
1569
- // Unfocus old path, if exists
1570
- this.blurPath(this._currentFocus);
1571
- }
1572
-
1573
- // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
1574
- const widgetId = _.first(path);
1575
- // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
1576
- const interWidgetPath = _.rest(path);
1577
-
1578
- // Widget handles parsing of the interWidgetPath
1579
- const focusWidget = this.getWidgetInstance(widgetId);
1580
- if (focusWidget && focusWidget.focusInputPath) {
1581
- focusWidget.focusInputPath(interWidgetPath);
1582
- }
1583
- };
1584
-
1585
- blurPath: (path: FocusPath) => void = (path: FocusPath) => {
1586
- // No need to blur if it's not focused
1587
- if (!_.isEqual(this._currentFocus, path)) {
1588
- return;
1589
- }
1590
-
1591
- // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
1592
- const widgetId = _.first(path);
1593
- // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
1594
- const interWidgetPath = _.rest(path);
1595
- const widget = this.getWidgetInstance(widgetId);
1596
- // We might be in the editor and blurring a widget that no
1597
- // longer exists, so only blur if we actually found the widget
1598
- if (widget) {
1599
- const blurWidget = this.getWidgetInstance(widgetId);
1600
- if (blurWidget && blurWidget.blurInputPath) {
1601
- // Widget handles parsing of the interWidgetPath
1602
- blurWidget.blurInputPath(interWidgetPath);
1603
- }
1604
- }
1605
- };
1606
-
1607
- blur: () => void = () => {
1608
- if (this._currentFocus) {
1609
- this.blurPath(this._currentFocus);
1610
- }
1611
- };
1612
-
1613
- // Serializes widget state. Seems to be used only by editors though.
1614
- serialize: () => Record<any, any> = () => {
1615
- const state: Record<string, any> = {};
1616
- _.each(
1617
- this.state.widgetInfo,
1618
- function (info, id) {
1619
- // eslint-disable-next-line @babel/no-invalid-this
1620
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
1621
- // eslint-disable-next-line @babel/no-invalid-this
1622
- const widget = this.getWidgetInstance(id);
1623
- const s = widget.serialize();
1624
- if (!_.isEmpty(s)) {
1625
- state[id] = s;
1626
- }
1627
- },
1628
- this,
1629
- );
1630
- return state;
1631
- };
1632
-
1633
- emptyWidgets: () => any = () => {
1634
- // @ts-expect-error - TS2345 - Argument of type '(id: string) => boolean | undefined' is not assignable to parameter of type 'Iteratee<string[], boolean, string>'.
1635
- return _.filter(this.widgetIds, (id) => {
1636
- const widgetInfo = this._getWidgetInfo(id);
1637
- if (widgetInfo.static) {
1638
- // Static widgets shouldn't count as empty
1639
- return false;
1640
- }
1641
- const widget = this.getWidgetInstance(id);
1642
- if (widget && widget.simpleValidate) {
1643
- const score: PerseusScore = widget.simpleValidate(
1644
- widgetInfo.options,
1645
- // @ts-expect-error - TS2345 - Argument of type 'null' is not assignable to parameter of type '((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined'.
1646
- null,
1647
- );
1648
- return Util.scoreIsEmpty(score);
1649
- }
1650
- });
1651
- };
1652
-
1653
- _setWidgetProps: SetWidgetPropsFn = (id, newProps, cb, silent) => {
1654
- this.setState(
1655
- (prevState) => {
1656
- const widgetProps = {
1657
- ...prevState.widgetProps,
1658
- [id]: {
1659
- ...prevState.widgetProps[id],
1660
- ...newProps,
1661
- },
1662
- } as const;
1663
-
1664
- // Update the `lastUsedWidgetId` to this widget - unless we're
1665
- // in silent mode. We only want to track the last widget that
1666
- // was actually _used_, and silent updates generally don't come
1667
- // from _usage_.
1668
- const lastUsedWidgetId = silent
1669
- ? prevState.lastUsedWidgetId
1670
- : id;
1671
-
1672
- if (!silent) {
1673
- this.props.onSerializedStateUpdated(
1674
- this.getSerializedState(widgetProps),
1675
- );
1676
- }
1677
-
1678
- return {
1679
- lastUsedWidgetId,
1680
- widgetProps,
1681
- };
1682
- },
1683
- () => {
1684
- // Wait until all components have rendered. In React 16 setState
1685
- // callback fires immediately after this componentDidUpdate, and
1686
- // there is no guarantee that parent/siblings components have
1687
- // finished rendering.
1688
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API
1689
- // eslint-disable-next-line no-restricted-syntax
1690
- setTimeout(() => {
1691
- const cbResult = cb && cb();
1692
- if (!silent) {
1693
- this.props.onInteractWithWidget(id);
1694
- }
1695
- if (cbResult !== false) {
1696
- // TODO(jack): For some reason, some widgets don't always
1697
- // end up in refs here, which is repro-able if you make an
1698
- // [[ orderer 1 ]] and copy-paste this, then change it to
1699
- // be an [[ orderer 2 ]]. The resulting Renderer ends up
1700
- // with an "orderer 2" ref but not an "orderer 1" ref.
1701
- // @_@??
1702
- // TODO(jack): Figure out why this is happening and fix it
1703
- // As far as I can tell, this is only an issue in the
1704
- // editor-page, so doing this shouldn't break clients
1705
- // hopefully
1706
- this._setCurrentFocus([id]);
1707
- }
1708
- }, 0);
1709
- },
1710
- );
1711
- };
1712
-
1713
- setInputValue: (
1714
- path: FocusPath,
1715
- newValue: string,
1716
- focus?: () => unknown,
1717
- ) => void = (path, newValue, focus) => {
1718
- // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
1719
- const widgetId = _.first(path);
1720
- // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List<any>'.
1721
- const interWidgetPath = _.rest(path);
1722
- const widget = this.getWidgetInstance(widgetId);
1723
-
1724
- // Widget handles parsing of the interWidgetPath.
1725
- widget?.setInputValue?.(interWidgetPath, newValue, focus);
1726
- };
1727
-
1728
- /**
1729
- * Returns an array of the widget `.getUserInput()` results
1730
- */
1731
- getUserInput: () => ReadonlyArray<WidgetUserInput | null | undefined> =
1732
- () => {
1733
- return this.widgetIds.map((id: string) => {
1734
- const widget = this.getWidgetInstance(id);
1735
- if (widget && widget.getUserInput) {
1736
- // TODO(Jeremy): Add the widget ID in here so we can more
1737
- // easily correlate it to the widget state.
1738
- return widget.getUserInput();
1739
- }
1740
- });
1741
- };
1742
-
1743
- /**
1744
- * Returns an array of all widget IDs in the order they occur in
1745
- * the content.
1746
- */
1747
- getWidgetIds: () => ReadonlyArray<string> = () => {
1748
- return this.widgetIds;
1749
- };
1750
-
1751
- /**
1752
- * Returns the result of `.getUserInput()` for each widget, in
1753
- * a map from widgetId to userInput.
1754
- * NOTE(jeremy): This function is hauntingly similar to `getUserInput` with
1755
- * the major difference being that this function returns a map of
1756
- * `widgetID` => UserInput and `getUserInput` simply returns an array. It
1757
- * would be trivial to map between the results of each of these functions,
1758
- * so we should aim to remove one of these functions.
1759
- */
1760
- getUserInputForWidgets: () => {
1761
- [widgetId: string]: WidgetUserInput | null | undefined;
1762
- } = () => {
1763
- return mapObjectFromArray(this.widgetIds, (id) => {
1764
- const widget = this.getWidgetInstance(id);
1765
- if (widget && widget.getUserInput) {
1766
- return widget.getUserInput();
1767
- }
1768
- });
1769
- };
1770
-
1771
- /**
1772
- * Returns an object mapping from widget ID to perseus-style score.
1773
- * The keys of this object are the values of the array returned
1774
- * from `getWidgetIds`.
1775
- */
1776
- scoreWidgets: () => {
1777
- [widgetId: string]: PerseusScore;
1778
- } = () => {
1779
- const widgetProps = this.state.widgetInfo;
1780
- const onInputError = this.getApiOptions().onInputError;
1781
-
1782
- const gradedWidgetIds = _.filter(this.widgetIds, (id) => {
1783
- const props = widgetProps[id];
1784
- const widgetIsGraded: boolean =
1785
- props?.graded == null || props.graded;
1786
- const widgetIsStatic = !!props?.static;
1787
- // Ungraded widgets or widgets set to static shouldn't be graded.
1788
- return widgetIsGraded && !widgetIsStatic;
1789
- });
1790
-
1791
- const widgetScores: Record<string, PerseusScore> = {};
1792
- _.each(gradedWidgetIds, (id) => {
1793
- const props = widgetProps[id];
1794
- const widget = this.getWidgetInstance(id);
1795
- // widget can be undefined if it hasn't yet been rendered
1796
- if (widget && widget.simpleValidate) {
1797
- widgetScores[id] = widget.simpleValidate(
1798
- {...props?.options, scoring: true},
1799
- onInputError,
1800
- );
1801
- }
1802
- });
1803
-
1804
- return widgetScores;
1805
- };
1806
-
1807
- /**
1808
- * Grades the content.
1809
- */
1810
- score: () => PerseusScore = () => {
1811
- return _.reduce(this.scoreWidgets(), Util.combineScores, Util.noScore);
1812
- };
1813
-
1814
- guessAndScore: () => [any, PerseusScore] = () => {
1815
- const totalGuess = this.getUserInput();
1816
- const totalScore = this.score();
1817
-
1818
- return [totalGuess, totalScore];
1819
- };
1820
-
1821
- examples: () => ReadonlyArray<string> | null | undefined = () => {
1822
- const widgetIds = this.widgetIds;
1823
- const examples = _.compact(
1824
- _.map(widgetIds, (widgetId) => {
1825
- const widget = this.getWidgetInstance(widgetId);
1826
- return widget != null && widget.examples
1827
- ? widget.examples()
1828
- : null;
1829
- }),
1830
- );
1831
-
1832
- // no widgets with examples
1833
- if (!examples.length) {
1834
- return null;
1835
- }
1836
-
1837
- const allEqual = _.all(examples, function (example) {
1838
- return _.isEqual(examples[0], example);
1839
- });
1840
-
1841
- // some widgets have different examples
1842
- // TODO(alex): handle this better
1843
- if (!allEqual) {
1844
- return null;
1845
- }
1846
-
1847
- return examples[0];
1848
- };
1849
-
1850
- // TranslationLinter callback
1851
- handletranslationLintErrors: (lintErrors: ReadonlyArray<string>) => void = (
1852
- lintErrors: ReadonlyArray<string>,
1853
- ) => {
1854
- if (!this._isMounted) {
1855
- return;
1856
- }
1857
-
1858
- this.setState({
1859
- translationLintErrors: lintErrors,
1860
- });
1861
- };
1862
-
1863
- render(): React.ReactNode {
1864
- const apiOptions = this.getApiOptions();
1865
-
1866
- if (this.reuseMarkdown) {
1867
- return this.lastRenderedMarkdown;
1868
- }
1869
-
1870
- const content = this.getContent(this.props, this.state);
1871
- // `this.widgetIds` is appended to in `this.outputMarkdown`:
1872
- this.widgetIds = [];
1873
-
1874
- if (this.shouldRenderJiptPlaceholder(this.props, this.state)) {
1875
- // Crowdin's JIPT (Just in place translation) uses a fake language
1876
- // with language tag "en-pt" where the value of the translations
1877
- // look like: {crwdns2657085:0}{crwdne2657085:0} where it keeps the
1878
- // {crowdinId:ngettext variant}. We detect whether the current
1879
- // content matches this, so we can take over rendering of
1880
- // the perseus content as the translators interact with jipt.
1881
- // We search for only part of the tag that crowdin uses to guard
1882
- // against them changing the format on us. The full tag it looks
1883
- // for can be found in https://cdn.crowdin.net/jipt/jipt.js
1884
- // globalPhrase var.
1885
-
1886
- // If we haven't already added this component to the registry do so
1887
- // now. showHints() may cause this component to be rerendered
1888
- // before jipt has a chance to replace its contents, so this check
1889
- // will keep us from adding the component to the registry a second
1890
- // time.
1891
- if (!this.translationIndex) {
1892
- this.translationIndex =
1893
- getDependencies().rendererTranslationComponents.addComponent(
1894
- this,
1895
- );
1896
- }
1897
-
1898
- // For articles, we add jipt data to individual paragraphs. For
1899
- // exercises, we add it to the renderer and let translators
1900
- // translate the entire thing. For the article equivalent of
1901
- // this if block, search this file for where we render a
1902
- // QuestionParagraph, and see the `isJipt:` parameter sent to
1903
- // PerseusMarkdown.parse()
1904
- if (!apiOptions.isArticle) {
1905
- // We now need to output this tag, as jipt looks for it to be
1906
- // able to replace it with a translation that it runs an ajax
1907
- // call to get. We add a data attribute with the index to the
1908
- // Persues.TranslationComponent registry so that when jipt
1909
- // calls its before_dom_insert we can lookup this component by
1910
- // this attribute and render the text with markdown.
1911
- return (
1912
- <DefinitionProvider>
1913
- <div
1914
- data-perseus-component-index={this.translationIndex}
1915
- >
1916
- {content}
1917
- </div>
1918
- </DefinitionProvider>
1919
- );
1920
- }
1921
- }
1922
-
1923
- // Hacks:
1924
- // We use mutable state here to figure out whether the output
1925
- // had two columns.
1926
- // It is updated to true by `this.outputMarkdown` if a
1927
- // column break is found
1928
- // TODO(aria): We now have a state variable threaded through
1929
- // simple-markdown output. We should mutate it instead of
1930
- // state on this component to do this in a less hacky way.
1931
- this._isTwoColumn = false;
1932
-
1933
- // Parse the string of markdown to a parse tree
1934
- const parsedMarkdown = this.props.inline
1935
- ? PerseusMarkdown.parseInline(content, {
1936
- // Recognize crowdin IDs while translating articles
1937
- // (This should never be hit by exercises, though if you
1938
- // decide you want to add a check that this is an article,
1939
- // go for it.)
1940
- isJipt: this.translationIndex != null,
1941
- })
1942
- : PerseusMarkdown.parse(content, {
1943
- isJipt: this.translationIndex != null,
1944
- });
1945
-
1946
- // Optionally apply the linter to the parse tree
1947
- if (this.props.linterContext.highlightLint) {
1948
- // If highlightLint is true and lint is detected, this call
1949
- // will modify the parse tree by adding lint nodes that will
1950
- // serve to highlight the lint when rendered
1951
- const context: Context = {
1952
- content: this.props.content,
1953
- widgets: this.props.widgets,
1954
- ...this.props.linterContext,
1955
- };
1956
-
1957
- PerseusLinter.runLinter(parsedMarkdown, context, true);
1958
-
1959
- // Apply the lint errors from the last TranslationLinter run.
1960
- // TODO(joshuan): Support overlapping dots.
1961
- this._translationLinter.applyLintErrors(parsedMarkdown, [
1962
- ...this.state.translationLintErrors,
1963
- ...(this.props.legacyPerseusLint || []),
1964
- ]);
1965
- }
1966
-
1967
- // Render the linted markdown parse tree with React components
1968
- const markdownContents = this.outputMarkdown(parsedMarkdown, {
1969
- baseElements: apiOptions.baseElements,
1970
- });
1971
-
1972
- const className = classNames({
1973
- [ApiClassNames.RENDERER]: true,
1974
- [ApiClassNames.RESPONSIVE_RENDERER]: true,
1975
- [ApiClassNames.TWO_COLUMN_RENDERER]: this._isTwoColumn,
1976
- });
1977
-
1978
- this.lastRenderedMarkdown = (
1979
- <DefinitionProvider>
1980
- <div className={className}>{markdownContents}</div>
1981
- </DefinitionProvider>
1982
- );
1983
- return this.lastRenderedMarkdown;
1984
- }
1985
- }
1986
-
1987
- const styles = {
1988
- mobileZoomableParentFix: {
1989
- // NOTE(abdul): There is an issue where transform animations will
1990
- // cause the Zoomable component to disappear when running on the
1991
- // native app on iPad (iOS 13). I found some answers online that recommend
1992
- // transforming the parent in 3D space. Doing this forces hardware
1993
- // acceleration, which causes the process to run on the GPU. It's not
1994
- // clear to me why this fixes the issue, but it's suggested online
1995
- // to people dealing with similar disappearance and flickering issues.
1996
- transform: "translate3d(0,0,0)",
1997
- },
1998
- } as const;
1999
-
2000
- export default Renderer;