@khanacademy/perseus 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (503) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +7 -0
  3. package/dist/es/index.js +15 -1
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +11934 -0
  6. package/dist/index.js +15 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/strings.js +148 -1
  9. package/package.json +40 -4
  10. package/src/README.md +7 -0
  11. package/src/__stories__/item-renderer.stories.jsx +27 -0
  12. package/src/__stories__/server-item-renderer.stories.jsx +27 -0
  13. package/src/__testdata__/item-renderer_testdata.js +161 -0
  14. package/src/__testdata__/renderer_testdata.js +123 -0
  15. package/src/__tests__/__snapshots__/item-renderer_test.jsx.snap +233 -0
  16. package/src/__tests__/__snapshots__/perseus-markdown_test.jsx.snap +516 -0
  17. package/src/__tests__/__snapshots__/pure-markdown_test.js.snap +113 -0
  18. package/src/__tests__/__snapshots__/renderer_test.jsx.snap +576 -0
  19. package/src/__tests__/__snapshots__/server-item-renderer_test.jsx.snap +306 -0
  20. package/src/__tests__/a11y_test.jsx +304 -0
  21. package/src/__tests__/item-renderer_test.jsx +892 -0
  22. package/src/__tests__/mock-asset-loading-widget.jsx +57 -0
  23. package/src/__tests__/mock-widget.jsx +35 -0
  24. package/src/__tests__/perseus-markdown_test.jsx +747 -0
  25. package/src/__tests__/pure-markdown_test.js +552 -0
  26. package/src/__tests__/renderability_test.jsx +481 -0
  27. package/src/__tests__/renderer-api_test.jsx +333 -0
  28. package/src/__tests__/renderer_test.jsx +1792 -0
  29. package/src/__tests__/server-item-renderer_test.jsx +516 -0
  30. package/src/__tests__/test-items/image-item.js +25 -0
  31. package/src/__tests__/test-items/input-number-1-item.js +27 -0
  32. package/src/__tests__/test-items/input-number-2-item.js +39 -0
  33. package/src/__tests__/test-items/table-item.js +30 -0
  34. package/src/__tests__/tex-wrangler_test.js +83 -0
  35. package/src/__tests__/util_test.js +41 -0
  36. package/src/a11y.js +46 -0
  37. package/src/all-widgets.js +9 -0
  38. package/src/article-renderer.jsx +215 -0
  39. package/src/asset-context.js +30 -0
  40. package/src/basic-widgets.js +13 -0
  41. package/src/components/__stories__/button-group.stories.jsx +61 -0
  42. package/src/components/__stories__/fixed-to-responsive.stories.jsx +132 -0
  43. package/src/components/__stories__/graph.stories.jsx +27 -0
  44. package/src/components/__stories__/graphie.stories.jsx +28 -0
  45. package/src/components/__stories__/hud.stories.jsx +36 -0
  46. package/src/components/__stories__/icon.stories.jsx +56 -0
  47. package/src/components/__stories__/image-loader.stories.jsx +60 -0
  48. package/src/components/__stories__/info-tip.stories.jsx +33 -0
  49. package/src/components/__stories__/inline-icon.stories.jsx +39 -0
  50. package/src/components/__stories__/input-with-examples.stories.jsx +51 -0
  51. package/src/components/__stories__/lint.stories.jsx +40 -0
  52. package/src/components/__stories__/math-input.stories.jsx +29 -0
  53. package/src/components/__stories__/math-output.stories.jsx +24 -0
  54. package/src/components/__stories__/multi-button-group.stories.jsx +69 -0
  55. package/src/components/__stories__/number-input.stories.jsx +42 -0
  56. package/src/components/__stories__/possible-answers.stories.jsx +22 -0
  57. package/src/components/__stories__/prop-check-box.stories.jsx +62 -0
  58. package/src/components/__stories__/range-input.stories.jsx +28 -0
  59. package/src/components/__stories__/simple-keypad-input.stories.jsx +28 -0
  60. package/src/components/__stories__/sortable.stories.jsx +73 -0
  61. package/src/components/__stories__/stub-tag-editor.stories.jsx +44 -0
  62. package/src/components/__stories__/svg-image.stories.jsx +60 -0
  63. package/src/components/__stories__/tex.stories.jsx +18 -0
  64. package/src/components/__stories__/text-buttons.stories.jsx +51 -0
  65. package/src/components/__stories__/text-input.stories.jsx +34 -0
  66. package/src/components/__stories__/text-list-editor.stories.jsx +33 -0
  67. package/src/components/__stories__/zoomable-tex.stories.jsx +39 -0
  68. package/src/components/__stories__/zoomable.stories.jsx +40 -0
  69. package/src/components/__testdata__/sorter_testdata.js +20 -0
  70. package/src/components/__tests__/__snapshots__/sortable_test.jsx.snap +79 -0
  71. package/src/components/__tests__/__snapshots__/sorter_test.jsx.snap +297 -0
  72. package/src/components/__tests__/number-input_test.jsx +112 -0
  73. package/src/components/__tests__/sortable_test.jsx +234 -0
  74. package/src/components/__tests__/sorter_test.jsx +111 -0
  75. package/src/components/__tests__/zoomable_test.jsx +388 -0
  76. package/src/components/button-group.jsx +131 -0
  77. package/src/components/constants.js +15 -0
  78. package/src/components/fixed-to-responsive.jsx +163 -0
  79. package/src/components/graph.jsx +414 -0
  80. package/src/components/graphie-classes.jsx +148 -0
  81. package/src/components/graphie-movables.jsx +342 -0
  82. package/src/components/graphie.jsx +353 -0
  83. package/src/components/highlighting/highlightable-content.jsx +245 -0
  84. package/src/components/highlighting/highlights.js +208 -0
  85. package/src/components/highlighting/ranges.js +299 -0
  86. package/src/components/highlighting/types.js +73 -0
  87. package/src/components/highlighting/ui/highlight-renderer.jsx +179 -0
  88. package/src/components/highlighting/ui/highlight-set-renderer.jsx +190 -0
  89. package/src/components/highlighting/ui/highlight-tooltip.jsx +153 -0
  90. package/src/components/highlighting/ui/highlighting-ui.jsx +163 -0
  91. package/src/components/highlighting/ui/selection-tracker.jsx +195 -0
  92. package/src/components/highlighting/ui/types.js +21 -0
  93. package/src/components/highlighting/ui/util.js +166 -0
  94. package/src/components/highlighting/word-indexer.jsx +155 -0
  95. package/src/components/hud.jsx +179 -0
  96. package/src/components/icon.jsx +149 -0
  97. package/src/components/image-loader.jsx +175 -0
  98. package/src/components/info-tip/info-tip.jsx +149 -0
  99. package/src/components/info-tip.jsx +38 -0
  100. package/src/components/inline-icon.jsx +81 -0
  101. package/src/components/input-with-examples.jsx +231 -0
  102. package/src/components/lint.jsx +456 -0
  103. package/src/components/math-input.jsx +282 -0
  104. package/src/components/math-output.jsx +128 -0
  105. package/src/components/multi-button-group.jsx +137 -0
  106. package/src/components/new-tooltip/README.md +110 -0
  107. package/src/components/new-tooltip/__stories__/flexible-position.stories.jsx +437 -0
  108. package/src/components/new-tooltip/__stories__/new-tooltip.stories.jsx +665 -0
  109. package/src/components/new-tooltip/__stories__/tooltip-arrow.stories.jsx +70 -0
  110. package/src/components/new-tooltip/__stories__/tooltip-bubble.stories.jsx +98 -0
  111. package/src/components/new-tooltip/__tests__/style-constants_test.js +8 -0
  112. package/src/components/new-tooltip/__tests__/util_test.js +47 -0
  113. package/src/components/new-tooltip/flexible-position.jsx +182 -0
  114. package/src/components/new-tooltip/new-tooltip.jsx +399 -0
  115. package/src/components/new-tooltip/style-constants.js +13 -0
  116. package/src/components/new-tooltip/tooltip-arrow.jsx +184 -0
  117. package/src/components/new-tooltip/tooltip-bubble.jsx +223 -0
  118. package/src/components/new-tooltip/tooltip-portal.jsx +101 -0
  119. package/src/components/new-tooltip/tooltip-positioner.jsx +529 -0
  120. package/src/components/new-tooltip/types.js +183 -0
  121. package/src/components/new-tooltip/util.js +27 -0
  122. package/src/components/number-input.jsx +253 -0
  123. package/src/components/possible-answers.jsx +36 -0
  124. package/src/components/prop-check-box.jsx +78 -0
  125. package/src/components/range-input.jsx +61 -0
  126. package/src/components/simple-keypad-input.jsx +91 -0
  127. package/src/components/sortable.jsx +872 -0
  128. package/src/components/stub-tag-editor.jsx +53 -0
  129. package/src/components/svg-image.jsx +762 -0
  130. package/src/components/tex-buttons.jsx +322 -0
  131. package/src/components/tex.jsx +70 -0
  132. package/src/components/text-input.jsx +82 -0
  133. package/src/components/text-list-editor.jsx +157 -0
  134. package/src/components/tooltip.jsx +395 -0
  135. package/src/components/visibility-observer/__stories__/visibility-observer.stories.jsx +140 -0
  136. package/src/components/visibility-observer/get-scroll-parent.js +45 -0
  137. package/src/components/visibility-observer/visibility-observer.js +109 -0
  138. package/src/components/visibility-observer/visibility-observer.jsx +123 -0
  139. package/src/components/zoomable-tex.jsx +70 -0
  140. package/src/components/zoomable.jsx +304 -0
  141. package/src/components.js +17 -0
  142. package/src/dependencies.js +22 -0
  143. package/src/error-boundary.jsx +48 -0
  144. package/src/extra-widgets.js +75 -0
  145. package/src/gorgon/README +40 -0
  146. package/src/gorgon/__tests__/matcher_test.js +497 -0
  147. package/src/gorgon/__tests__/rule_test.js +101 -0
  148. package/src/gorgon/__tests__/rules_test.js +487 -0
  149. package/src/gorgon/__tests__/selector-parser_test.js +52 -0
  150. package/src/gorgon/__tests__/tree-transformer_test.js +446 -0
  151. package/src/gorgon/gorgon.js +290 -0
  152. package/src/gorgon/proptypes.js +29 -0
  153. package/src/gorgon/rule.js +413 -0
  154. package/src/gorgon/rules/absolute-url.js +24 -0
  155. package/src/gorgon/rules/all-rules.js +72 -0
  156. package/src/gorgon/rules/blockquoted-math.js +10 -0
  157. package/src/gorgon/rules/blockquoted-widget.js +10 -0
  158. package/src/gorgon/rules/double-spacing-after-terminal.js +12 -0
  159. package/src/gorgon/rules/extra-content-spacing.js +12 -0
  160. package/src/gorgon/rules/heading-level-1.js +14 -0
  161. package/src/gorgon/rules/heading-level-skip.js +20 -0
  162. package/src/gorgon/rules/heading-sentence-case.js +11 -0
  163. package/src/gorgon/rules/heading-title-case.js +63 -0
  164. package/src/gorgon/rules/image-alt-text.js +21 -0
  165. package/src/gorgon/rules/image-in-table.js +10 -0
  166. package/src/gorgon/rules/image-spaces-around-urls.js +35 -0
  167. package/src/gorgon/rules/image-widget.js +50 -0
  168. package/src/gorgon/rules/link-click-here.js +11 -0
  169. package/src/gorgon/rules/lint-utils.js +48 -0
  170. package/src/gorgon/rules/long-paragraph.js +14 -0
  171. package/src/gorgon/rules/math-adjacent.js +10 -0
  172. package/src/gorgon/rules/math-align-extra-break.js +11 -0
  173. package/src/gorgon/rules/math-align-linebreaks.js +43 -0
  174. package/src/gorgon/rules/math-empty.js +10 -0
  175. package/src/gorgon/rules/math-font-size.js +12 -0
  176. package/src/gorgon/rules/math-frac.js +10 -0
  177. package/src/gorgon/rules/math-nested.js +11 -0
  178. package/src/gorgon/rules/math-starts-with-space.js +12 -0
  179. package/src/gorgon/rules/math-text-empty.js +10 -0
  180. package/src/gorgon/rules/math-without-dollars.js +14 -0
  181. package/src/gorgon/rules/nested-lists.js +11 -0
  182. package/src/gorgon/rules/profanity.js +10 -0
  183. package/src/gorgon/rules/table-missing-cells.js +20 -0
  184. package/src/gorgon/rules/unbalanced-code-delimiters.js +14 -0
  185. package/src/gorgon/rules/unescaped-dollar.js +10 -0
  186. package/src/gorgon/rules/widget-in-table.js +10 -0
  187. package/src/gorgon/selector.js +506 -0
  188. package/src/gorgon/tree-transformer.js +588 -0
  189. package/src/hint-renderer.jsx +159 -0
  190. package/src/hints-renderer.jsx +359 -0
  191. package/src/icon-paths.js +120 -0
  192. package/src/index.js +149 -2
  193. package/src/init.js +49 -0
  194. package/src/interaction-tracker.js +60 -0
  195. package/src/interactive2/__tests__/movable-mock.js +48 -0
  196. package/src/interactive2/__tests__/movable-point_test.js +146 -0
  197. package/src/interactive2/interactive-util.js +152 -0
  198. package/src/interactive2/movable-helper-methods.js +160 -0
  199. package/src/interactive2/movable-line-options.js +392 -0
  200. package/src/interactive2/movable-line.js +322 -0
  201. package/src/interactive2/movable-point-options.js +190 -0
  202. package/src/interactive2/movable-point.jsx +604 -0
  203. package/src/interactive2/movable-polygon-options.js +351 -0
  204. package/src/interactive2/movable-polygon.js +367 -0
  205. package/src/interactive2/movable.js +273 -0
  206. package/src/interactive2/objective_.js +79 -0
  207. package/src/interactive2/types.js +3 -0
  208. package/src/interactive2/wrapped-defaults.js +86 -0
  209. package/src/interactive2/wrapped-ellipse.js +87 -0
  210. package/src/interactive2/wrapped-line.js +109 -0
  211. package/src/interactive2/wrapped-path.js +47 -0
  212. package/src/interactive2.js +91 -0
  213. package/src/item-renderer.jsx +518 -0
  214. package/src/item-version.js +12 -0
  215. package/src/jipt-paragraphs.js +44 -0
  216. package/src/jquery.mobile.vmouse.js +545 -0
  217. package/src/keypad-context.js +33 -0
  218. package/src/loading-context.js +20 -0
  219. package/src/logging/log.js +107 -0
  220. package/src/logging/log_test.js +41 -0
  221. package/src/mixins/changeable.jsx +115 -0
  222. package/src/mixins/editor-jsonify.jsx +13 -0
  223. package/src/mixins/provide-keypad.jsx +131 -0
  224. package/src/mixins/widget-jsonify-deprecated.jsx +23 -0
  225. package/src/mixins/widget-prop-blacklist.js +36 -0
  226. package/src/multi-items/__stories__/multi-renderer.stories.js +88 -0
  227. package/src/multi-items/__testdata__/multi-renderer_testdata.js +174 -0
  228. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +994 -0
  229. package/src/multi-items/__tests__/items_test.js +245 -0
  230. package/src/multi-items/__tests__/multi-renderer_test.js +903 -0
  231. package/src/multi-items/__tests__/prop-type-builders_test.js +134 -0
  232. package/src/multi-items/__tests__/shapes_test.js +44 -0
  233. package/src/multi-items/__tests__/trees_test.js +316 -0
  234. package/src/multi-items/item-types.js +55 -0
  235. package/src/multi-items/items.js +176 -0
  236. package/src/multi-items/multi-renderer.jsx +542 -0
  237. package/src/multi-items/prop-type-builders.js +105 -0
  238. package/src/multi-items/shape-types.js +47 -0
  239. package/src/multi-items/shapes.js +53 -0
  240. package/src/multi-items/tree-types.js +42 -0
  241. package/src/multi-items/trees.js +345 -0
  242. package/src/multi-items.js +76 -0
  243. package/src/not-gorgon.js +129 -0
  244. package/src/perseus-api.jsx +270 -0
  245. package/src/perseus-env.js +26 -0
  246. package/src/perseus-error.js +19 -0
  247. package/src/perseus-markdown.jsx +434 -0
  248. package/src/perseus-types.js +1597 -0
  249. package/src/pure-markdown.js +346 -0
  250. package/src/question-paragraph.jsx +30 -0
  251. package/src/renderability.jsx +108 -0
  252. package/src/renderer.jsx +1931 -0
  253. package/src/server-item-renderer.jsx +467 -0
  254. package/src/sigfigs.jsx +492 -0
  255. package/src/styles/articles.less +748 -0
  256. package/src/styles/constants.js +119 -0
  257. package/src/styles/global-constants.js +350 -0
  258. package/src/styles/global-styles.js +307 -0
  259. package/src/styles/khan-exercise.css +1304 -0
  260. package/src/styles/media-queries.js +48 -0
  261. package/src/styles/perseus-renderer.less +1008 -0
  262. package/src/styles/shared.js +132 -0
  263. package/src/styles/util.less +47 -0
  264. package/src/styles/variables.boostrap.less +301 -0
  265. package/src/styles/variables.less +68 -0
  266. package/src/styles/variables.shared.less +356 -0
  267. package/src/styles/widgets/categorizer.less +39 -0
  268. package/src/styles/widgets/definition.less +86 -0
  269. package/src/styles/widgets/dropdown.less +3 -0
  270. package/src/styles/widgets/expression.less +147 -0
  271. package/src/styles/widgets/graded-group.less +36 -0
  272. package/src/styles/widgets/grapher.less +11 -0
  273. package/src/styles/widgets/group.less +19 -0
  274. package/src/styles/widgets/image.less +29 -0
  275. package/src/styles/widgets/interactive-graph.less +61 -0
  276. package/src/styles/widgets/label-image.less +41 -0
  277. package/src/styles/widgets/matcher.less +8 -0
  278. package/src/styles/widgets/matrix.less +120 -0
  279. package/src/styles/widgets/measurer.less +20 -0
  280. package/src/styles/widgets/orderer.less +97 -0
  281. package/src/styles/widgets/passage.less +151 -0
  282. package/src/styles/widgets/plotter.less +33 -0
  283. package/src/styles/widgets/radio.less +52 -0
  284. package/src/styles/widgets/reaction-diagram.less +35 -0
  285. package/src/styles/widgets/sequence.less +5 -0
  286. package/src/styles/widgets/sortable.less +127 -0
  287. package/src/styles/widgets/table.less +56 -0
  288. package/src/styles/widgets/transformer.less +28 -0
  289. package/src/styles/widgets/unit.less +14 -0
  290. package/src/tex-wrangler.js +176 -0
  291. package/src/traversal.jsx +141 -0
  292. package/src/types.js +538 -0
  293. package/src/util/a11y.js +24 -0
  294. package/src/util/answer-types.js +862 -0
  295. package/src/util/answer-types_test.js +115 -0
  296. package/src/util/colors.js +94 -0
  297. package/src/util/fix-passage-refs.jsx +122 -0
  298. package/src/util/graph-utils.js +16 -0
  299. package/src/util/graphie.js +1585 -0
  300. package/src/util/interactive.js +4071 -0
  301. package/src/util/katex-preprocess.js +16 -0
  302. package/src/util/math.js +296 -0
  303. package/src/util/mobile-native-utils.js +14 -0
  304. package/src/util/mobile-native-utils_test.js +24 -0
  305. package/src/util/register-all-widgets-for-testing.js +12 -0
  306. package/src/util/scroll-utils.js +162 -0
  307. package/src/util/sizing-utils.js +46 -0
  308. package/src/util/tex.js +203 -0
  309. package/src/util/url-utils.js +36 -0
  310. package/src/util.js +990 -0
  311. package/src/version.js +11 -0
  312. package/src/widget-container.jsx +174 -0
  313. package/src/widgets/__stories__/categorizer.stories.jsx +15 -0
  314. package/src/widgets/__stories__/cs-program.stories.jsx +15 -0
  315. package/src/widgets/__stories__/definition.stories.jsx +38 -0
  316. package/src/widgets/__stories__/dropdown.stories.jsx +15 -0
  317. package/src/widgets/__stories__/explanation.stories.jsx +19 -0
  318. package/src/widgets/__stories__/expression.stories.jsx +26 -0
  319. package/src/widgets/__stories__/graded-group-set.stories.jsx +32 -0
  320. package/src/widgets/__stories__/graded-group.stories.jsx +32 -0
  321. package/src/widgets/__stories__/grapher.stories.jsx +47 -0
  322. package/src/widgets/__stories__/group.stories.jsx +15 -0
  323. package/src/widgets/__stories__/iframe.stories.jsx +15 -0
  324. package/src/widgets/__stories__/image.stories.jsx +62 -0
  325. package/src/widgets/__stories__/input-number.stories.jsx +90 -0
  326. package/src/widgets/__stories__/interaction.stories.jsx +15 -0
  327. package/src/widgets/__stories__/interactive-graph.stories.jsx +61 -0
  328. package/src/widgets/__stories__/matcher.stories.jsx +15 -0
  329. package/src/widgets/__stories__/matrix.stories.jsx +15 -0
  330. package/src/widgets/__stories__/number-line.stories.jsx +15 -0
  331. package/src/widgets/__stories__/numeric-input.stories.jsx +19 -0
  332. package/src/widgets/__stories__/orderer.stories.jsx +15 -0
  333. package/src/widgets/__stories__/passage-ref.stories.jsx +19 -0
  334. package/src/widgets/__stories__/passage.stories.jsx +19 -0
  335. package/src/widgets/__stories__/radio.stories.jsx +82 -0
  336. package/src/widgets/__stories__/sorter.stories.jsx +15 -0
  337. package/src/widgets/__stories__/transformer.stories.jsx +15 -0
  338. package/src/widgets/__stories__/video.stories.jsx +15 -0
  339. package/src/widgets/__testdata__/categorizer_testdata.js +50 -0
  340. package/src/widgets/__testdata__/cs-program_testdata.js +28 -0
  341. package/src/widgets/__testdata__/dropdown_testdata.js +34 -0
  342. package/src/widgets/__testdata__/explanation_testdata.js +56 -0
  343. package/src/widgets/__testdata__/expression_testdata.js +95 -0
  344. package/src/widgets/__testdata__/graded-group-set_testdata.js +358 -0
  345. package/src/widgets/__testdata__/graded-group_testdata.js +79 -0
  346. package/src/widgets/__testdata__/grapher_testdata.js +376 -0
  347. package/src/widgets/__testdata__/group_testdata.js +156 -0
  348. package/src/widgets/__testdata__/iframe_testdata.js +36 -0
  349. package/src/widgets/__testdata__/image_testdata.js +38 -0
  350. package/src/widgets/__testdata__/input-number_testdata.js +73 -0
  351. package/src/widgets/__testdata__/interaction_testdata.js +264 -0
  352. package/src/widgets/__testdata__/interactive-graph_testdata.js +646 -0
  353. package/src/widgets/__testdata__/matcher_testdata.js +34 -0
  354. package/src/widgets/__testdata__/matrix_testdata.js +29 -0
  355. package/src/widgets/__testdata__/number-line_testdata.js +31 -0
  356. package/src/widgets/__testdata__/numeric-input_testdata.js +266 -0
  357. package/src/widgets/__testdata__/orderer_testdata.js +58 -0
  358. package/src/widgets/__testdata__/passage-ref_testdata.js +41 -0
  359. package/src/widgets/__testdata__/passage_testdata.js +71 -0
  360. package/src/widgets/__testdata__/radio_testdata.js +210 -0
  361. package/src/widgets/__testdata__/sorter_testdata.js +20 -0
  362. package/src/widgets/__testdata__/transformer_testdata.js +109 -0
  363. package/src/widgets/__testdata__/video_testdata.js +22 -0
  364. package/src/widgets/__tests__/__snapshots__/categorizer_test.jsx.snap +928 -0
  365. package/src/widgets/__tests__/__snapshots__/cs-program_test.jsx.snap +75 -0
  366. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +99 -0
  367. package/src/widgets/__tests__/__snapshots__/dropdown_test.jsx.snap +143 -0
  368. package/src/widgets/__tests__/__snapshots__/explanation_test.js.snap +473 -0
  369. package/src/widgets/__tests__/__snapshots__/expression_test.jsx.snap +38 -0
  370. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +754 -0
  371. package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +310 -0
  372. package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +863 -0
  373. package/src/widgets/__tests__/__snapshots__/grapher_test.jsx.snap +2880 -0
  374. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +1154 -0
  375. package/src/widgets/__tests__/__snapshots__/iframe_test.jsx.snap +69 -0
  376. package/src/widgets/__tests__/__snapshots__/image_test.jsx.snap +388 -0
  377. package/src/widgets/__tests__/__snapshots__/input-number_test.jsx.snap +978 -0
  378. package/src/widgets/__tests__/__snapshots__/interaction_test.jsx.snap +311 -0
  379. package/src/widgets/__tests__/__snapshots__/interactive-graph_test.jsx.snap +2915 -0
  380. package/src/widgets/__tests__/__snapshots__/matcher_test.jsx.snap +1039 -0
  381. package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +450 -0
  382. package/src/widgets/__tests__/__snapshots__/number-line_test.jsx.snap +888 -0
  383. package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +930 -0
  384. package/src/widgets/__tests__/__snapshots__/orderer_test.jsx.snap +275 -0
  385. package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +446 -0
  386. package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +1988 -0
  387. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +7107 -0
  388. package/src/widgets/__tests__/__snapshots__/video_test.jsx.snap +163 -0
  389. package/src/widgets/__tests__/categorizer_test.jsx +205 -0
  390. package/src/widgets/__tests__/cs-program_test.jsx +45 -0
  391. package/src/widgets/__tests__/definition_test.js +152 -0
  392. package/src/widgets/__tests__/dropdown_test.jsx +106 -0
  393. package/src/widgets/__tests__/explanation_test.js +199 -0
  394. package/src/widgets/__tests__/expression_test.jsx +439 -0
  395. package/src/widgets/__tests__/graded-group-set-jipt_test.jsx +33 -0
  396. package/src/widgets/__tests__/graded-group-set_test.jsx +270 -0
  397. package/src/widgets/__tests__/graded-group_test.jsx +233 -0
  398. package/src/widgets/__tests__/grapher.cypress.jsx +486 -0
  399. package/src/widgets/__tests__/grapher_test.jsx +35 -0
  400. package/src/widgets/__tests__/group_test.jsx +512 -0
  401. package/src/widgets/__tests__/iframe_test.jsx +47 -0
  402. package/src/widgets/__tests__/image_test.jsx +65 -0
  403. package/src/widgets/__tests__/input-number_test.jsx +360 -0
  404. package/src/widgets/__tests__/interaction_test.jsx +33 -0
  405. package/src/widgets/__tests__/interactive-graph.cypress.jsx +697 -0
  406. package/src/widgets/__tests__/interactive-graph_test.jsx +105 -0
  407. package/src/widgets/__tests__/matcher_test.jsx +157 -0
  408. package/src/widgets/__tests__/matrix_test.jsx +84 -0
  409. package/src/widgets/__tests__/number-line_test.jsx +81 -0
  410. package/src/widgets/__tests__/numeric-input_test.jsx +574 -0
  411. package/src/widgets/__tests__/orderer_test.jsx +75 -0
  412. package/src/widgets/__tests__/passage-ref_test.jsx +166 -0
  413. package/src/widgets/__tests__/passage_test.jsx +133 -0
  414. package/src/widgets/__tests__/radio/base-radio_test.jsx +82 -0
  415. package/src/widgets/__tests__/radio_test.jsx +651 -0
  416. package/src/widgets/__tests__/renderQuestion.jsx +65 -0
  417. package/src/widgets/__tests__/unit_test.jsx +144 -0
  418. package/src/widgets/__tests__/video_test.jsx +55 -0
  419. package/src/widgets/categorizer.jsx +338 -0
  420. package/src/widgets/cs-program.jsx +228 -0
  421. package/src/widgets/definition.jsx +141 -0
  422. package/src/widgets/dropdown.jsx +143 -0
  423. package/src/widgets/explanation.jsx +315 -0
  424. package/src/widgets/expression.jsx +654 -0
  425. package/src/widgets/graded-group-answer-bar.jsx +152 -0
  426. package/src/widgets/graded-group-set.jsx +285 -0
  427. package/src/widgets/graded-group.jsx +448 -0
  428. package/src/widgets/grapher/util.jsx +819 -0
  429. package/src/widgets/grapher.jsx +658 -0
  430. package/src/widgets/group.jsx +213 -0
  431. package/src/widgets/iframe.jsx +199 -0
  432. package/src/widgets/image.jsx +309 -0
  433. package/src/widgets/input-number.jsx +410 -0
  434. package/src/widgets/interaction/arrow-picker.jsx +39 -0
  435. package/src/widgets/interaction/color-picker.jsx +69 -0
  436. package/src/widgets/interaction/dash-picker.jsx +46 -0
  437. package/src/widgets/interaction/element-container.jsx +97 -0
  438. package/src/widgets/interaction.jsx +742 -0
  439. package/src/widgets/interactive-graph.jsx +2821 -0
  440. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +93 -0
  441. package/src/widgets/label-image/__stories__/marker.stories.jsx +172 -0
  442. package/src/widgets/label-image/answer-choices.jsx +311 -0
  443. package/src/widgets/label-image/marker.jsx +365 -0
  444. package/src/widgets/label-image/types.js +33 -0
  445. package/src/widgets/label-image.jsx +841 -0
  446. package/src/widgets/label-image_test.js +840 -0
  447. package/src/widgets/lights-puzzle.jsx +356 -0
  448. package/src/widgets/matcher.jsx +271 -0
  449. package/src/widgets/matrix.jsx +615 -0
  450. package/src/widgets/measurer.jsx +215 -0
  451. package/src/widgets/molecule/__tests__/layout_test.jsx +159 -0
  452. package/src/widgets/molecule/__tests__/parser_test.jsx +142 -0
  453. package/src/widgets/molecule/molecule-drawing.jsx +175 -0
  454. package/src/widgets/molecule/molecule-layout.jsx +367 -0
  455. package/src/widgets/molecule/smiles-parser.jsx +300 -0
  456. package/src/widgets/molecule.jsx +172 -0
  457. package/src/widgets/number-line.jsx +858 -0
  458. package/src/widgets/numeric-input.jsx +622 -0
  459. package/src/widgets/orderer.jsx +753 -0
  460. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +382 -0
  461. package/src/widgets/passage/get-line-height-for-node.js +10 -0
  462. package/src/widgets/passage/passage-markdown.jsx +416 -0
  463. package/src/widgets/passage-ref-target.jsx +70 -0
  464. package/src/widgets/passage-ref.jsx +198 -0
  465. package/src/widgets/passage.jsx +601 -0
  466. package/src/widgets/plotter.jsx +1198 -0
  467. package/src/widgets/radio/__stories__/base-radio.stories.jsx +40 -0
  468. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +157 -0
  469. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +26 -0
  470. package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +64 -0
  471. package/src/widgets/radio/__stories__/cross-out-menu.stories.jsx +59 -0
  472. package/src/widgets/radio/__stories__/cross-out-tooltip.stories.jsx +59 -0
  473. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +38 -0
  474. package/src/widgets/radio/__stories__/option-status.stories.jsx +118 -0
  475. package/src/widgets/radio/__stories__/toggleable-radio-button.stories.jsx +28 -0
  476. package/src/widgets/radio/base-radio.jsx +642 -0
  477. package/src/widgets/radio/choice-icon.jsx +468 -0
  478. package/src/widgets/radio/choice-none-above.jsx +54 -0
  479. package/src/widgets/radio/choice.jsx +692 -0
  480. package/src/widgets/radio/choice.jsx.fixture.js +64 -0
  481. package/src/widgets/radio/cross-out-button.jsx +136 -0
  482. package/src/widgets/radio/cross-out-menu-wrapper.jsx +492 -0
  483. package/src/widgets/radio/cross-out-tooltip.jsx +129 -0
  484. package/src/widgets/radio/focus-ring.jsx +51 -0
  485. package/src/widgets/radio/option-status.jsx +89 -0
  486. package/src/widgets/radio/toggleable-radio-button.jsx +133 -0
  487. package/src/widgets/radio/util.js +32 -0
  488. package/src/widgets/radio/util_test.js +19 -0
  489. package/src/widgets/radio/widget.jsx +470 -0
  490. package/src/widgets/radio.jsx +156 -0
  491. package/src/widgets/reaction-diagram.jsx +136 -0
  492. package/src/widgets/sequence.jsx +140 -0
  493. package/src/widgets/simulator.jsx +808 -0
  494. package/src/widgets/sorter.jsx +146 -0
  495. package/src/widgets/table.jsx +395 -0
  496. package/src/widgets/transformer.jsx +2870 -0
  497. package/src/widgets/unit.jsx +371 -0
  498. package/src/widgets/video-transcript-link.jsx +84 -0
  499. package/src/widgets/video-transcript-link_test.js +211 -0
  500. package/src/widgets/video.jsx +131 -0
  501. package/src/widgets.js +431 -0
  502. package/src/zoom.js +539 -0
  503. package/src/__tests__/index.test.js +0 -14
