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