@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,371 @@
1
+ // @flow
2
+ /* eslint-disable react/sort-comp */
3
+ // TODO(joel): teach KAS how to accept an answer only if it's expressed in
4
+ // terms of a certain type.
5
+ // TODO(joel): Allow sigfigs within a range rather than an exact expected
6
+ // value?
7
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
8
+ import lens from "hubble";
9
+ import $ from "jquery";
10
+ import KAS from "kas";
11
+ import * as React from "react";
12
+ import ReactDOM from "react-dom";
13
+ import _ from "underscore";
14
+
15
+ import MathOutput from "../components/math-output.jsx";
16
+ import * as Changeable from "../mixins/changeable.jsx";
17
+ import {ClassNames as ApiClassNames, ApiOptions} from "../perseus-api.jsx";
18
+ import {SignificantFigures, displaySigFigs} from "../sigfigs.jsx";
19
+
20
+ import type {
21
+ APIOptions,
22
+ ChangeHandler,
23
+ FocusPath,
24
+ WidgetExports,
25
+ } from "../types.js";
26
+
27
+ const ALL = "all";
28
+ const MAX_SIGFIGS = 10;
29
+
30
+ export const countSigfigs = function (value: string): number {
31
+ return new SignificantFigures(value).sigFigs;
32
+ };
33
+
34
+ export const sigfigPrint = function (num: number, sigfigs: number): string {
35
+ return displaySigFigs(num, sigfigs, -MAX_SIGFIGS, false);
36
+ };
37
+
38
+ type Rubric =
39
+ | {|
40
+ value: string,
41
+ sigfigs: number,
42
+ accepting: "all",
43
+ |}
44
+ | {|
45
+ value: string,
46
+ sigfigs: number,
47
+ accepting: "some",
48
+ acceptingUnits: $ReadOnlyArray<string>,
49
+ |};
50
+
51
+ type ValidationResult =
52
+ | {|
53
+ type: "points",
54
+ earned: 1 | 0,
55
+ message: ?string,
56
+ total: number,
57
+ |}
58
+ | {|
59
+ type: "invalid",
60
+ message: ?string,
61
+ |};
62
+
63
+ type Props = {|
64
+ apiOptions: APIOptions,
65
+ onChange: ChangeHandler,
66
+ value: string,
67
+ onBlur: (FocusPath) => mixed,
68
+ onFocus: (FocusPath) => mixed,
69
+ |};
70
+
71
+ type DefaultProps = {|
72
+ apiOptions: Props["apiOptions"],
73
+ value: Props["value"],
74
+ |};
75
+
76
+ /* I just wrote this, but it's old by analogy to `OldExpression`, in that it's
77
+ * the version that non-mathquill platforms get stuck with. Constructed with an
78
+ * <input>, a parser, popsicle sticks, and glue.
79
+ *
80
+ * In the same way as OldExpression, this parses continuously as you type, then
81
+ * shows and hides an error buddy. The error message is only shown after a
82
+ * rolling two second delay, but hidden immediately on further typing.
83
+ */
84
+ class OldUnitInput extends React.Component<Props> {
85
+ _errorTimeout: ?TimeoutID;
86
+
87
+ static defaultProps: DefaultProps = {
88
+ apiOptions: ApiOptions.defaults,
89
+ value: "",
90
+ };
91
+
92
+ static validate(userInput: string, rubric: Rubric): ValidationResult {
93
+ const answer = KAS.unitParse(rubric.value).expr;
94
+ const guess = KAS.unitParse(userInput);
95
+ if (!guess.parsed) {
96
+ return {
97
+ type: "invalid",
98
+ message: i18n._("I couldn't understand those units."),
99
+ };
100
+ }
101
+
102
+ // Note: we check sigfigs, then numerical correctness, then units, so
103
+ // the most significant things come last, that way the user will see
104
+ // the most important message.
105
+ let message = null;
106
+
107
+ // did the user specify the right number of sigfigs?
108
+ // TODO(joel) - add a grading mode where the wrong number of sigfigs
109
+ // isn't marked wrong
110
+ const sigfigs = rubric.sigfigs;
111
+ const sigfigsCorrect = countSigfigs(guess.coefficient) === sigfigs;
112
+ if (!sigfigsCorrect) {
113
+ message = i18n._("Check your significant figures.");
114
+ }
115
+
116
+ // now we need to check that the answer is correct to the precision we
117
+ // require.
118
+ let numericallyCorrect;
119
+ try {
120
+ const x = new KAS.Var("x");
121
+ const equality = new KAS.Eq(
122
+ answer.simplify(),
123
+ "=",
124
+ new KAS.Mul(x, guess.expr.simplify()),
125
+ );
126
+
127
+ const conversion = equality.solveLinearEquationForVariable(x);
128
+
129
+ // Make sure the conversion factor between the user's input answer
130
+ // and the canonical answer is 1, to sigfig places.
131
+ // TODO(joel) is this sound?
132
+ numericallyCorrect =
133
+ Number(conversion.eval()).toPrecision(sigfigs) ===
134
+ Number(1).toPrecision(sigfigs);
135
+ } catch (e) {
136
+ numericallyCorrect = false;
137
+ }
138
+
139
+ if (!numericallyCorrect) {
140
+ message = i18n._("That answer is numerically incorrect.");
141
+ }
142
+
143
+ let kasCorrect;
144
+ const guessUnit = primUnits(guess.expr.simplify());
145
+ const answerUnit = primUnits(answer.simplify());
146
+
147
+ if (rubric.accepting === ALL) {
148
+ // We're accepting all units - KAS does the hard work of figuring
149
+ // out if the user's unit is equivalent to the author's unit.
150
+ kasCorrect = KAS.compare(guessUnit, answerUnit).equal;
151
+ } else {
152
+ // Are any of the accepted units the same as what the user entered?
153
+ kasCorrect = _(rubric.acceptingUnits).any((unit) => {
154
+ const thisAnswerUnit = primUnits(
155
+ KAS.unitParse(unit).unit.simplify(),
156
+ );
157
+ return KAS.compare(
158
+ thisAnswerUnit,
159
+ guessUnit,
160
+ // TODO(joel) - make this work as intended.
161
+ // { form: true }
162
+ ).equal;
163
+ });
164
+ }
165
+ if (!kasCorrect) {
166
+ message = i18n._("Check your units.");
167
+ }
168
+
169
+ const correct = kasCorrect && numericallyCorrect && sigfigsCorrect;
170
+
171
+ return {
172
+ type: "points",
173
+ earned: correct ? 1 : 0,
174
+ total: 1,
175
+ message,
176
+ };
177
+ }
178
+
179
+ // TODO(joel) think about showing the error buddy
180
+ render(): React.Node {
181
+ const input = this.props.apiOptions.staticRender ? (
182
+ <MathOutput
183
+ onChange={this.handleChange}
184
+ ref="input" // eslint-disable-line react/no-string-refs
185
+ className={ApiClassNames.INTERACTIVE}
186
+ value={this.props.value}
187
+ onFocus={this.handleFocus}
188
+ onBlur={this.handleBlur}
189
+ />
190
+ ) : (
191
+ <input
192
+ onChange={this.handleChange}
193
+ ref="input" // eslint-disable-line react/no-string-refs
194
+ className={ApiClassNames.INTERACTIVE}
195
+ value={this.props.value}
196
+ onFocus={this.handleFocus}
197
+ onBlur={this.handleBlur}
198
+ />
199
+ );
200
+
201
+ return (
202
+ <div className="old-unit-input">
203
+ {input}
204
+ {/* eslint-disable-next-line react/no-string-refs */}
205
+ <div ref="error" className="error" style={{display: "none"}}>
206
+ {i18n._("I don't understand that")}
207
+ </div>
208
+ </div>
209
+ );
210
+ }
211
+
212
+ _showError: () => void = () => {
213
+ if (this.props.value === "") {
214
+ return;
215
+ }
216
+
217
+ // eslint-disable-next-line react/no-string-refs
218
+ const $error = $(ReactDOM.findDOMNode(this.refs.error));
219
+ if (!$error.is(":visible")) {
220
+ $error
221
+ .css({top: 50, opacity: 0.1})
222
+ .show()
223
+ .animate({top: 0, opacity: 1.0}, 300);
224
+ }
225
+ };
226
+
227
+ _hideError: () => void = () => {
228
+ // eslint-disable-next-line react/no-string-refs
229
+ const $error = $(ReactDOM.findDOMNode(this.refs.error));
230
+ if ($error.is(":visible")) {
231
+ $error.animate({top: 50, opacity: 0.1}, 300, function () {
232
+ // eslint-disable-next-line @babel/no-invalid-this
233
+ $(this).hide();
234
+ });
235
+ }
236
+ };
237
+
238
+ change: (...args: $ReadOnlyArray<any>) => ?$FlowFixMe = (...args) => {
239
+ return Changeable.change.apply(this, args);
240
+ };
241
+
242
+ componentDidUpdate() {
243
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
244
+ // eslint-disable-next-line no-restricted-syntax
245
+ clearTimeout(this._errorTimeout);
246
+ if (KAS.unitParse(this.props.value).parsed) {
247
+ this._hideError();
248
+ } else {
249
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
250
+ // eslint-disable-next-line no-restricted-syntax
251
+ this._errorTimeout = setTimeout(this._showError, 2000);
252
+ }
253
+ }
254
+
255
+ componentWillUnmount() {
256
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
257
+ // eslint-disable-next-line no-restricted-syntax
258
+ clearTimeout(this._errorTimeout);
259
+ }
260
+
261
+ handleBlur: () => void = () => {
262
+ this.props.onBlur([]);
263
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
264
+ // eslint-disable-next-line no-restricted-syntax
265
+ clearTimeout(this._errorTimeout);
266
+ if (!KAS.unitParse(this.props.value).parsed) {
267
+ this._showError();
268
+ }
269
+ };
270
+
271
+ handleChange: (event: SyntheticInputEvent<>) => void = (
272
+ event: SyntheticInputEvent<>,
273
+ ) => {
274
+ this._hideError();
275
+ this.props.onChange({value: event.target.value});
276
+ };
277
+
278
+ simpleValidate: (
279
+ rubric: Rubric,
280
+ onInputError?: () => mixed,
281
+ ) => ValidationResult = (rubric: Rubric, onInputError?: () => mixed) => {
282
+ onInputError = onInputError || function () {};
283
+ return OldUnitInput.validate(this.getUserInput(), rubric);
284
+ };
285
+
286
+ getUserInput: () => string = () => {
287
+ return this.props.value;
288
+ };
289
+
290
+ // begin mobile stuff
291
+
292
+ getInputPaths: () => $ReadOnlyArray<$ReadOnlyArray<any>> = () => {
293
+ // The widget itself is an input, so we return a single empty list to
294
+ // indicate this.
295
+ return [[]];
296
+ };
297
+
298
+ focusInputPath: (inputPath: FocusPath) => void = (inputPath: FocusPath) => {
299
+ // eslint-disable-next-line react/no-string-refs
300
+ const input = ReactDOM.findDOMNode(this.refs.input);
301
+ if (input) {
302
+ /**
303
+ * TODO(somewhatabstract, JIRA-XXXX):
304
+ * Change to using a ref callback so that focus() can be
305
+ * accessed.
306
+ */
307
+ // $FlowFixMe[prop-missing]
308
+ input.focus();
309
+ }
310
+ };
311
+
312
+ handleFocus: () => void = () => {
313
+ this.props.onFocus([]);
314
+ };
315
+
316
+ blurInputPath: (inputPath: FocusPath) => void = (inputPath: FocusPath) => {
317
+ // eslint-disable-next-line react/no-string-refs
318
+ const input = ReactDOM.findDOMNode(this.refs.input);
319
+ if (input) {
320
+ /**
321
+ * TODO(somewhatabstract, JIRA-XXXX):
322
+ * Change to using a ref callback so that focus() can be
323
+ * accessed.
324
+ */
325
+ // $FlowFixMe[prop-missing]
326
+ input.blur();
327
+ }
328
+ };
329
+
330
+ setInputValue: (
331
+ path: FocusPath,
332
+ newValue: string,
333
+ cb: () => mixed,
334
+ ) => void = (path: FocusPath, newValue: string, cb: () => mixed) => {
335
+ this.props.onChange(
336
+ {
337
+ value: newValue,
338
+ },
339
+ cb,
340
+ );
341
+ };
342
+
343
+ getDOMNodeForPath: () => null | Element | Text = () => {
344
+ // eslint-disable-next-line react/no-string-refs
345
+ return ReactDOM.findDOMNode(this.refs.input);
346
+ };
347
+
348
+ getGrammarTypeForPath: (inputPath: FocusPath) => string = (
349
+ inputPath: FocusPath,
350
+ ) => {
351
+ return "unit";
352
+ };
353
+
354
+ // end mobile stuff
355
+ }
356
+
357
+ // Extract the primitive units from a unit expression. This first simplifies
358
+ // `expr` to a `Mul` like "5 kg m / s^2" then removes the first term.
359
+ const primUnits = function (expr) {
360
+ return expr.simplify().asMul().partition()[1].flatten().simplify();
361
+ };
362
+
363
+ export default ({
364
+ name: "unit-input",
365
+ displayName: "Unit",
366
+ defaultAlignment: "inline-block",
367
+ widget: OldUnitInput,
368
+ transform: (x: any): any => lens(x).del(["value"]).freeze(),
369
+ version: {major: 0, minor: 1},
370
+ hidden: true,
371
+ }: WidgetExports<typeof OldUnitInput>);
@@ -0,0 +1,84 @@
1
+ // @flow
2
+ /**
3
+ * This is a component that adds a link to the transcript for embedded videos in articles.
4
+ */
5
+
6
+ import {View, Text} from "@khanacademy/wonder-blocks-core";
7
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
8
+ import {Strut} from "@khanacademy/wonder-blocks-layout";
9
+ import Link from "@khanacademy/wonder-blocks-link";
10
+ import {StyleSheet} from "aphrodite";
11
+ import * as React from "react";
12
+
13
+ import {getDependencies} from "../dependencies.js";
14
+
15
+ const IS_URL = /^https?:\/\//;
16
+
17
+ const getYoutubeId = (url: string): string => {
18
+ const regExp =
19
+ /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
20
+ const match = url.match(regExp);
21
+ if (match && match[7].length === 11) {
22
+ return match[7];
23
+ }
24
+ return "videoNotFound";
25
+ };
26
+
27
+ // will accept a youtube link or a video's slug name from embedded video and then query accordingly.
28
+ type Props = {|
29
+ location: string,
30
+ |};
31
+
32
+ /**
33
+ * Video Transcript Link Component.
34
+ */
35
+ const VideoTranscriptLink = (props: Props): React.Node => {
36
+ const {location} = props;
37
+ const {useVideo} = getDependencies();
38
+ const [id, kind] = IS_URL.test(location)
39
+ ? [getYoutubeId(location), "YOUTUBE_ID"]
40
+ : [location, "READABLE_ID"];
41
+ // The result value conforms to the wonder-blocks-data `Result` type
42
+ // which is used by our GraphQL framework.
43
+ const result = useVideo(id, kind);
44
+
45
+ switch (result.status) {
46
+ case "loading":
47
+ return <View>{i18n._("Loading...")}</View>;
48
+ case "success": {
49
+ const video = result.data?.video;
50
+ return (
51
+ <View style={styles.transcriptLink}>
52
+ <Text>{video?.title}</Text>
53
+ <Strut size={10} />
54
+ <Link
55
+ href={
56
+ "/transcript/" +
57
+ (video?.contentId || "videoNotFound")
58
+ }
59
+ target="_blank"
60
+ className="visited-no-recolor"
61
+ >
62
+ {i18n._("See video transcript")}
63
+ </Link>
64
+ </View>
65
+ );
66
+ }
67
+ case "error":
68
+ return <View>{i18n._("Something went wrong.")}</View>;
69
+ case "aborted":
70
+ return <View>{i18n._("Something went wrong.")}</View>;
71
+ default:
72
+ return <View>{i18n._("Something went wrong.")}</View>;
73
+ }
74
+ };
75
+
76
+ const styles = StyleSheet.create({
77
+ transcriptLink: {
78
+ flexDirection: "row",
79
+ width: "100%",
80
+ justifyContent: "center",
81
+ },
82
+ });
83
+
84
+ export default VideoTranscriptLink;
@@ -0,0 +1,211 @@
1
+ // @flow
2
+ import {render, screen} from "@testing-library/react";
3
+ import * as React from "react";
4
+ import "@testing-library/jest-dom/extend-expect";
5
+
6
+ import {testDependencies} from "../../../../testing/test-dependencies.js";
7
+ import * as Dependencies from "../dependencies.js";
8
+
9
+ import VideoTranscriptLink from "./video-transcript-link.jsx";
10
+
11
+ describe("VideoTranscriptLink", () => {
12
+ beforeEach(() => {
13
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue({
14
+ ...testDependencies,
15
+ useVideo: (videoId, kind) => {
16
+ if (videoId === "5qap5aO4i9A") {
17
+ return {
18
+ status: "success",
19
+ data: {
20
+ video: {
21
+ id: "YoutubeVideo",
22
+ title: "Youtube Video Title",
23
+ youtubeId: "5qap5aO4i9A",
24
+ contentId: "contentId",
25
+ __typename: "Video",
26
+ },
27
+ },
28
+ };
29
+ }
30
+ if (videoId === "slug-video-id") {
31
+ return {
32
+ status: "success",
33
+ data: {
34
+ video: {
35
+ id: "VideoId",
36
+ title: "Slug Video Title",
37
+ youtubeId: "YoutubeId",
38
+ contentId: "contentId",
39
+ __typename: "Video",
40
+ },
41
+ },
42
+ };
43
+ }
44
+ return {status: "loading"};
45
+ },
46
+ });
47
+ });
48
+
49
+ it("renders the title with youtube video name when given a youtube link", async () => {
50
+ // Arrange
51
+ render(
52
+ <VideoTranscriptLink location="https://www.youtube.com/watch?v=5qap5aO4i9A" />,
53
+ );
54
+
55
+ // Assert
56
+ // Find by Text test will fail if the indicated text is missing
57
+ await screen.findByText("Youtube Video Title");
58
+ });
59
+
60
+ it("renders the title with slug video name when provided a slug-id", async () => {
61
+ // Arrange
62
+ render(<VideoTranscriptLink location="slug-video-id" />);
63
+
64
+ // Assert
65
+ await screen.findByText("Slug Video Title");
66
+ });
67
+
68
+ it("renders the button with the correct link", async () => {
69
+ // Act
70
+ render(<VideoTranscriptLink location="slug-video-id" />);
71
+
72
+ // Assert
73
+ await screen.findByText("Slug Video Title");
74
+ await expect(screen.getByRole("link")).toHaveAttribute(
75
+ "href",
76
+ "/transcript/contentId",
77
+ );
78
+ });
79
+
80
+ it("can parse a youtube URL with feature text", async () => {
81
+ // Act
82
+ render(
83
+ <VideoTranscriptLink location="http://www.youtube.com/watch?v=5qap5aO4i9A&feature=feedrec_grec_index" />,
84
+ );
85
+
86
+ // Assert
87
+ await screen.findByText("Youtube Video Title");
88
+ });
89
+
90
+ it("can parse a youtube URL with userID text", async () => {
91
+ // Act
92
+ render(
93
+ <VideoTranscriptLink location="http://www.youtube.com/user/IngridMichaelsonVEVO#p/a/u/1/5qap5aO4i9A" />,
94
+ );
95
+
96
+ // Assert
97
+ await screen.findByText("Youtube Video Title");
98
+ });
99
+
100
+ it("can parse a youtube URL with location/language text", async () => {
101
+ // Act
102
+ render(
103
+ <VideoTranscriptLink location="http://www.youtube.com/v/5qap5aO4i9A?fs=1&amp;hl=en_US&amp;rel=0" />,
104
+ );
105
+
106
+ // Assert
107
+ await screen.findByText("Youtube Video Title");
108
+ });
109
+
110
+ it("can parse a youtube URL with a time stamp", async () => {
111
+ // Act
112
+ render(
113
+ <VideoTranscriptLink location="http://www.youtube.com/watch?v=5qap5aO4i9A#t=0m10s" />,
114
+ );
115
+
116
+ // Assert
117
+ await screen.findByText("Youtube Video Title");
118
+ });
119
+
120
+ it("can parse an embed youtube URL", async () => {
121
+ // Act
122
+ render(
123
+ <VideoTranscriptLink location="http://www.youtube.com/embed/5qap5aO4i9A?rel=0" />,
124
+ );
125
+
126
+ // Assert
127
+ await screen.findByText("Youtube Video Title");
128
+ });
129
+
130
+ it("can parse a shorthand youtube URL", async () => {
131
+ // Act
132
+ render(<VideoTranscriptLink location="http://youtu.be/5qap5aO4i9A" />);
133
+
134
+ // Assert
135
+ await screen.findByText("Youtube Video Title");
136
+ });
137
+
138
+ it("should link to /transcript/videoNotFound if the URL is not a youtube URL", () => {
139
+ // Arrange
140
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue({
141
+ ...testDependencies,
142
+ useVideo: (videoId, kind) => {
143
+ return {status: "success", data: {}};
144
+ },
145
+ });
146
+
147
+ render(<VideoTranscriptLink location="http://stackoverflow.com" />);
148
+
149
+ expect(
150
+ screen.getByRole("link", {name: "See video transcript"}),
151
+ ).toHaveAttribute("href", "/transcript/videoNotFound");
152
+ });
153
+
154
+ it("should handle a success state", () => {
155
+ // Arrange
156
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue({
157
+ ...testDependencies,
158
+ useVideo: (videoId, kind) => {
159
+ return {status: "loading"};
160
+ },
161
+ });
162
+
163
+ render(<VideoTranscriptLink location="regula-1" />);
164
+
165
+ expect(screen.getByText("Loading...")).toBeInTheDocument();
166
+ });
167
+
168
+ it("should link to /transcript/videoNotFound if there's no data", () => {
169
+ // Arrange
170
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue({
171
+ ...testDependencies,
172
+ useVideo: (videoId, kind) => {
173
+ return {status: "success", data: {}};
174
+ },
175
+ });
176
+
177
+ render(<VideoTranscriptLink location="regula-1" />);
178
+
179
+ expect(
180
+ screen.getByRole("link", {name: "See video transcript"}),
181
+ ).toHaveAttribute("href", "/transcript/videoNotFound");
182
+ });
183
+
184
+ it("should handle an error state", () => {
185
+ // Arrange
186
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue({
187
+ ...testDependencies,
188
+ useVideo: (videoId, kind) => {
189
+ return {status: "error"};
190
+ },
191
+ });
192
+
193
+ render(<VideoTranscriptLink location="regula-1" />);
194
+
195
+ expect(screen.getByText("Something went wrong.")).toBeInTheDocument();
196
+ });
197
+
198
+ it("should handle an aborted state", () => {
199
+ // Arrange
200
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue({
201
+ ...testDependencies,
202
+ useVideo: (videoId, kind) => {
203
+ return {status: "aborted"};
204
+ },
205
+ });
206
+
207
+ render(<VideoTranscriptLink location="regula-1" />);
208
+
209
+ expect(screen.getByText("Something went wrong.")).toBeInTheDocument();
210
+ });
211
+ });