@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
package/src/util.js ADDED
@@ -0,0 +1,990 @@
1
+ /* eslint-disable @babel/no-invalid-this, getter-return, one-var */
2
+ // @flow
3
+ import _ from "underscore";
4
+
5
+ import {Errors} from "./logging/log.js";
6
+ import {PerseusError} from "./perseus-error.js";
7
+ import KhanAnswerTypes from "./util/answer-types.js";
8
+
9
+ import type {Widget, PerseusScore, KEScore} from "./types.js";
10
+
11
+ type WordPosition = {|
12
+ start: number,
13
+ end: number,
14
+ |};
15
+
16
+ type WordAndPosition = {|
17
+ string: string,
18
+ pos: WordPosition,
19
+ |};
20
+
21
+ type RNG = () => number;
22
+
23
+ type ParsedValue = {|
24
+ value: number,
25
+ exact: boolean,
26
+ |};
27
+
28
+ // TODO: dedupe this with Coord in interactive2/types.js
29
+ type Coordinates = [number, number];
30
+
31
+ export type GridDimensions = {|
32
+ scale: number,
33
+ tickStep: number,
34
+ unityLabel: boolean,
35
+ |};
36
+
37
+ type QueryParams = {
38
+ [param: string]: string,
39
+ ...
40
+ };
41
+
42
+ type Position = {|
43
+ top: number,
44
+ left: number,
45
+ |};
46
+
47
+ type TouchHandlers = {|
48
+ pointerDown: boolean,
49
+ currentTouchIdentifier: ?string,
50
+ |};
51
+
52
+ let supportsPassive = false;
53
+
54
+ const svgLabelsRegex = /^web\+graphie:/;
55
+ // For offline exercises in the mobile app, we download the graphie data
56
+ // (svgs and localized data files) and serve them from the local file
57
+ // system (with file://). We replace urls that start with `web+graphie`
58
+ // in the perseus json with this `file+graphie` prefix to indicate that
59
+ // they should have the `file://` protocol instead of `https://`.
60
+ const svgLocalLabelsRegex = /^file\+graphie:/;
61
+
62
+ const nestedMap = function <T, M>(
63
+ children: T | $ReadOnlyArray<T>,
64
+ func: (T) => M,
65
+ context: mixed,
66
+ ): M | $ReadOnlyArray<M> {
67
+ if (Array.isArray(children)) {
68
+ return _.map(children, function (child) {
69
+ return nestedMap(child, func);
70
+ });
71
+ }
72
+ return func.call(context, children);
73
+ };
74
+
75
+ /**
76
+ * Used to compare equality of two input paths, which are represented as
77
+ * arrays of strings.
78
+ */
79
+ function inputPathsEqual(
80
+ a: ?$ReadOnlyArray<string>,
81
+ b: ?$ReadOnlyArray<string>,
82
+ ): boolean {
83
+ if (a == null || b == null) {
84
+ return (a == null) === (b == null);
85
+ }
86
+
87
+ return (
88
+ a.length === b.length &&
89
+ a.every((item, index) => {
90
+ return b[index] === item;
91
+ })
92
+ );
93
+ }
94
+
95
+ // Keep `rWidgetRule` in sync with `WIDGET_RE`
96
+ // in content_internal/perseus_traversal.py
97
+ // sync-start:widget-id-regex 1271666319 services/content-editing/perseus/translation/content_traversal.go
98
+ // sync-start:widget-id-regex 680240484 services/static/javascript/perseus-merged-editor-package/editor.jsx
99
+ // sync-start:widget-id-regex 1400368396 content_internal/perseus_traversal.py
100
+ const rWidgetRule: RegExp = /^\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]/;
101
+ const rTypeFromWidgetId: RegExp = /^([a-z-]+) ([0-9]+)$/;
102
+ // sync-end:widget-id-regex
103
+
104
+ const rWidgetParts: RegExp = new RegExp(rWidgetRule.source + "$");
105
+ const snowman = "\u2603";
106
+ const noScore: PerseusScore = {
107
+ type: "points",
108
+ earned: 0,
109
+ total: 0,
110
+ message: null,
111
+ };
112
+
113
+ const seededRNG: (seed: number) => RNG = function (seed: number): RNG {
114
+ let randomSeed = seed;
115
+
116
+ return function () {
117
+ // Robert Jenkins' 32 bit integer hash function.
118
+ let seed = randomSeed;
119
+ seed = (seed + 0x7ed55d16 + (seed << 12)) & 0xffffffff;
120
+ seed = (seed ^ 0xc761c23c ^ (seed >>> 19)) & 0xffffffff;
121
+ seed = (seed + 0x165667b1 + (seed << 5)) & 0xffffffff;
122
+ seed = ((seed + 0xd3a2646c) ^ (seed << 9)) & 0xffffffff;
123
+ seed = (seed + 0xfd7046c5 + (seed << 3)) & 0xffffffff;
124
+ seed = (seed ^ 0xb55a4f09 ^ (seed >>> 16)) & 0xffffffff;
125
+ return (randomSeed = seed & 0xfffffff) / 0x10000000;
126
+ };
127
+ };
128
+
129
+ // Shuffle an array using a given random seed or function.
130
+ // If `ensurePermuted` is true, the input and ouput are guaranteed to be
131
+ // distinct permutations.
132
+ function shuffle<T>(
133
+ array: $ReadOnlyArray<T>,
134
+ randomSeed: number | RNG,
135
+ ensurePermuted: boolean = false,
136
+ ): $ReadOnlyArray<T> {
137
+ // Always return a copy of the input array
138
+ const shuffled = _.clone(array);
139
+
140
+ // Handle edge cases (input array is empty or uniform)
141
+ if (
142
+ !shuffled.length ||
143
+ _.all(shuffled, function (value) {
144
+ return _.isEqual(value, shuffled[0]);
145
+ })
146
+ ) {
147
+ return shuffled;
148
+ }
149
+
150
+ let random;
151
+ if (typeof randomSeed === "function") {
152
+ random = randomSeed;
153
+ } else {
154
+ random = seededRNG(randomSeed);
155
+ }
156
+
157
+ do {
158
+ // Fischer-Yates shuffle
159
+ for (let top = shuffled.length; top > 0; top--) {
160
+ const newEnd = Math.floor(random() * top),
161
+ temp = shuffled[newEnd];
162
+
163
+ shuffled[newEnd] = shuffled[top - 1];
164
+ shuffled[top - 1] = temp;
165
+ }
166
+ } while (ensurePermuted && _.isEqual(array, shuffled));
167
+
168
+ return shuffled;
169
+ }
170
+
171
+ /**
172
+ * TODO(somewhatabstract, FEI-3463):
173
+ * Drop this custom split thing.
174
+ */
175
+ // In IE8, split doesn't work right. Implement it ourselves.
176
+ const split: (str: string, r: RegExp) => $ReadOnlyArray<string> = "x".split(
177
+ /(.)/g,
178
+ ).length
179
+ ? function (str, r) {
180
+ return str.split(r);
181
+ }
182
+ : function (str, r) {
183
+ // Based on Steven Levithan's MIT-licensed split, available at
184
+ // http://blog.stevenlevithan.com/archives/cross-browser-split
185
+ const output = [];
186
+ let lastIndex = (r.lastIndex = 0);
187
+ let match;
188
+
189
+ while ((match = r.exec(str))) {
190
+ const m = match;
191
+ output.push(str.slice(lastIndex, m.index));
192
+ output.push(...m.slice(1));
193
+ lastIndex = m.index + m[0].length;
194
+ }
195
+
196
+ output.push(str.slice(lastIndex));
197
+ return output;
198
+ };
199
+
200
+ /**
201
+ * Combine two score objects.
202
+ *
203
+ * Given two score objects for two different widgets, combine them so that
204
+ * if one is wrong, the total score is wrong, etc.
205
+ */
206
+ function combineScores(
207
+ scoreA: PerseusScore,
208
+ scoreB: PerseusScore,
209
+ ): PerseusScore {
210
+ let message;
211
+
212
+ if (scoreA.type === "points" && scoreB.type === "points") {
213
+ if (
214
+ scoreA.message &&
215
+ scoreB.message &&
216
+ scoreA.message !== scoreB.message
217
+ ) {
218
+ // TODO(alpert): Figure out how to combine messages usefully
219
+ message = null;
220
+ } else {
221
+ message = scoreA.message || scoreB.message;
222
+ }
223
+
224
+ return {
225
+ type: "points",
226
+ earned: scoreA.earned + scoreB.earned,
227
+ total: scoreA.total + scoreB.total,
228
+ message: message,
229
+ };
230
+ }
231
+ if (scoreA.type === "points" && scoreB.type === "invalid") {
232
+ return scoreB;
233
+ }
234
+ if (scoreA.type === "invalid" && scoreB.type === "points") {
235
+ return scoreA;
236
+ }
237
+ if (scoreA.type === "invalid" && scoreB.type === "invalid") {
238
+ if (
239
+ scoreA.message &&
240
+ scoreB.message &&
241
+ scoreA.message !== scoreB.message
242
+ ) {
243
+ // TODO(alpert): Figure out how to combine messages usefully
244
+ message = null;
245
+ } else {
246
+ message = scoreA.message || scoreB.message;
247
+ }
248
+
249
+ return {
250
+ type: "invalid",
251
+ message: message,
252
+ };
253
+ }
254
+
255
+ /**
256
+ * The above checks cover all combinations of score type, so if we get here
257
+ * then something is amiss with our inputs.
258
+ */
259
+ throw new PerseusError(
260
+ "PerseusScore with unknown type encountered",
261
+ Errors.InvalidInput,
262
+ {
263
+ metadata: {
264
+ scoreA: JSON.stringify(scoreA),
265
+ scoreB: JSON.stringify(scoreB),
266
+ },
267
+ },
268
+ );
269
+ }
270
+
271
+ function keScoreFromPerseusScore(
272
+ score: PerseusScore,
273
+ guess: $FlowFixMe,
274
+ state: $FlowFixMe,
275
+ ): KEScore {
276
+ if (score.type === "points") {
277
+ return {
278
+ empty: false,
279
+ correct: score.earned >= score.total,
280
+ message: score.message,
281
+ guess: guess,
282
+ state: state,
283
+ };
284
+ }
285
+ if (score.type === "invalid") {
286
+ return {
287
+ empty: true,
288
+ correct: false,
289
+ message: score.message,
290
+ suppressAlmostThere: score.suppressAlmostThere,
291
+ guess: guess,
292
+ state: state,
293
+ };
294
+ }
295
+ throw new PerseusError(
296
+ "Invalid score type: " + score.type,
297
+ Errors.InvalidInput,
298
+ {
299
+ metadata: {
300
+ score: JSON.stringify(score),
301
+ guess: JSON.stringify(guess),
302
+ state: JSON.stringify(state),
303
+ },
304
+ },
305
+ );
306
+ }
307
+
308
+ /**
309
+ * Return the first valid interpretation of 'text' as a number, in the form
310
+ * {value: 2.3, exact: true}.
311
+ */
312
+ function firstNumericalParse(text: string): ?ParsedValue {
313
+ // TODO(alpert): This is sort of hacky...
314
+ let first;
315
+ const val = KhanAnswerTypes.predicate.createValidatorFunctional(
316
+ function (ans) {
317
+ first = ans;
318
+ return true; /* break */
319
+ },
320
+ {
321
+ simplify: "optional",
322
+ inexact: true,
323
+ forms: "integer, proper, improper, pi, log, mixed, decimal",
324
+ },
325
+ );
326
+
327
+ val(text);
328
+ return first;
329
+ }
330
+
331
+ function stringArrayOfSize(size: number): $ReadOnlyArray<string> {
332
+ return _(size).times(function () {
333
+ return "";
334
+ });
335
+ }
336
+
337
+ /**
338
+ * For a graph's x or y dimension, given the tick step,
339
+ * the ranges extent (e.g. [-10, 10]), the pixel dimension constraint,
340
+ * and the grid step, return a bunch of configurations for that dimension.
341
+ *
342
+ * Example:
343
+ * gridDimensionConfig(10, [-50, 50], 400, 5)
344
+ *
345
+ * Returns: {
346
+ * scale: 4,
347
+ * snap: 2.5,
348
+ * tickStep: 2,
349
+ * unityLabel: true
350
+ * };
351
+ */
352
+ function gridDimensionConfig(
353
+ absTickStep: number,
354
+ extent: Coordinates,
355
+ dimensionConstraint: number,
356
+ gridStep: number,
357
+ ): GridDimensions {
358
+ const scale = scaleFromExtent(extent, dimensionConstraint);
359
+ const stepPx = absTickStep * scale;
360
+ const unityLabel = stepPx > 30;
361
+ return {
362
+ scale: scale,
363
+ tickStep: absTickStep / gridStep,
364
+ unityLabel: unityLabel,
365
+ };
366
+ }
367
+ /**
368
+ * Given the range, step, and boxSize, calculate the reasonable gridStep.
369
+ * Used for when one was not given explicitly.
370
+ *
371
+ * Example:
372
+ * getGridStep([[-10, 10], [-10, 10]], [1, 1], 340)
373
+ *
374
+ * Returns: [1, 1]
375
+ *
376
+ * TODO(somewhatabstract, FEI-3464): Consolidate query string parsing functions.
377
+ */
378
+ function getGridStep(
379
+ range: [Coordinates, Coordinates],
380
+ step: Coordinates,
381
+ boxSize: number,
382
+ ): Coordinates {
383
+ return _(2).times(function (i) {
384
+ const scale = scaleFromExtent(range[i], boxSize);
385
+ const gridStep = gridStepFromTickStep(step[i], scale);
386
+ return gridStep;
387
+ });
388
+ }
389
+
390
+ function snapStepFromGridStep(gridStep: [number, number]): [number, number] {
391
+ return [gridStep[0] / 2, gridStep[1] / 2];
392
+ }
393
+
394
+ /**
395
+ * Given the tickStep and the graph's scale, find a
396
+ * grid step.
397
+ * Example:
398
+ * gridStepFromTickStep(200, 0.2) // returns 100
399
+ */
400
+ function gridStepFromTickStep(tickStep: number, scale: number): ?number {
401
+ const tickWidth = tickStep * scale;
402
+ const x = tickStep;
403
+ const y = Math.pow(10, Math.floor(Math.log(x) / Math.LN10));
404
+ const leadingDigit = Math.floor(x / y);
405
+ if (tickWidth < 25) {
406
+ return tickStep;
407
+ }
408
+ if (tickWidth < 50) {
409
+ if (leadingDigit === 5) {
410
+ return tickStep;
411
+ }
412
+ return tickStep / 2;
413
+ }
414
+ if (leadingDigit === 1) {
415
+ return tickStep / 2;
416
+ }
417
+ if (leadingDigit === 2) {
418
+ return tickStep / 4;
419
+ }
420
+ if (leadingDigit === 5) {
421
+ return tickStep / 5;
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Given the range and a dimension, come up with the appropriate
427
+ * scale.
428
+ * Example:
429
+ * scaleFromExtent([-25, 25], 500) // returns 10
430
+ */
431
+ function scaleFromExtent(
432
+ extent: Coordinates,
433
+ dimensionConstraint: number,
434
+ ): number {
435
+ const span = extent[1] - extent[0];
436
+ const scale = dimensionConstraint / span;
437
+ return scale;
438
+ }
439
+
440
+ /**
441
+ * Return a reasonable tick step given extent and dimension.
442
+ * (extent is [begin, end] of the domain.)
443
+ * Example:
444
+ * tickStepFromExtent([-10, 10], 300) // returns 2
445
+ */
446
+ function tickStepFromExtent(
447
+ extent: Coordinates,
448
+ dimensionConstraint: number,
449
+ ): number {
450
+ const span = extent[1] - extent[0];
451
+
452
+ let tickFactor;
453
+ // If single number digits
454
+ if (15 < span && span <= 20) {
455
+ tickFactor = 23;
456
+
457
+ // triple digit or decimal
458
+ } else if (span > 100 || span < 5) {
459
+ tickFactor = 10;
460
+
461
+ // double digit
462
+ } else {
463
+ tickFactor = 16;
464
+ }
465
+ const constraintFactor = dimensionConstraint / 500;
466
+ const desiredNumTicks = tickFactor * constraintFactor;
467
+ return tickStepFromNumTicks(span, desiredNumTicks);
468
+ }
469
+
470
+ /**
471
+ * Find a good tick step for the desired number of ticks in the range
472
+ * Modified from d3.scale.linear: d3_scale_linearTickRange.
473
+ * Thanks, mbostock!
474
+ * Example:
475
+ * tickStepFromNumTicks(50, 6) // returns 10
476
+ */
477
+ function tickStepFromNumTicks(span: number, numTicks: number): number {
478
+ let step = Math.pow(10, Math.floor(Math.log(span / numTicks) / Math.LN10));
479
+ const err = (numTicks / span) * step;
480
+
481
+ // Filter ticks to get closer to the desired count.
482
+ if (err <= 0.15) {
483
+ step *= 10;
484
+ } else if (err <= 0.35) {
485
+ step *= 5;
486
+ } else if (err <= 0.75) {
487
+ step *= 2;
488
+ }
489
+
490
+ // Round start and stop values to step interval.
491
+ return step;
492
+ }
493
+
494
+ type Range = [number, number]; // [min, max]
495
+
496
+ const constrainTickStep = (step: number, range: Range): number => {
497
+ const span = range[1] - range[0];
498
+ const numTicks = span / step;
499
+ if (numTicks <= 10) {
500
+ // Will displays fine on mobile
501
+ return step;
502
+ }
503
+ if (numTicks <= 20) {
504
+ // Will be crowded on mobile, so hide every other tick
505
+ return step * 2;
506
+ }
507
+ // Fallback in case we somehow have more than 20 ticks
508
+ // Note: This shouldn't happen due to GraphSettings.validStep
509
+ return tickStepFromNumTicks(span, 10);
510
+ };
511
+
512
+ /**
513
+ * Constrain tick steps intended for desktop size graphs
514
+ * to something more suitable for mobile size graphs.
515
+ * Specifically, we aim for 10 or fewer ticks per graph axis.
516
+ */
517
+ function constrainedTickStepsFromTickSteps(
518
+ tickSteps: Coordinates,
519
+ ranges: [Range, Range],
520
+ ): Coordinates {
521
+ return [
522
+ constrainTickStep(tickSteps[0], ranges[0]),
523
+ constrainTickStep(tickSteps[1], ranges[1]),
524
+ ];
525
+ }
526
+
527
+ /**
528
+ * Transparently update deprecated props so that the code to deal
529
+ * with them only lives in one place: (Widget).deprecatedProps
530
+ *
531
+ * For example, if a boolean `foo` was deprecated in favor of a
532
+ * number 'bar':
533
+ * deprecatedProps: {
534
+ * foo: function(props) {
535
+ * return {bar: props.foo ? 1 : 0};
536
+ * }
537
+ * }
538
+ */
539
+ const DeprecationMixin: $FlowFixMe = {
540
+ // This lifecycle stage is only called before first render
541
+ // TODO(jangmi, CP-3288): Remove usage of `UNSAFE_componentWillMount`
542
+ UNSAFE_componentWillMount: function () {
543
+ const newProps = {};
544
+
545
+ _.each(
546
+ this.deprecatedProps,
547
+ function (func, prop) {
548
+ if (_.has(this.props, prop)) {
549
+ _.extend(newProps, func(this.props));
550
+ }
551
+ },
552
+ this,
553
+ );
554
+
555
+ if (!_.isEmpty(newProps)) {
556
+ // Set new props directly so that widget renders correctly
557
+ // when it first mounts, even though these will be overwritten
558
+ // almost immediately afterwards...
559
+ _.extend(this.props, newProps);
560
+
561
+ // ...when we propagate the new props upwards and they come
562
+ // back down again.
563
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API
564
+ // eslint-disable-next-line no-restricted-syntax
565
+ setTimeout(this.props.onChange, 0, newProps);
566
+ }
567
+ },
568
+ };
569
+
570
+ /**
571
+ * Approximate equality on numbers and primitives.
572
+ */
573
+ function eq<T>(x: T, y: T): boolean {
574
+ if (typeof x === "number" && typeof y === "number") {
575
+ return Math.abs(x - y) < 1e-9;
576
+ }
577
+ return x === y;
578
+ }
579
+
580
+ /**
581
+ * Deep approximate equality on primitives, numbers, arrays, and objects.
582
+ * Recursive.
583
+ */
584
+ function deepEq<T>(x: T, y: T): boolean {
585
+ if (Array.isArray(x) && Array.isArray(y)) {
586
+ if (x.length !== y.length) {
587
+ return false;
588
+ }
589
+ for (let i = 0; i < x.length; i++) {
590
+ if (!deepEq(x[i], y[i])) {
591
+ return false;
592
+ }
593
+ }
594
+ return true;
595
+ }
596
+ if (Array.isArray(x) || Array.isArray(y)) {
597
+ return false;
598
+ }
599
+ if (typeof x === "function" && typeof y === "function") {
600
+ return eq(x, y);
601
+ }
602
+ if (typeof x === "function" || typeof y === "function") {
603
+ return false;
604
+ }
605
+ if (typeof x === "object" && typeof y === "object" && !!x && !!y) {
606
+ return (
607
+ x === y ||
608
+ (_.all(x, function (v, k) {
609
+ return deepEq(y[k], v);
610
+ }) &&
611
+ _.all(y, function (v, k) {
612
+ return deepEq(x[k], v);
613
+ }))
614
+ );
615
+ }
616
+ if ((typeof x === "object" && !!x) || (typeof y === "object" && !!y)) {
617
+ return false;
618
+ }
619
+ return eq(x, y);
620
+ }
621
+
622
+ /**
623
+ * Query String Parser
624
+ *
625
+ * Original from:
626
+ * http://stackoverflow.com/questions/901115/get-querystring-values-in-javascript/2880929#2880929
627
+ */
628
+ function parseQueryString(query: string): QueryParams {
629
+ // TODO(jangmi, CP-3340): Use withLocation to access SSR safe location.
630
+ // eslint-disable-next-line no-restricted-syntax
631
+ query = query || window.location.search.substring(1);
632
+ const urlParams = {};
633
+ // Regex for replacing addition symbol with a space
634
+ const a = /\+/g;
635
+ const r = /([^&=]+)=?([^&]*)/g;
636
+ const d = function (s) {
637
+ return decodeURIComponent(s.replace(a, " "));
638
+ };
639
+
640
+ let e;
641
+ while ((e = r.exec(query))) {
642
+ const m = e;
643
+ urlParams[d(m[1])] = d(m[2]);
644
+ }
645
+
646
+ return urlParams;
647
+ }
648
+
649
+ /**
650
+ * Query string adder
651
+ * Works for URLs without #.
652
+ * Original from:
653
+ * http://stackoverflow.com/questions/5999118/add-or-update-query-string-parameter
654
+ */
655
+ function updateQueryString(uri: string, key: string, value: string): string {
656
+ value = encodeURIComponent(value);
657
+ const re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
658
+ const separator = uri.indexOf("?") !== -1 ? "&" : "?";
659
+ if (uri.match(re)) {
660
+ return uri.replace(re, "$1" + key + "=" + value + "$2");
661
+ }
662
+ return uri + separator + key + "=" + value;
663
+ }
664
+
665
+ /**
666
+ * A more strict encodeURIComponent that escapes `()'!`s
667
+ * Especially useful for creating URLs that are embeddable in markdown
668
+ *
669
+ * Adapted from
670
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
671
+ * This function and the above original available under the
672
+ * CC-BY-SA 2.5 license.
673
+ */
674
+ function strongEncodeURIComponent(str: string): string {
675
+ return (
676
+ encodeURIComponent(str)
677
+ // Note that although RFC3986 reserves "!", RFC5987 does not,
678
+ // so we do not need to escape it
679
+ .replace(/['()!]/g, window.escape) // i.e., %27 %28 %29
680
+ .replace(/\*/g, "%2A")
681
+ );
682
+ }
683
+
684
+ /**
685
+ * TODO(somewhatabstract, JIRA-XXXX):
686
+ * This does not appear to be used within webapp. Could be dead code.
687
+ * Need to check with mobile.
688
+ */
689
+ // There are certain widgets where we don't want to provide the "answered"
690
+ // highlight indicator.
691
+ // The issue with just using the `graded` flag on questions is that showing
692
+ // that a certain widget is ungraded can sometimes reveal the answer to a
693
+ // question ("is this transformation possible? if so, do it")
694
+ // This is kind of a hack to get around this.
695
+ function widgetShouldHighlight(widget: Widget): boolean {
696
+ if (!widget) {
697
+ return false;
698
+ }
699
+ switch (widget.type) {
700
+ /**
701
+ * Highlight bar blacklist
702
+ */
703
+ case "measurer":
704
+ case "protractor":
705
+ return true;
706
+
707
+ default:
708
+ return false;
709
+ }
710
+ }
711
+
712
+ /**
713
+ * If a widget says that it is empty once it is graded.
714
+ * Trying to encapsulate references to the score format.
715
+ */
716
+ function scoreIsEmpty(score: PerseusScore): boolean {
717
+ // HACK(benkomalo): ugh. this isn't great; the Perseus score objects
718
+ // overload the type "invalid" for what should probably be three
719
+ // distinct cases:
720
+ // - truly empty or not fully filled out
721
+ // - invalid or malformed inputs
722
+ // - "almost correct" like inputs where the widget wants to give
723
+ // feedback (e.g. a fraction needs to be reduced, or `pi` should
724
+ // be used instead of 3.14)
725
+ //
726
+ // Unfortunately the coercion happens all over the place, as these
727
+ // Perseus style score objects are created *everywhere* (basically
728
+ // in every widget), so it's hard to change now. We assume that
729
+ // anything with a "message" is not truly empty, and one of the
730
+ // latter two cases for now.
731
+ return (
732
+ score.type === "invalid" &&
733
+ (!score.message || score.message.length === 0)
734
+ );
735
+ }
736
+
737
+ /*
738
+ * The touchHandlers are used to track the current state of the touch
739
+ * event, such as whether or not the user is currently pressed down (either
740
+ * through touch or mouse) on the screen.
741
+ */
742
+ const touchHandlers: TouchHandlers = {
743
+ pointerDown: false,
744
+ currentTouchIdentifier: null,
745
+ };
746
+
747
+ function resetTouchHandlers() {
748
+ Object.assign(touchHandlers, {
749
+ pointerDown: false,
750
+ currentTouchIdentifier: null,
751
+ });
752
+ }
753
+
754
+ /**
755
+ * Extracts the location of a touch or mouse event, allowing you to pass
756
+ * in a "mouseup", "mousedown", or "mousemove" event and receive the
757
+ * correct coordinates. Shouldn't be used with "vmouse" events.
758
+ */
759
+ function extractPointerLocation(event: $FlowFixMe): ?Position {
760
+ let touchOrEvent;
761
+
762
+ if (touchHandlers.pointerDown) {
763
+ // Look for the touch matching the one we're tracking; ignore others
764
+ if (touchHandlers.currentTouchIdentifier != null) {
765
+ const len = event.changedTouches ? event.changedTouches.length : 0;
766
+ for (let i = 0; i < len; i++) {
767
+ if (
768
+ event.changedTouches[i].identifier ===
769
+ touchHandlers.currentTouchIdentifier
770
+ ) {
771
+ touchOrEvent = event.changedTouches[i];
772
+ }
773
+ }
774
+ } else {
775
+ touchOrEvent = event;
776
+ }
777
+
778
+ const isEndish =
779
+ event.type === "touchend" || event.type === "touchcancel";
780
+ if (touchOrEvent && isEndish) {
781
+ touchHandlers.pointerDown = false;
782
+ touchHandlers.currentTouchIdentifier = null;
783
+ }
784
+ } else {
785
+ // touchstart or mousedown
786
+ touchHandlers.pointerDown = true;
787
+ if (event.changedTouches) {
788
+ touchOrEvent = event.changedTouches[0];
789
+ touchHandlers.currentTouchIdentifier = touchOrEvent.identifier;
790
+ } else {
791
+ touchOrEvent = event;
792
+ }
793
+ }
794
+
795
+ if (touchOrEvent) {
796
+ return {
797
+ left: touchOrEvent.pageX,
798
+ top: touchOrEvent.pageY,
799
+ };
800
+ }
801
+ }
802
+
803
+ // Older browsers don't support passive events and so we need to feature-
804
+ // detect them and do event subscription differently for them.
805
+ // See: orderer.jsx
806
+ const supportsPassiveEvents: () => boolean = () => {
807
+ // Test via a getter in the options object to see if the passive
808
+ // property is accessed
809
+ try {
810
+ const opts = Object.defineProperty({}, "passive", {
811
+ get: function () {
812
+ supportsPassive = true;
813
+ },
814
+ });
815
+ window.addEventListener("testPassive", null, opts);
816
+ window.removeEventListener("testPassive", null, opts);
817
+ } catch (e) {
818
+ // Intentionally left empty!
819
+ }
820
+
821
+ return supportsPassive;
822
+ };
823
+
824
+ /**
825
+ * Pass this function as the touchstart for an element to
826
+ * avoid sending the touch to the mobile scratchpad
827
+ */
828
+ function captureScratchpadTouchStart(e: TouchEvent) {
829
+ e.stopPropagation();
830
+ }
831
+
832
+ function getImageSize(url: string, callback: (number, number) => void): void {
833
+ const img = new Image();
834
+ img.onload = function () {
835
+ // IE 11 seems to have problems calculating the heights of svgs
836
+ // if they're not in the DOM. To solve this, we add the element to
837
+ // the dom, wait for a rerender, and use `.clientWidth` and
838
+ // `.clientHeight`. I think we could also solve the problem by
839
+ // adding the image to the document before setting the src, but then
840
+ // the experience would be worse for other browsers.
841
+ // TODO(scottgrant): This is correctly calculating the width of SVG
842
+ // images in browsers, but incorrectly saving the width of what may
843
+ // be a smaller viewport when using the editor, and reusing that
844
+ // width in a full-screen article.
845
+ if (img.width === 0 && img.height === 0) {
846
+ document.body?.appendChild(img);
847
+ // TODO(scottgrant): Remove this use of _.defer.
848
+ _.defer(function () {
849
+ callback(img.clientWidth, img.clientHeight);
850
+ document.body?.removeChild(img);
851
+ });
852
+ } else {
853
+ callback(img.width, img.height);
854
+ }
855
+ };
856
+
857
+ img.src = getRealImageUrl(url);
858
+ }
859
+
860
+ // Sometimes other components want to download the actual image e.g. to
861
+ // determine its size. Here, we transform an .svg-labels url into the
862
+ // correct image url, and leave normal image urls alone
863
+ function getRealImageUrl(url: string): string {
864
+ if (isLabeledSVG(url)) {
865
+ return getSvgUrl(url);
866
+ }
867
+ return url;
868
+ }
869
+
870
+ function isLabeledSVG(url: string): boolean {
871
+ return svgLabelsRegex.test(url) || svgLocalLabelsRegex.test(url);
872
+ }
873
+
874
+ // For each svg+labels, there are two urls we need to download from. This gets
875
+ // the base url without the suffix, and `getSvgUrl` and `getDataUrl` apply
876
+ // appropriate suffixes to get the image and other data
877
+ function getBaseUrl(url: string): string {
878
+ return url
879
+ .replace(svgLabelsRegex, "https:")
880
+ .replace(svgLocalLabelsRegex, "file:");
881
+ }
882
+
883
+ function getSvgUrl(url: string): string {
884
+ return getBaseUrl(url) + ".svg";
885
+ }
886
+
887
+ function getDataUrl(url: string): string {
888
+ return getBaseUrl(url) + "-data.json";
889
+ }
890
+
891
+ /**
892
+ * Gets the word right before where the textarea cursor is
893
+ *
894
+ * @param {Element} textarea - The textarea DOM element
895
+ * @return {JSON} - An object with the word and its starting and ending positions in the textarea
896
+ */
897
+ function getWordBeforeCursor(textarea: HTMLTextAreaElement): WordAndPosition {
898
+ const text = textarea.value;
899
+
900
+ const endPos = textarea.selectionStart - 1;
901
+ const startPos =
902
+ Math.max(
903
+ text.lastIndexOf("\n", endPos),
904
+ text.lastIndexOf(" ", endPos),
905
+ ) + 1;
906
+
907
+ return {
908
+ string: text.substring(startPos, endPos + 1),
909
+ pos: {
910
+ start: startPos,
911
+ end: endPos,
912
+ },
913
+ };
914
+ }
915
+
916
+ /**
917
+ * Moves the textarea cursor at the specified position
918
+ *
919
+ * @param {Element} textarea - The textarea DOM element
920
+ * @param {int} pos - The position where the cursor will be moved
921
+ */
922
+ function moveCursor(textarea: HTMLTextAreaElement, pos: number): void {
923
+ textarea.selectionStart = pos;
924
+ textarea.selectionEnd = pos;
925
+ }
926
+
927
+ const textarea = {
928
+ getWordBeforeCursor,
929
+ moveCursor,
930
+ };
931
+
932
+ /**
933
+ * Many of our labels are automatically converted into math mode without
934
+ * the dollar signs. Unfortunately, this makes them untranslatable! This
935
+ * helper function removes the math mode symbols from a string if we want
936
+ * to translate it but don't need the extra dollar signs.
937
+ */
938
+ const unescapeMathMode: (label: string) => string = (label) =>
939
+ label.startsWith("$") && label.endsWith("$") ? label.slice(1, -1) : label;
940
+
941
+ const random: RNG = seededRNG(new Date().getTime() & 0xffffffff);
942
+
943
+ const Util = {
944
+ inputPathsEqual,
945
+ nestedMap,
946
+ rWidgetRule,
947
+ rTypeFromWidgetId,
948
+ rWidgetParts,
949
+ snowman,
950
+ noScore,
951
+ seededRNG,
952
+ shuffle,
953
+ split,
954
+ combineScores,
955
+ keScoreFromPerseusScore,
956
+ firstNumericalParse,
957
+ stringArrayOfSize,
958
+ gridDimensionConfig,
959
+ getGridStep,
960
+ snapStepFromGridStep,
961
+ scaleFromExtent,
962
+ tickStepFromExtent,
963
+ gridStepFromTickStep,
964
+ tickStepFromNumTicks,
965
+ constrainedTickStepsFromTickSteps,
966
+ DeprecationMixin,
967
+ eq,
968
+ deepEq,
969
+ parseQueryString,
970
+ updateQueryString,
971
+ strongEncodeURIComponent,
972
+ widgetShouldHighlight,
973
+ scoreIsEmpty,
974
+ touchHandlers,
975
+ resetTouchHandlers,
976
+ extractPointerLocation,
977
+ supportsPassiveEvents,
978
+ captureScratchpadTouchStart,
979
+ getImageSize,
980
+ getRealImageUrl,
981
+ isLabeledSVG,
982
+ getBaseUrl,
983
+ getSvgUrl,
984
+ getDataUrl,
985
+ textarea,
986
+ unescapeMathMode,
987
+ random,
988
+ };
989
+
990
+ export default Util;