@@ -0,0 +1,542 @@
1
+ /* eslint-disable react/no-unsafe */
2
+ // TODO(mdr): There are some Flow errors in this file, and they're tricky, so
3
+ // I'm skipping Flow errors for now.
4
+ // @flow
5
+ /**
6
+ * Main entry point to the MultiRenderer render portion.
7
+ *
8
+ * This file exposes the `MultiRenderer` component which performs
9
+ * multi-rendering. To multi-render a question, pass in the content of the item
10
+ * to the `MultiRenderer` component as a props. Then, pass in a function which
11
+ * takes an object of renderers (in the same structure as the content), and
12
+ * return a render tree. The `MultiRenderer` component will allow you to
13
+ * combine scores, serialized state, etc. without having to manually call on
14
+ * each of the functions. It also handles inter-widgets requests between the
15
+ * different renderers.
16
+ *
17
+ * Example:
18
+ *
19
+ * item = {_multi: {
20
+ * left: <content data>,
21
+ * right: [<content data>, <content data>],
22
+ * }}
23
+ * shape = shapes.shape({
24
+ * left: shapes.content,
25
+ * right: shapes.arrayOf(shapes.content),
26
+ * })
27
+ *
28
+ * <MultiRenderer item={item} shape={shape}>
29
+ * {({renderers}) =>
30
+ * <div>
31
+ * <div id="left">{renderers.left}</div>
32
+ * <ul id="right">
33
+ * {renderers.right.map(r => <li>{r}</li>)}
34
+ * </ul>
35
+ * </div>
36
+ * }
37
+ * </MultiRenderer>
38
+ */
39
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
40
+ import {StyleSheet, css} from "aphrodite";
41
+ import lens from "hubble";
42
+ import * as React from "react";
43
+
44
+ import HintsRenderer from "../hints-renderer.jsx";
45
+ import {Errors, Log} from "../logging/log.js";
46
+ import Renderer from "../renderer.jsx";
47
+ import Util from "../util.js";
48
+
49
+ import {itemToTree} from "./items.js";
50
+ import {buildMapper} from "./trees.js";
51
+
52
+ import type {Widget} from "../renderer.jsx";
53
+ import type {APIOptions, FilterCriterion} from "../types.js";
54
+ import type {Item, ContentNode, HintNode, TagsNode} from "./item-types.js";
55
+ import type {Shape, ArrayShape} from "./shape-types.js";
56
+ import type {Tree} from "./tree-types.js";
57
+ import type {TreeMapper, ContentMapper, HintMapper, Path} from "./trees.js";
58
+
59
+ type Hint = any; // TODO(mdr)
60
+ type Score = any; // TODO(mdr)
61
+ type SerializedState = any; // TODO(mdr)
62
+
63
+ type RendererProps = React.ElementConfig<typeof Renderer>;
64
+
65
+ type ContentRendererElement = React.Element<any>;
66
+ type HintRendererElement = React.Element<any>;
67
+ type ContentRendererData = {|
68
+ makeRenderer: () => ContentRendererElement,
69
+ ref: ?Renderer,
70
+ |};
71
+ type FindWidgetsFunc = (criterion: FilterCriterion) => $ReadOnlyArray<?Widget>;
72
+ type HintRendererData = {|
73
+ makeRenderer: () => HintRendererElement,
74
+ findExternalWidgets: ?FindWidgetsFunc,
75
+ ref: null,
76
+ hint: Hint,
77
+ |};
78
+ type RendererData = ContentRendererData | HintRendererData;
79
+ type RendererDataTree = Tree<ContentRendererData, HintRendererData, null>;
80
+
81
+ /**
82
+ * TODO(somewhatabstract, JIRA-XXXX):
83
+ * Some usage of this type somewhere is causing Flow to believe that these
84
+ * elements should be components. Don't know where that is, to fix it up
85
+ * properly. In reality, these types are just too hard to use and aren't
86
+ * really helping us out as I now have to suppress this rather than
87
+ * get value from the type. :(
88
+ */
89
+ // $FlowFixMe[not-a-component]
90
+ // $FlowFixMe[incompatible-type-arg]
91
+ type RendererTree = Tree<ContentRendererElement, HintRendererElement, null>;
92
+ type ScoreTree = Tree<Score, null, null>;
93
+ type SerializedStateTree = Tree<SerializedState, null, null>;
94
+
95
+ type Props = {|
96
+ item: Item,
97
+ shape: Shape,
98
+ children: (tree: {|renderers: RendererTree|}) => React.Element<any>,
99
+ serializedState?: ?SerializedStateTree,
100
+ onSerializedStateUpdated?: (state: SerializedStateTree) => void,
101
+ onInteractWithWidget?: (id: string) => void,
102
+ apiOptions?: APIOptions,
103
+ reviewMode?: ?boolean,
104
+ |};
105
+ type State = {|
106
+ // We cache functions to generate renderers and refs in `rendererDataTree`,
107
+ // and change them every time content changes. This isn't just a performance
108
+ // optimization; see `_makeContentRendererData` for more discussion.
109
+ rendererDataTree: ?RendererDataTree,
110
+ // But, if traversing the tree fails, we store the Error in `renderError`.
111
+ renderError: ?Error,
112
+ |};
113
+
114
+ class MultiRenderer extends React.Component<Props, State> {
115
+ rendererDataTreeMapper: TreeMapper<
116
+ ContentNode,
117
+ ContentRendererData,
118
+ HintNode,
119
+ HintRendererData,
120
+ TagsNode,
121
+ null,
122
+ >;
123
+ getRenderersMapper: TreeMapper<
124
+ ContentRendererData,
125
+ ContentRendererElement,
126
+ HintRendererData,
127
+ HintRendererElement,
128
+ null,
129
+ null,
130
+ >;
131
+
132
+ constructor(props: Props) {
133
+ super(props);
134
+
135
+ this.rendererDataTreeMapper = buildMapper()
136
+ .setContentMapper((c, _, p) => this._makeContentRendererData(c, p))
137
+ .setHintMapper((h) => this._makeHintRendererData(h))
138
+ .setTagsMapper((t) => null);
139
+
140
+ this.getRenderersMapper = buildMapper()
141
+ .setContentMapper((c) => c.makeRenderer())
142
+ .setHintMapper((h) => h.makeRenderer())
143
+ .setArrayMapper((renderers, data, shape) =>
144
+ this._annotateRendererArray(
145
+ (renderers: $FlowFixMe),
146
+ (data: $FlowFixMe),
147
+ shape,
148
+ ),
149
+ );
150
+
151
+ // Keep state in sync with props.
152
+ this.state = this._tryMakeRendererState(this.props);
153
+ }
154
+ /* eslint-enable react/sort-comp */
155
+
156
+ UNSAFE_componentWillReceiveProps(nextProps: Props) {
157
+ // Keep state in sync with props.
158
+ if (nextProps.item !== this.props.item) {
159
+ this.setState(this._tryMakeRendererState(nextProps));
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Attempt to build a State that includes a renderer tree corresponding to
165
+ * the item provided in props. On error, return a state with `renderError`
166
+ * set instead.
167
+ */
168
+ _tryMakeRendererState(props: Props): State {
169
+ try {
170
+ return {
171
+ rendererDataTree: this._makeRendererDataTree(
172
+ props.item,
173
+ props.shape,
174
+ ),
175
+ renderError: null,
176
+ };
177
+ } catch (e) {
178
+ Log.error("Error building tree state", Errors.Internal, {
179
+ cause: e,
180
+ });
181
+ return {
182
+ rendererDataTree: null,
183
+ renderError: e,
184
+ };
185
+ }
186
+ }
187
+
188
+ _handleSerializedStateUpdated: (path: Path, newState: any) => void = (
189
+ path,
190
+ newState,
191
+ ) => {
192
+ const {onSerializedStateUpdated} = this.props;
193
+
194
+ if (onSerializedStateUpdated) {
195
+ const oldState = this._getSerializedState(
196
+ this.props.serializedState,
197
+ );
198
+ onSerializedStateUpdated(
199
+ lens(oldState).set(path, newState).freeze(),
200
+ );
201
+ }
202
+ };
203
+
204
+ /**
205
+ * Props that aren't directly used by the MultiRenderer are delegated to
206
+ * the underlying Renderers.
207
+ */
208
+ _getRendererProps(): RendererProps {
209
+ // `item`, `children`, and others are unused. I'm
210
+ // explicitly pulling them out of `this.props` so I don't pass them to
211
+ // `<Renderer>`. I'm not sure how else to do this.
212
+ const {
213
+ item: _,
214
+ children: __,
215
+ shape: ___,
216
+ serializedState: ____,
217
+ onSerializedStateUpdated: _____,
218
+ ...otherProps
219
+ } = this.props;
220
+ return otherProps;
221
+ }
222
+
223
+ /**
224
+ * Construct a Renderer and a ref placeholder for the given ContentNode.
225
+ */
226
+ _makeContentRendererData(
227
+ content: ContentNode,
228
+ path: Path,
229
+ ): ContentRendererData {
230
+ // NOTE(emily): The `findExternalWidgets` function here is computed
231
+ // inline and thus changes each time we run this function. If it
232
+ // were to change every render, it would cause the Renderer to
233
+ // re-render a lot more than is necessary. Don't re-compute this
234
+ // element unless it is necessary!
235
+ // HACK(mdr): Flow can't prove that this is a ContentRendererData,
236
+ // because of how we awkwardly construct it in order to obtain a
237
+ // circular reference. But it is, I promise.
238
+ const data: any = {ref: null, makeRenderer: null};
239
+
240
+ const refFunc = (e) => (data.ref = e);
241
+ const findExternalWidgets = (criterion) =>
242
+ this._findWidgets(data, criterion);
243
+ const handleSerializedState = (state) =>
244
+ this._handleSerializedStateUpdated(path, state);
245
+
246
+ data.makeRenderer = () => (
247
+ /**
248
+ * TODO(somewhatabstract, JIRA-XXXX):
249
+ * `content` contains props that Renderer doesn't have. However,
250
+ * since the type for `content` is not exact, it's hard to know
251
+ * if this spread is including undocumented props so mapping
252
+ * one to one could introduce a bug. Need to work out the exact
253
+ * type for ContentNode and then fix this.
254
+ */
255
+ // $FlowFixMe[cannot-spread-inexact]
256
+ <Renderer
257
+ {...this._getRendererProps()}
258
+ {...content}
259
+ ref={refFunc}
260
+ findExternalWidgets={findExternalWidgets}
261
+ serializedState={
262
+ this.props.serializedState
263
+ ? lens(this.props.serializedState).get(path)
264
+ : null
265
+ }
266
+ onSerializedStateUpdated={handleSerializedState}
267
+ />
268
+ );
269
+ return data;
270
+ }
271
+
272
+ /**
273
+ * Construct a Renderer for the given HintNode, and keep track of the hint
274
+ * itself for future use, too.
275
+ */
276
+ _makeHintRendererData(hint: HintNode): HintRendererData {
277
+ // TODO(mdr): Once HintsRenderer supports inter-widget communication,
278
+ // give it a ref. Until then, leave the ref null forever, to avoid
279
+ // confusing the findWidgets functions.
280
+ //
281
+ // NOTE(davidflanagan): As a partial step toward inter-widget
282
+ // communication we're going to pass a findExternalWidgets function
283
+ // (using a dummy data object). This allows passage-ref widgets in
284
+ // hints to use findWidget() to find the passage widgets they reference.
285
+ // Note that this is one-way only, however. It does not allow
286
+ // widgets in the question to find widgets in the hints, for example.
287
+ const findExternalWidgets = (criterion) =>
288
+ this._findWidgets(({}: any), criterion);
289
+
290
+ return {
291
+ hint,
292
+ findExternalWidgets, // _annotateRendererArray() needs this
293
+ ref: null,
294
+ makeRenderer: () => (
295
+ <HintsRenderer
296
+ {...this._getRendererProps()}
297
+ findExternalWidgets={findExternalWidgets}
298
+ hints={[hint]}
299
+ />
300
+ ),
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Construct a tree of interconnected RendererDatas, corresponding to the
306
+ * given item. Called in `_tryMakeRendererState`, in order to store this
307
+ * tree in the component state.
308
+ */
309
+ _makeRendererDataTree(item: Item, shape: Shape): RendererDataTree {
310
+ const itemTree = itemToTree(item);
311
+ return this.rendererDataTreeMapper.mapTree(itemTree, shape);
312
+ }
313
+
314
+ /**
315
+ * Return all widgets that meet the given criterion, from all Renderers
316
+ * except the Renderer that triggered this call.
317
+ *
318
+ * This function is provided to each Renderer's `findExternalWidgets` prop,
319
+ * which enables widgets in different Renderers to discover each other and
320
+ * communicate.
321
+ */
322
+ _findWidgets(
323
+ callingData: RendererData,
324
+ filterCriterion: FilterCriterion,
325
+ ): $ReadOnlyArray<?Widget> {
326
+ const results = [];
327
+
328
+ this._mapRenderers((data) => {
329
+ if (callingData !== data && data.ref) {
330
+ results.push(...data.ref.findInternalWidgets(filterCriterion));
331
+ }
332
+ });
333
+
334
+ return results;
335
+ }
336
+
337
+ /**
338
+ * Copy the renderer tree, apply the given transformation to the leaf nodes
339
+ * and the optional given transformation to the array nodes, and return the
340
+ * result.
341
+ *
342
+ * Used to provide structured data to the call site (the Renderer tree on
343
+ * `render`, the Score tree on `getScores`, etc.), and to traverse the
344
+ * renderer tree even when we disregard the output (like in
345
+ * `_findWidgets`).
346
+ */
347
+ _mapRenderers<O>(
348
+ // eslint-disable-next-line no-restricted-syntax
349
+ leafMapper: ContentMapper<RendererData, O> &
350
+ HintMapper<RendererData, O>,
351
+ ): ?Tree<O, O, null> {
352
+ const {rendererDataTree} = this.state;
353
+
354
+ if (!rendererDataTree) {
355
+ return null;
356
+ }
357
+
358
+ const mapper = buildMapper()
359
+ .setContentMapper(leafMapper)
360
+ .setHintMapper(leafMapper);
361
+ return mapper.mapTree(rendererDataTree, this.props.shape);
362
+ }
363
+
364
+ _scoreFromRef(ref: ?Renderer): Score {
365
+ if (!ref) {
366
+ return null;
367
+ }
368
+
369
+ const [guess, score] = ref.guessAndScore();
370
+ let state;
371
+ if (ref.getSerializedState) {
372
+ state = ref.getSerializedState();
373
+ }
374
+ return Util.keScoreFromPerseusScore(score, guess, state);
375
+ }
376
+
377
+ /**
378
+ * Return a tree in the shape of the multi-item, with scores at each of
379
+ * the content nodes and `null` at the other leaf nodes.
380
+ */
381
+ getScores(): ScoreTree {
382
+ return this._mapRenderers((data) => this._scoreFromRef(data.ref));
383
+ }
384
+
385
+ /**
386
+ * Return a single composite score for all rendered content nodes.
387
+ * The `guess` is a tree in the shape of the multi-item, with an individual
388
+ * guess at each content node and `null` at the other leaf nodes.
389
+ */
390
+ score(): Score {
391
+ const scores = [];
392
+ const state = [];
393
+ const guess = this._mapRenderers((data) => {
394
+ if (!data.ref) {
395
+ return null;
396
+ }
397
+
398
+ if (data.ref.getSerializedState) {
399
+ state.push(data.ref.getSerializedState());
400
+ }
401
+
402
+ scores.push(data.ref.score());
403
+ return data.ref?.getUserInput();
404
+ });
405
+
406
+ const combinedScore = scores.reduce(Util.combineScores);
407
+
408
+ return Util.keScoreFromPerseusScore(combinedScore, guess, state);
409
+ }
410
+
411
+ /**
412
+ * Return a tree in the shape of the multi-item, with serialized state at
413
+ * each of the content nodes and `null` at the other leaf nodes.
414
+ *
415
+ * If the lastSerializedState argument is supplied, this function will fill
416
+ * in the state of not-currently-rendered content and hint nodes with the
417
+ * values from the previous serialized state. If no lastSerializedState is
418
+ * supplied, `null` will be returned for not-currently-rendered content and
419
+ * hint nodes.
420
+ */
421
+ _getSerializedState(
422
+ lastSerializedState?: SerializedStateTree,
423
+ ): SerializedStateTree {
424
+ return this._mapRenderers((data, _, path) => {
425
+ if (data.ref) {
426
+ return data.ref.getSerializedState();
427
+ }
428
+ if (lastSerializedState) {
429
+ return lens(lastSerializedState).get(path);
430
+ }
431
+ return null;
432
+ });
433
+ }
434
+
435
+ /**
436
+ * Given a tree in the shape of the multi-item, with serialized state at
437
+ * each of the content nodes, restore each state to the corresponding
438
+ * renderer if currently mounted.
439
+ */
440
+ restoreSerializedState(
441
+ serializedState: SerializedState,
442
+ callback?: () => any,
443
+ ) {
444
+ // We want to call our async callback only once all of the childrens'
445
+ // callbacks have run. We add one to this counter before we call out to
446
+ // each renderer and decrement it when it runs our callback.
447
+ let numCallbacks = 0;
448
+ const countCallback = () => {
449
+ numCallbacks--;
450
+ if (callback && numCallbacks === 0) {
451
+ callback();
452
+ }
453
+ };
454
+
455
+ this._mapRenderers((data, _, path) => {
456
+ if (!data.ref) {
457
+ return;
458
+ }
459
+
460
+ const state = lens(serializedState).get(path);
461
+ if (!state) {
462
+ return;
463
+ }
464
+
465
+ numCallbacks++;
466
+ data.ref?.restoreSerializedState(state, countCallback);
467
+ });
468
+ }
469
+
470
+ /**
471
+ * Given an array of renderers, if it happens to be an array of *hint*
472
+ * renderers, then attach a `firstN` method to the array, which allows the
473
+ * layout to render the hints together in one HintsRenderer.
474
+ */
475
+ _annotateRendererArray(
476
+ renderers: $ReadOnlyArray<Renderer>,
477
+ rendererDatas: $ReadOnlyArray<RendererData>,
478
+ shape: ArrayShape,
479
+ ): $ReadOnlyArray<Renderer> {
480
+ if (shape.elementShape.type === "hint") {
481
+ // The shape says that these are HintRendererDatas, even though
482
+ // it's not provable at compile time, so perform a cast.
483
+ const hintRendererDatas: $ReadOnlyArray<HintRendererData> =
484
+ (rendererDatas: any);
485
+
486
+ renderers = [...renderers];
487
+ (renderers: any).firstN = (n) => (
488
+ <HintsRenderer
489
+ {...this._getRendererProps()}
490
+ findExternalWidgets={
491
+ hintRendererDatas[0]
492
+ ? hintRendererDatas[0].findExternalWidgets
493
+ : undefined
494
+ }
495
+ hints={hintRendererDatas.map((d) => d.hint)}
496
+ hintsVisible={n}
497
+ />
498
+ );
499
+ }
500
+ return renderers;
501
+ }
502
+
503
+ /**
504
+ * Return a tree in the shape of the multi-item, with a Renderer at each
505
+ * content node and a HintRenderer at each hint node.
506
+ *
507
+ * This is generated by running each of the `makeRenderer` functions at the
508
+ * leaf nodes.
509
+ */
510
+ _getRenderers(): RendererTree {
511
+ return this.getRenderersMapper.mapTree(
512
+ this.state.rendererDataTree,
513
+ this.props.shape,
514
+ );
515
+ }
516
+
517
+ render(): React.Node {
518
+ if (this.state.renderError) {
519
+ return (
520
+ <div className={css(styles.error)}>
521
+ {i18n.$_("Error rendering: %(error)s", {
522
+ error: String(this.state.renderError),
523
+ })}
524
+ </div>
525
+ );
526
+ }
527
+
528
+ // Pass the renderer tree to the `children` function, which will
529
+ // determine the actual content of this component.
530
+ return this.props.children({
531
+ renderers: this._getRenderers(),
532
+ });
533
+ }
534
+ }
535
+
536
+ const styles = StyleSheet.create({
537
+ error: {
538
+ color: "red",
539
+ },
540
+ });
541
+
542
+ export default MultiRenderer;
@@ -0,0 +1,105 @@
1
+ // @flow
2
+ /**
3
+ * Utility functions to build React PropTypes for multi-items and shapes.
4
+ *
5
+ * If you're writing new components, though, consider using the Item and Shape
6
+ * Flow types instead.
7
+ */
8
+ /* instanbul ignore file */
9
+
10
+ import PropTypes from "prop-types";
11
+
12
+ import {Errors} from "../logging/log.js";
13
+ import {PerseusError} from "../perseus-error.js";
14
+
15
+ import type {Shape} from "./shape-types.js";
16
+
17
+ /**
18
+ * A recursive PropType that accepts Shape objects, and rejects other objects.
19
+ *
20
+ * Usage: `propTypes: {shape: shapePropType}`.
21
+ */
22
+ export function shapePropType(...args: $ReadOnlyArray<any>): $FlowFixMe {
23
+ const itemShape = PropTypes.oneOfType([
24
+ PropTypes.shape({
25
+ type: PropTypes.oneOf(["content"]).isRequired,
26
+ }).isRequired,
27
+ PropTypes.shape({
28
+ type: PropTypes.oneOf(["hint"]).isRequired,
29
+ }).isRequired,
30
+ PropTypes.shape({
31
+ type: PropTypes.oneOf(["tags"]).isRequired,
32
+ }).isRequired,
33
+ PropTypes.shape({
34
+ type: PropTypes.oneOf(["object"]).isRequired,
35
+ shape: PropTypes.objectOf(shapePropType),
36
+ }).isRequired,
37
+ PropTypes.shape({
38
+ type: PropTypes.oneOf(["array"]).isRequired,
39
+ elementShape: shapePropType,
40
+ }).isRequired,
41
+ ]);
42
+
43
+ return itemShape(...args);
44
+ }
45
+
46
+ /**
47
+ * Return a PropType that accepts Items of the given shape, and rejects other
48
+ * objects.
49
+ *
50
+ * Usage: `propTypes: {item: buildPropTypeForShape(myShape)}`
51
+ */
52
+ export function buildPropTypeForShape(shape: Shape): $FlowFixMe {
53
+ return PropTypes.oneOfType([
54
+ PropTypes.shape({
55
+ _multi: buildTreePropTypeForShape(shape),
56
+ }),
57
+ PropTypes.oneOf([null, undefined]),
58
+ ]);
59
+ }
60
+
61
+ /**
62
+ * Return a PropType that accepts ItemTrees of the given shape, and rejects
63
+ * other objects.
64
+ */
65
+ function buildTreePropTypeForShape(shape: Shape): $FlowFixMe {
66
+ if (shape.type === "content") {
67
+ return PropTypes.shape({
68
+ // TODO(mdr): Remove #LegacyContentNode support.
69
+ __type: PropTypes.oneOf(["content", "item"]).isRequired,
70
+ content: PropTypes.string,
71
+ images: PropTypes.objectOf(PropTypes.any),
72
+ widgets: PropTypes.objectOf(PropTypes.any),
73
+ });
74
+ }
75
+ if (shape.type === "hint") {
76
+ return PropTypes.shape({
77
+ __type: PropTypes.oneOf(["hint"]).isRequired,
78
+ content: PropTypes.string,
79
+ images: PropTypes.objectOf(PropTypes.any),
80
+ widgets: PropTypes.objectOf(PropTypes.any),
81
+ replace: PropTypes.bool,
82
+ });
83
+ }
84
+ if (shape.type === "tags") {
85
+ return PropTypes.arrayOf(PropTypes.string.isRequired);
86
+ }
87
+ if (shape.type === "array") {
88
+ const elementPropType = buildTreePropTypeForShape(shape.elementShape);
89
+ return PropTypes.arrayOf(elementPropType.isRequired);
90
+ }
91
+ if (shape.type === "object") {
92
+ const valueShapes = shape.shape;
93
+ const propTypeShape = {};
94
+ Object.keys(valueShapes).forEach((key) => {
95
+ propTypeShape[key] = buildTreePropTypeForShape(
96
+ valueShapes[key],
97
+ ).isRequired;
98
+ });
99
+ return PropTypes.shape(propTypeShape);
100
+ }
101
+ throw new PerseusError(
102
+ `unexpected shape type ${shape.type}`,
103
+ Errors.InvalidInput,
104
+ );
105
+ }
@@ -0,0 +1,47 @@
1
+ // @flow
2
+ /**
3
+ * Type definitions for multi-item shapes.
4
+ *
5
+ * A shape is an object that serves as a runtime type declaration: it specifies
6
+ * a tree structure for a particular class of multi-item.
7
+ *
8
+ * We use shapes instead static compile-time typing because the CMS needs to
9
+ * understand the shape of our content library's multi-items at runtime, and
10
+ * it's not always possible to infer the full shape from an example multi-item.
11
+ *
12
+ * Shapes also enable us to traverse a multi-item-shaped tree with confidence,
13
+ * even when we can't infer the shape from the tree alone.
14
+ *
15
+ * We *could* go all-in on a more general library to make certain Flow types
16
+ * runtime-inspectable, in order to DRY some things up, but that's probably a
17
+ * big ol' infrastructural magic mess, and the narrower scope of Shapes makes
18
+ * it easier to be confident that we've covered all cases rather than having to
19
+ * deal with all possible Javascript types.
20
+ */
21
+ export type ContentShape = {type: "content", ...};
22
+ export type HintShape = {type: "hint", ...};
23
+ export type TagsShape = {type: "tags", ...};
24
+ export type ArrayShape = {
25
+ type: "array",
26
+ /**
27
+ * Each element of an ArrayNode has the same shape, which is specified by
28
+ * the `elementShape` property.
29
+ */
30
+ elementShape: Shape,
31
+ ...
32
+ };
33
+ export type ObjectShape = {
34
+ type: "object",
35
+ /**
36
+ * Each property of an ObjectNode has its own shape, which is specified
37
+ * under the corresponding key in the `shape` property.
38
+ */
39
+ shape: {[k: string]: Shape, ...},
40
+ ...
41
+ };
42
+ export type Shape =
43
+ | ContentShape
44
+ | HintShape
45
+ | TagsShape
46
+ | ArrayShape
47
+ | ObjectShape;