@khanacademy/perseus 0.0.1 → 0.2.1

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