react-native-marcus 0.1.0 → 0.2.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 (294) hide show
  1. package/README.md +220 -63
  2. package/RNMarcus.podspec +6 -0
  3. package/android/src/main/java/app/getbullet/marcus/MarcusPackage.kt +2 -1
  4. package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorView.kt +168 -0
  5. package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorViewManager.kt +38 -0
  6. package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorViewManagerSpec.kt +19 -0
  7. package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorView.kt +23 -1
  8. package/android/src/main/java/app/getbullet/marcus/MarkdownBackgroundPainter.kt +147 -0
  9. package/android/src/main/java/app/getbullet/marcus/MarkdownBackgroundPass.kt +19 -0
  10. package/android/src/main/java/app/getbullet/marcus/MarkdownCopy.kt +87 -0
  11. package/android/src/main/java/app/getbullet/marcus/MarkdownFormatter.kt +652 -113
  12. package/android/src/main/java/app/getbullet/marcus/MarkdownRanges.kt +47 -0
  13. package/android/src/main/java/app/getbullet/marcus/MarkdownStyle.kt +140 -61
  14. package/android/src/main/java/app/getbullet/marcus/MarkdownUtils.kt +37 -7
  15. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownAbsoluteLineHeightSpan.kt +48 -0
  16. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundSpan.kt +102 -48
  17. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBlockIndentSpan.kt +22 -4
  18. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBulletSpan.kt +78 -0
  19. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownCodeBlockSpan.kt +177 -0
  20. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontStyleSpan.kt +32 -0
  21. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontWeightSpan.kt +34 -0
  22. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownGapSpan.kt +24 -11
  23. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownLetterSpacingSpan.kt +32 -0
  24. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownTextShadowSpan.kt +32 -0
  25. package/android/src/main/new_arch/CMakeLists.txt +17 -0
  26. package/android/src/main/new_arch/RNMarcusSpec.h +1 -0
  27. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorShadowNode.cpp +235 -0
  28. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorShadowNode.h +62 -0
  29. package/android/src/reactNativeVersionPatch/CustomMountingManager/81/app/getbullet/marcus/CustomFabricUIManager.java +22 -3
  30. package/android/src/reactNativeVersionPatch/CustomMountingManager/latest/app/getbullet/marcus/CustomFabricUIManager.java +22 -3
  31. package/android/src/test/java/app/getbullet/marcus/MarkdownCopyTest.kt +69 -0
  32. package/android/src/test/java/app/getbullet/marcus/MarkdownFormatterTest.kt +170 -25
  33. package/android/src/test/java/app/getbullet/marcus/RenderModel.kt +22 -5
  34. package/android/src/test/java/app/getbullet/marcus/spans/MarkdownCodeBlockSpanTest.kt +91 -0
  35. package/android/src/test/resources/render-model.txt +221 -16
  36. package/apple/MarcusSwizzle.h +25 -0
  37. package/apple/MarcusTextDecoratorComponentView.h +22 -0
  38. package/apple/MarcusTextDecoratorComponentView.mm +72 -0
  39. package/apple/MarcusTextDecoratorShadowNode.h +62 -0
  40. package/apple/MarcusTextDecoratorShadowNode.mm +192 -0
  41. package/apple/MarcusTextDecoratorViewManager.h +4 -0
  42. package/apple/MarcusTextDecoratorViewManager.mm +11 -0
  43. package/apple/MarcusTextInputDecoratorComponentView.mm +1 -1
  44. package/apple/MarcusTextInputDecoratorShadowNode.mm +1 -1
  45. package/apple/MarcusTextLayoutManager.h +40 -0
  46. package/apple/MarcusTextLayoutManager.mm +173 -0
  47. package/apple/MarkdownAttributes.h +23 -2
  48. package/apple/MarkdownAttributes.m +6 -0
  49. package/apple/MarkdownCopy.swift +63 -0
  50. package/apple/MarkdownDecorator.swift +2 -2
  51. package/apple/MarkdownFormatter+React.swift +23 -0
  52. package/apple/MarkdownFormatter.swift +869 -148
  53. package/apple/MarkdownParagraphLayoutManager.swift +455 -0
  54. package/apple/MarkdownSwiftInterop.h +0 -1
  55. package/apple/MarkdownTextBackground.swift +59 -1
  56. package/apple/MarkdownTextFieldObserver.swift +2 -2
  57. package/apple/MarkdownTextLayoutFragment.swift +159 -33
  58. package/apple/MarkdownTextLayoutManagerDelegate.swift +67 -3
  59. package/apple/MarkdownTextStorageDelegate.swift +2 -2
  60. package/apple/MarkdownTextViewObserver.swift +2 -2
  61. package/apple/MarkdownUtils.swift +38 -0
  62. package/apple/MarkdownWritingDirection.swift +132 -0
  63. package/apple/RCTMarcusStyle+Codegen.h +115 -9
  64. package/apple/RCTMarcusStyle.h +128 -31
  65. package/apple/RCTMarcusStyle.m +48 -0
  66. package/apple/RCTParagraphComponentView+Marcus.h +17 -0
  67. package/apple/RCTParagraphComponentView+Marcus.mm +38 -0
  68. package/apple/RCTTextInputComponentView+Marcus.mm +1 -25
  69. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorState.h +37 -0
  70. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorViewComponentDescriptor.h +17 -0
  71. package/lib/commonjs/MarcusTextDecoratorViewNativeComponent.ts +199 -0
  72. package/lib/commonjs/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
  73. package/lib/commonjs/MarkdownText.js +330 -0
  74. package/lib/commonjs/MarkdownText.js.map +1 -0
  75. package/lib/commonjs/MarkdownText.web.js +529 -0
  76. package/lib/commonjs/MarkdownText.web.js.map +1 -0
  77. package/lib/commonjs/MarkdownTextInput.js +22 -27
  78. package/lib/commonjs/MarkdownTextInput.js.map +1 -1
  79. package/lib/commonjs/MarkdownTextInput.web.js +12 -7
  80. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
  81. package/lib/commonjs/atomUtils.js +112 -0
  82. package/lib/commonjs/atomUtils.js.map +1 -0
  83. package/lib/commonjs/childrenUtils.js +36 -0
  84. package/lib/commonjs/childrenUtils.js.map +1 -0
  85. package/lib/commonjs/index.js +8 -7
  86. package/lib/commonjs/index.js.map +1 -1
  87. package/lib/commonjs/math/environment.js +69 -0
  88. package/lib/commonjs/math/environment.js.map +1 -0
  89. package/lib/commonjs/math/index.js +61 -0
  90. package/lib/commonjs/math/index.js.map +1 -0
  91. package/lib/commonjs/math/mathjax.js +548 -0
  92. package/lib/commonjs/math/mathjax.js.map +1 -0
  93. package/lib/commonjs/metro.js +172 -0
  94. package/lib/commonjs/metro.js.map +1 -0
  95. package/lib/commonjs/parser/index.d.ts +3 -0
  96. package/lib/commonjs/parser/index.js +450 -42
  97. package/lib/commonjs/parser/index.js.map +4 -4
  98. package/lib/commonjs/stripUtils.js +743 -0
  99. package/lib/commonjs/stripUtils.js.map +1 -0
  100. package/lib/commonjs/styleUtils.js +194 -23
  101. package/lib/commonjs/styleUtils.js.map +1 -1
  102. package/lib/commonjs/web/MarkdownElements.css +67 -0
  103. package/lib/commonjs/web/MarkdownTextInput.css +9 -7
  104. package/lib/commonjs/web/utils/blockLayout.js +95 -7
  105. package/lib/commonjs/web/utils/blockLayout.js.map +1 -1
  106. package/lib/commonjs/web/utils/blockUtils.js +327 -54
  107. package/lib/commonjs/web/utils/blockUtils.js.map +1 -1
  108. package/lib/commonjs/web/utils/elementUtils.js +80 -0
  109. package/lib/commonjs/web/utils/elementUtils.js.map +1 -0
  110. package/lib/commonjs/web/utils/parserUtils.js +84 -28
  111. package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
  112. package/lib/commonjs/web/utils/treeUtils.js +6 -0
  113. package/lib/commonjs/web/utils/treeUtils.js.map +1 -1
  114. package/lib/commonjs/web/utils/webStyleUtils.js +25 -5
  115. package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -1
  116. package/lib/module/MarcusTextDecoratorViewNativeComponent.ts +199 -0
  117. package/lib/module/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
  118. package/lib/module/MarkdownText.js +325 -0
  119. package/lib/module/MarkdownText.js.map +1 -0
  120. package/lib/module/MarkdownText.web.js +524 -0
  121. package/lib/module/MarkdownText.web.js.map +1 -0
  122. package/lib/module/MarkdownTextInput.js +24 -29
  123. package/lib/module/MarkdownTextInput.js.map +1 -1
  124. package/lib/module/MarkdownTextInput.web.js +12 -7
  125. package/lib/module/MarkdownTextInput.web.js.map +1 -1
  126. package/lib/module/atomUtils.js +106 -0
  127. package/lib/module/atomUtils.js.map +1 -0
  128. package/lib/module/childrenUtils.js +33 -0
  129. package/lib/module/childrenUtils.js.map +1 -0
  130. package/lib/module/index.js +1 -1
  131. package/lib/module/index.js.map +1 -1
  132. package/lib/module/math/environment.js +67 -0
  133. package/lib/module/math/environment.js.map +1 -0
  134. package/lib/module/math/index.js +56 -0
  135. package/lib/module/math/index.js.map +1 -0
  136. package/lib/module/math/mathjax.js +541 -0
  137. package/lib/module/math/mathjax.js.map +1 -0
  138. package/lib/module/metro.js +169 -0
  139. package/lib/module/metro.js.map +1 -0
  140. package/lib/module/parser/index.d.ts +3 -0
  141. package/lib/module/parser/index.js +450 -42
  142. package/lib/module/parser/index.js.map +4 -4
  143. package/lib/module/stripUtils.js +739 -0
  144. package/lib/module/stripUtils.js.map +1 -0
  145. package/lib/module/styleUtils.js +196 -25
  146. package/lib/module/styleUtils.js.map +1 -1
  147. package/lib/module/web/MarkdownElements.css +67 -0
  148. package/lib/module/web/MarkdownTextInput.css +9 -7
  149. package/lib/module/web/utils/blockLayout.js +94 -8
  150. package/lib/module/web/utils/blockLayout.js.map +1 -1
  151. package/lib/module/web/utils/blockUtils.js +328 -56
  152. package/lib/module/web/utils/blockUtils.js.map +1 -1
  153. package/lib/module/web/utils/elementUtils.js +75 -0
  154. package/lib/module/web/utils/elementUtils.js.map +1 -0
  155. package/lib/module/web/utils/parserUtils.js +86 -30
  156. package/lib/module/web/utils/parserUtils.js.map +1 -1
  157. package/lib/module/web/utils/treeUtils.js +6 -0
  158. package/lib/module/web/utils/treeUtils.js.map +1 -1
  159. package/lib/module/web/utils/webStyleUtils.js +25 -5
  160. package/lib/module/web/utils/webStyleUtils.js.map +1 -1
  161. package/lib/typescript/commonjs/e2e/links.test.d.ts +2 -0
  162. package/lib/typescript/commonjs/e2e/links.test.d.ts.map +1 -0
  163. package/lib/typescript/commonjs/src/MarcusTextDecoratorViewNativeComponent.d.ts +93 -0
  164. package/lib/typescript/commonjs/src/MarcusTextDecoratorViewNativeComponent.d.ts.map +1 -0
  165. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +64 -51
  166. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/src/MarkdownText.d.ts +127 -0
  168. package/lib/typescript/commonjs/src/MarkdownText.d.ts.map +1 -0
  169. package/lib/typescript/commonjs/src/MarkdownText.web.d.ts +126 -0
  170. package/lib/typescript/commonjs/src/MarkdownText.web.d.ts.map +1 -0
  171. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts +23 -1
  172. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts +26 -4
  174. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts.map +1 -1
  175. package/lib/typescript/commonjs/src/atomUtils.d.ts +58 -0
  176. package/lib/typescript/commonjs/src/atomUtils.d.ts.map +1 -0
  177. package/lib/typescript/commonjs/src/childrenUtils.d.ts +12 -0
  178. package/lib/typescript/commonjs/src/childrenUtils.d.ts.map +1 -0
  179. package/lib/typescript/commonjs/src/commonTypes.d.ts +39 -3
  180. package/lib/typescript/commonjs/src/commonTypes.d.ts.map +1 -1
  181. package/lib/typescript/commonjs/src/index.d.ts +2 -1
  182. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  183. package/lib/typescript/commonjs/src/math/environment.d.ts +7 -0
  184. package/lib/typescript/commonjs/src/math/environment.d.ts.map +1 -0
  185. package/lib/typescript/commonjs/src/math/index.d.ts +64 -0
  186. package/lib/typescript/commonjs/src/math/index.d.ts.map +1 -0
  187. package/lib/typescript/commonjs/src/math/mathjax.d.ts +63 -0
  188. package/lib/typescript/commonjs/src/math/mathjax.d.ts.map +1 -0
  189. package/lib/typescript/commonjs/src/metro.d.ts +59 -0
  190. package/lib/typescript/commonjs/src/metro.d.ts.map +1 -0
  191. package/lib/typescript/commonjs/src/parser/index.d.ts +3 -3
  192. package/lib/typescript/commonjs/src/parser/index.d.ts.map +1 -1
  193. package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts.map +1 -1
  194. package/lib/typescript/commonjs/src/rangeUtils.d.ts +1 -1
  195. package/lib/typescript/commonjs/src/stripUtils.d.ts +108 -0
  196. package/lib/typescript/commonjs/src/stripUtils.d.ts.map +1 -0
  197. package/lib/typescript/commonjs/src/styleUtils.d.ts +62 -4
  198. package/lib/typescript/commonjs/src/styleUtils.d.ts.map +1 -1
  199. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts +32 -4
  200. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts.map +1 -1
  201. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts +20 -8
  202. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts.map +1 -1
  203. package/lib/typescript/commonjs/src/web/utils/elementUtils.d.ts +37 -0
  204. package/lib/typescript/commonjs/src/web/utils/elementUtils.d.ts.map +1 -0
  205. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts +5 -5
  206. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts.map +1 -1
  207. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts +6 -1
  208. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts.map +1 -1
  209. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts +2 -2
  210. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts.map +1 -1
  211. package/lib/typescript/module/e2e/links.test.d.ts +2 -0
  212. package/lib/typescript/module/e2e/links.test.d.ts.map +1 -0
  213. package/lib/typescript/module/src/MarcusTextDecoratorViewNativeComponent.d.ts +93 -0
  214. package/lib/typescript/module/src/MarcusTextDecoratorViewNativeComponent.d.ts.map +1 -0
  215. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +64 -51
  216. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
  217. package/lib/typescript/module/src/MarkdownText.d.ts +127 -0
  218. package/lib/typescript/module/src/MarkdownText.d.ts.map +1 -0
  219. package/lib/typescript/module/src/MarkdownText.web.d.ts +126 -0
  220. package/lib/typescript/module/src/MarkdownText.web.d.ts.map +1 -0
  221. package/lib/typescript/module/src/MarkdownTextInput.d.ts +23 -1
  222. package/lib/typescript/module/src/MarkdownTextInput.d.ts.map +1 -1
  223. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts +26 -4
  224. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts.map +1 -1
  225. package/lib/typescript/module/src/atomUtils.d.ts +58 -0
  226. package/lib/typescript/module/src/atomUtils.d.ts.map +1 -0
  227. package/lib/typescript/module/src/childrenUtils.d.ts +12 -0
  228. package/lib/typescript/module/src/childrenUtils.d.ts.map +1 -0
  229. package/lib/typescript/module/src/commonTypes.d.ts +39 -3
  230. package/lib/typescript/module/src/commonTypes.d.ts.map +1 -1
  231. package/lib/typescript/module/src/index.d.ts +2 -1
  232. package/lib/typescript/module/src/index.d.ts.map +1 -1
  233. package/lib/typescript/module/src/math/environment.d.ts +7 -0
  234. package/lib/typescript/module/src/math/environment.d.ts.map +1 -0
  235. package/lib/typescript/module/src/math/index.d.ts +64 -0
  236. package/lib/typescript/module/src/math/index.d.ts.map +1 -0
  237. package/lib/typescript/module/src/math/mathjax.d.ts +63 -0
  238. package/lib/typescript/module/src/math/mathjax.d.ts.map +1 -0
  239. package/lib/typescript/module/src/metro.d.ts +59 -0
  240. package/lib/typescript/module/src/metro.d.ts.map +1 -0
  241. package/lib/typescript/module/src/parser/index.d.ts +3 -3
  242. package/lib/typescript/module/src/parser/index.d.ts.map +1 -1
  243. package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts.map +1 -1
  244. package/lib/typescript/module/src/rangeUtils.d.ts +1 -1
  245. package/lib/typescript/module/src/stripUtils.d.ts +108 -0
  246. package/lib/typescript/module/src/stripUtils.d.ts.map +1 -0
  247. package/lib/typescript/module/src/styleUtils.d.ts +62 -4
  248. package/lib/typescript/module/src/styleUtils.d.ts.map +1 -1
  249. package/lib/typescript/module/src/web/utils/blockLayout.d.ts +32 -4
  250. package/lib/typescript/module/src/web/utils/blockLayout.d.ts.map +1 -1
  251. package/lib/typescript/module/src/web/utils/blockUtils.d.ts +20 -8
  252. package/lib/typescript/module/src/web/utils/blockUtils.d.ts.map +1 -1
  253. package/lib/typescript/module/src/web/utils/elementUtils.d.ts +37 -0
  254. package/lib/typescript/module/src/web/utils/elementUtils.d.ts.map +1 -0
  255. package/lib/typescript/module/src/web/utils/parserUtils.d.ts +5 -5
  256. package/lib/typescript/module/src/web/utils/parserUtils.d.ts.map +1 -1
  257. package/lib/typescript/module/src/web/utils/treeUtils.d.ts +6 -1
  258. package/lib/typescript/module/src/web/utils/treeUtils.d.ts.map +1 -1
  259. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts +2 -2
  260. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts.map +1 -1
  261. package/package.json +29 -2
  262. package/react-native.config.js +8 -1
  263. package/src/MarcusTextDecoratorViewNativeComponent.ts +199 -0
  264. package/src/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
  265. package/src/MarkdownText.tsx +472 -0
  266. package/src/MarkdownText.web.tsx +668 -0
  267. package/src/MarkdownTextInput.tsx +42 -38
  268. package/src/MarkdownTextInput.web.tsx +36 -13
  269. package/src/atomUtils.ts +112 -0
  270. package/src/childrenUtils.ts +39 -0
  271. package/src/commonTypes.ts +69 -9
  272. package/src/index.tsx +2 -1
  273. package/src/math/environment.ts +68 -0
  274. package/src/math/index.ts +78 -0
  275. package/src/math/mathjax.ts +604 -0
  276. package/src/metro.ts +238 -0
  277. package/src/parser/index.ts +175 -10
  278. package/src/parser/micromark-extension-mentions.ts +104 -11
  279. package/src/stripUtils.ts +919 -0
  280. package/src/styleUtils.ts +290 -26
  281. package/src/web/MarkdownElements.css +67 -0
  282. package/src/web/MarkdownTextInput.css +9 -7
  283. package/src/web/utils/blockLayout.ts +124 -14
  284. package/src/web/utils/blockUtils.ts +365 -59
  285. package/src/web/utils/elementUtils.ts +87 -0
  286. package/src/web/utils/parserUtils.ts +119 -31
  287. package/src/web/utils/treeUtils.ts +6 -1
  288. package/src/web/utils/webStyleUtils.ts +32 -8
  289. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundColorSpan.kt +0 -6
  290. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBoldSpan.kt +0 -6
  291. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownItalicSpan.kt +0 -6
  292. package/apple/RCTMarcusStyle+Codegen.mm +0 -64
  293. package/apple/RCTMarcusUtils.h +0 -26
  294. package/apple/RCTMarcusUtils.mm +0 -26
@@ -0,0 +1,604 @@
1
+ /**
2
+ * TeX -> SVG with MathJax 4, wired up the way React Native needs.
3
+ *
4
+ * Nothing imports this module. `react-native-marcus/metro`'s `withMath()`
5
+ * resolves `./index.ts` -- the stub beside it -- to this file, which is how an
6
+ * application opts into a dependency this size. See `./index.ts`.
7
+ *
8
+ * Three things here are not the defaults:
9
+ *
10
+ * - **`liteAdaptor`**, so nothing wants a DOM. It runs the same in Hermes, in a
11
+ * browser and in Node, which is what makes one renderer serve all three
12
+ * platforms and draw the same glyphs on each.
13
+ * - **`fontCache: "none"`**, so each expression is a standalone SVG with its
14
+ * glyph outlines inline. `"local"`, the default, emits `<use>` against a
15
+ * `<defs>` block, which is smaller on a page full of maths and useless when
16
+ * every expression is a separate attachment in someone else's view tree.
17
+ * - **`linebreaks: { inline: false }`**. v4 breaks long inline maths into
18
+ * several `<svg>` fragments separated by `<mjx-break>`; one attachment is one
19
+ * SVG, and left on, the metrics read off the first fragment are silently
20
+ * wrong.
21
+ *
22
+ * Every specifier below names `mjs/` outright rather than going through
23
+ * MathJax's `js/*` export, and that is not cosmetic. That export maps to `mjs`
24
+ * for `import` and to `cjs` for `require`, so a module reached both ways --
25
+ * which is exactly what this file does, importing the machinery and requiring
26
+ * the font ranges -- is loaded twice, and the two copies each hold half of what
27
+ * is needed. The symptom is every `\mathbb`, `\mathcal`, `\mathfrak`,
28
+ * `\mathsf` and `\mathtt` coming out blank while everything else draws
29
+ * perfectly. `mjs/` goes through the package's `"./*": "./*"` fallback, which
30
+ * has no conditions to disagree about.
31
+ */
32
+ import "./environment"
33
+
34
+ import { mathjax } from "@mathjax/src/mjs/mathjax.js"
35
+ import { TeX } from "@mathjax/src/mjs/input/tex.js"
36
+ import { SVG } from "@mathjax/src/mjs/output/svg.js"
37
+ import { liteAdaptor } from "@mathjax/src/mjs/adaptors/liteAdaptor.js"
38
+ import { RegisterHTMLHandler } from "@mathjax/src/mjs/handlers/html.js"
39
+ import { MathJaxNewcmFont } from "@mathjax/mathjax-newcm-font/mjs/svg.js"
40
+ import { SerializedMmlVisitor } from "@mathjax/src/mjs/core/MmlTree/SerializedMmlVisitor.js"
41
+ import { STATE } from "@mathjax/src/mjs/core/MathItem.js"
42
+ import type { MmlNode } from "@mathjax/src/mjs/core/MmlTree/MmlNode.js"
43
+
44
+ // Reached through the three modules that hold what is wanted rather than
45
+ // through SRE's own entry point, which drags in a command line parser, sets
46
+ // itself up ten milliseconds after import with a loader that cannot work here,
47
+ // and is written with an `export * as` that neither Metro's Babel preset nor
48
+ // Jest's will parse.
49
+ import { setupEngine } from "speech-rule-engine/js/api/control.js"
50
+ import { toSpeech } from "speech-rule-engine/js/api/string.js"
51
+ import { EnginePromise } from "speech-rule-engine/js/common/engine.js"
52
+
53
+ import "@mathjax/src/mjs/input/tex/base/BaseConfiguration.js"
54
+ import "@mathjax/src/mjs/input/tex/ams/AmsConfiguration.js"
55
+ import "@mathjax/src/mjs/input/tex/newcommand/NewcommandConfiguration.js"
56
+ import "@mathjax/src/mjs/input/tex/boldsymbol/BoldsymbolConfiguration.js"
57
+
58
+ import type { RenderedMath } from "./index"
59
+
60
+ /**
61
+ * Every glyph range, each behind a literal `require` so a bundler can see it.
62
+ *
63
+ * MathJax 4 splits its font data into forty files it fetches on demand. The
64
+ * fetching has to be replaced -- no bundler can follow it -- but it cannot be
65
+ * skipped: `getChar` resolves a missing glyph *through* this loader, and if
66
+ * nothing comes of the call it puts back the placeholder it just took out, so
67
+ * it and `loadDynamicFileSync` hand that placeholder to each other until the
68
+ * stack overflows.
69
+ *
70
+ * So: a thunk per range, a synchronous loader, and the whole font in the
71
+ * bundle -- a few megabytes, and drawing every expression rather than a chosen
72
+ * subset is what they buy. `loadRanges` below runs them all before the first
73
+ * expression is drawn; MathJax still asks for each one the first time it wants
74
+ * a glyph from it, and finds the module already run.
75
+ */
76
+ const RANGES: Record<string, () => unknown> = {
77
+ "PUA": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/PUA.js"),
78
+ "accents": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/accents.js"),
79
+ "accents-b-i": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/accents-b-i.js"),
80
+ "arabic": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/arabic.js"),
81
+ "arrows": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/arrows.js"),
82
+ "braille": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/braille.js"),
83
+ "braille-d": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/braille-d.js"),
84
+ "calligraphic": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/calligraphic.js"),
85
+ "cherokee": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/cherokee.js"),
86
+ "cyrillic": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/cyrillic.js"),
87
+ "cyrillic-ss": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/cyrillic-ss.js"),
88
+ "devanagari": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/devanagari.js"),
89
+ "double-struck": () =>
90
+ require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/double-struck.js"),
91
+ "fraktur": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/fraktur.js"),
92
+ "greek": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/greek.js"),
93
+ "greek-ss": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/greek-ss.js"),
94
+ "hebrew": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/hebrew.js"),
95
+ "latin": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/latin.js"),
96
+ "latin-b": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/latin-b.js"),
97
+ "latin-bi": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/latin-bi.js"),
98
+ "latin-i": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/latin-i.js"),
99
+ "marrows": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/marrows.js"),
100
+ "math": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/math.js"),
101
+ "monospace": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/monospace.js"),
102
+ "monospace-ex": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/monospace-ex.js"),
103
+ "monospace-l": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/monospace-l.js"),
104
+ "mshapes": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/mshapes.js"),
105
+ "phonetics": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/phonetics.js"),
106
+ "phonetics-ss": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/phonetics-ss.js"),
107
+ "sans-serif": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/sans-serif.js"),
108
+ "sans-serif-b": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/sans-serif-b.js"),
109
+ "sans-serif-bi": () =>
110
+ require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/sans-serif-bi.js"),
111
+ "sans-serif-ex": () =>
112
+ require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/sans-serif-ex.js"),
113
+ "sans-serif-i": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/sans-serif-i.js"),
114
+ "sans-serif-r": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/sans-serif-r.js"),
115
+ "script": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/script.js"),
116
+ "shapes": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/shapes.js"),
117
+ "symbols": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/symbols.js"),
118
+ "symbols-b-i": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/symbols-b-i.js"),
119
+ "variants": () => require("@mathjax/mathjax-newcm-font/mjs/svg/dynamic/variants.js"),
120
+ }
121
+
122
+ ;(mathjax as unknown as { asyncLoad: unknown }).asyncLoad = (name: string) => {
123
+ const range = String(name).replace(/^.*\//, "").replace(/\.js$/, "")
124
+ const load = RANGES[range]
125
+
126
+ if (!load) {
127
+ throw new Error(`[react-native-marcus] font range not bundled: ${range}`)
128
+ }
129
+
130
+ return load()
131
+ }
132
+ // Synchronous, so `getChar` resolves in place rather than throwing a retry only
133
+ // a promise-based caller could catch.
134
+ ;(mathjax as unknown as { asyncIsSynchronous: boolean }).asyncIsSynchronous = true
135
+
136
+ const adaptor = liteAdaptor()
137
+ RegisterHTMLHandler(adaptor)
138
+
139
+ const font = new MathJaxNewcmFont()
140
+
141
+ const doc = mathjax.document("", {
142
+ InputJax: new TeX({ packages: ["base", "ams", "newcommand", "boldsymbol"] }),
143
+ OutputJax: new SVG({
144
+ fontCache: "none",
145
+ font,
146
+ linebreaks: { inline: false },
147
+ displayOverflow: "overflow",
148
+ }),
149
+ })
150
+
151
+ /**
152
+ * Whether every range module has been run yet.
153
+ *
154
+ * MathJax pulls a range in the moment it first wants a glyph from it, which
155
+ * means it runs a font module from the middle of laying an expression out --
156
+ * and Hermes' stack does not have the room. It overflows, the expression
157
+ * fails, and only a second attempt succeeds, by which time the module has run.
158
+ * Twelve expressions across a dozen ranges cost a minute of throwing and
159
+ * retrying on an emulator; the same page draws in one pass once the modules
160
+ * have been run from here, where the stack is empty.
161
+ *
162
+ * This is deliberately *not* `font.loadDynamicFilesSync()`, which is MathJax's
163
+ * own way to do this and which quietly loses `\mathbb`, `\mathfrak`,
164
+ * `\mathsf` and `\mathtt`: it marks every range as resolved on the way past,
165
+ * so the four whose characters it does not manage to install can never be
166
+ * asked for again. Running the modules and leaving the bookkeeping alone keeps
167
+ * the normal path intact -- MathJax still resolves each range the first time it
168
+ * wants one, it just finds the module already run and does it in no time.
169
+ *
170
+ * Deferred to the first expression rather than done on import, so that an
171
+ * application which loads this module and draws nothing pays nothing.
172
+ */
173
+ let loaded = false
174
+
175
+ function loadRanges() {
176
+ if (loaded) {
177
+ return
178
+ }
179
+
180
+ // Set first: one range that will not load must not cost this again.
181
+ loaded = true
182
+
183
+ for (const load of Object.values(RANGES)) {
184
+ try {
185
+ load()
186
+ } catch {
187
+ // Left to the on-demand path, which warns through MathJax's own loader.
188
+ }
189
+ }
190
+ }
191
+
192
+ /** The opening tag, which carries the metrics and the sizing. */
193
+ const ROOT = /<svg[^>]*>/
194
+ /** The root's own sizing, which has to go: see `attemptRender`. */
195
+ const SIZING = /\s(?:width|height)="[^"]*"|\sstyle="[^"]*"/g
196
+ /** The `<svg>` element on its own, without the `<mjx-container>` around it. */
197
+ const ELEMENT = /<svg[\s\S]*<\/svg>/
198
+ const EX = /width="([-\d.]+)ex"[^>]*height="([-\d.]+)ex"/
199
+ const VA = /vertical-align:\s*([-\d.]+)ex/
200
+ /** What MathJax paints in, which a view mounted in a `Text` inherits nothing of. */
201
+ const CURRENT_COLOR = /currentColor/g
202
+ /**
203
+ * MathJax's own annotations -- `data-mml-node`, `data-latex`, `data-c` and the
204
+ * rest. They are for its context menu and its accessibility walker, neither of
205
+ * which is here, and they are a third of the bytes. They also reach the DOM
206
+ * through an SVG renderer that hands unknown attributes to React, which
207
+ * complains about every one of them.
208
+ */
209
+ const ANNOTATIONS = /\sdata-[a-zA-Z0-9-]+="[^"]*"/g
210
+
211
+ const available = true
212
+
213
+ /**
214
+ * The drawing cache. Rendering is fast -- around a millisecond on a mid-range
215
+ * phone -- but a display redraws for all sorts of reasons that have nothing to
216
+ * do with its maths, and the same expression at the same size in the same
217
+ * colour is the same picture every time.
218
+ */
219
+ const cache = new Map<string, RenderedMath | null>()
220
+
221
+ /**
222
+ * How many goes an expression gets before it is called undrawable.
223
+ *
224
+ * One, plus one. With the ranges loaded up front nothing should have to try
225
+ * twice; the second go is there because the failure it covers -- a glyph
226
+ * resolved from inside the layout, on a stack with no room left -- leaves the
227
+ * range loaded on its way out, so a retry is free and always the one that
228
+ * works.
229
+ */
230
+ const MAX_ATTEMPTS = 2
231
+
232
+ /**
233
+ * TeX in, one standalone SVG and its box out.
234
+ *
235
+ * The box is read off the root element rather than measured: MathJax reports
236
+ * width, height and the depth below the baseline in `ex`, and an `ex` is half
237
+ * the font size by its own `exFactor`. So nothing has to be laid out to find
238
+ * out how much room to ask for -- which is exactly what a text layout needs to
239
+ * know before it can lay the line out.
240
+ *
241
+ * Returns nothing for an expression it cannot draw, rather than throwing. TeX
242
+ * that reaches for something the bundled fonts do not carry ends in a
243
+ * `RangeError`, and one bad equation should cost the equation rather than the
244
+ * message it is in.
245
+ */
246
+ function renderMath(
247
+ tex: string,
248
+ display: boolean,
249
+ fontSize: number,
250
+ color: string | undefined,
251
+ ): RenderedMath | null {
252
+ const key = `${display ? "D" : "I"}${fontSize}\u0000${color ?? ""}\u0000${tex}`
253
+ const hit = cache.get(key)
254
+
255
+ if (hit !== undefined) {
256
+ return hit
257
+ }
258
+
259
+ loadRanges()
260
+
261
+ let rendered: RenderedMath | null = null
262
+
263
+ // See MAX_ATTEMPTS: the second go is the one that succeeds if a glyph had to
264
+ // be resolved from inside the layout after all.
265
+ for (let attempt = 0; attempt < MAX_ATTEMPTS && rendered === null; attempt++) {
266
+ rendered = attemptRender(tex, display, fontSize, color)
267
+ }
268
+
269
+ cache.set(key, rendered)
270
+
271
+ return rendered
272
+ }
273
+
274
+ function attemptRender(
275
+ tex: string,
276
+ display: boolean,
277
+ fontSize: number,
278
+ color: string | undefined,
279
+ ): RenderedMath | null {
280
+ try {
281
+ const node = doc.convert(tex, {
282
+ display,
283
+ em: fontSize,
284
+ ex: fontSize / 2,
285
+ containerWidth: 1e6,
286
+ })
287
+ // `outerHTML` wraps the picture in MathJax's own `<mjx-container>`, which is
288
+ // a custom element meant for a browser: an SVG renderer handed one has no
289
+ // idea what to do with it. Only the `<svg>` inside is wanted.
290
+ const svg = adaptor.outerHTML(node).match(ELEMENT)?.[0] ?? ""
291
+ const root = svg.match(ROOT)?.[0] ?? ""
292
+ const ex = EX.exec(root)
293
+
294
+ if (!ex) {
295
+ return null
296
+ }
297
+
298
+ const va = VA.exec(root)
299
+ const unit = fontSize / 2
300
+
301
+ return {
302
+ // Two things come off the root. Its `width`, `height` and
303
+ // `vertical-align` are stated in `ex`, and a browser resolves an `ex`
304
+ // from the real font's x-height while these numbers assume MathJax's own
305
+ // half-an-em: left in, the picture comes out a different width from the
306
+ // box reserved for it and the baseline shift is applied twice, once by
307
+ // the element and once by whatever is positioning it. The `viewBox`
308
+ // stays, so the picture scales to whatever size it is given. And
309
+ // `currentColor` inherits from nothing inside a `Text` on either
310
+ // platform, so the colour is painted in here rather than left to a prop
311
+ // the caller would have to know to pass.
312
+ svg: svg
313
+ .replace(ROOT, (tag) => tag.replace(SIZING, ""))
314
+ .replace(ANNOTATIONS, "")
315
+ .replace(CURRENT_COLOR, color ?? "currentColor"),
316
+ width: Number(ex[1]) * unit,
317
+ height: Number(ex[2]) * unit,
318
+ depth: va ? -Number(va[1]) * unit : 0,
319
+ }
320
+ } catch {
321
+ // Swallowed: only `renderMath` above knows whether a second go is worth it.
322
+ return null
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Reading an expression out loud, which is a different problem from drawing it.
328
+ *
329
+ * The engine is MathJax's own -- SRE, which it depends on and drives in a web
330
+ * worker for the same purpose in a browser. There is no worker here, so it is
331
+ * driven directly: TeX is compiled to MathML (`STATE.COMPILED` stops one step
332
+ * short of typesetting, so this costs a parse and nothing else) and MathML is
333
+ * what SRE reads.
334
+ *
335
+ * `mathspeak` is the ruleset MathJax defaults to, and the one whose phrasing --
336
+ * "StartFraction a Over b EndFraction" -- screen reader users of maths on the
337
+ * web already know.
338
+ */
339
+
340
+ /**
341
+ * Every locale SRE can speak, behind thunks a bundler can see, the way the font
342
+ * ranges are. `base` carries everything language-independent and is always
343
+ * needed.
344
+ *
345
+ * The two rulesets SRE ships that are not languages are left out: `nemeth` and
346
+ * `euro` are braille codes rather than speech, and a braille cell read aloud is
347
+ * nobody's accessible name.
348
+ *
349
+ * They are all named here because a `require` has to be written down for a
350
+ * bundler to find it -- and then all but the ones an application asked for are
351
+ * resolved away to nothing by `withMath(config, { locales })`, which is what
352
+ * keeps a display that reads in English from carrying the four megabytes of
353
+ * rules for the twelve languages it never speaks. See `../metro.ts`.
354
+ */
355
+ const MAPS: Record<string, () => unknown> = {
356
+ base: () => require("speech-rule-engine/lib/mathmaps/base.json"),
357
+ af: () => require("speech-rule-engine/lib/mathmaps/af.json"),
358
+ ca: () => require("speech-rule-engine/lib/mathmaps/ca.json"),
359
+ da: () => require("speech-rule-engine/lib/mathmaps/da.json"),
360
+ de: () => require("speech-rule-engine/lib/mathmaps/de.json"),
361
+ en: () => require("speech-rule-engine/lib/mathmaps/en.json"),
362
+ es: () => require("speech-rule-engine/lib/mathmaps/es.json"),
363
+ fr: () => require("speech-rule-engine/lib/mathmaps/fr.json"),
364
+ hi: () => require("speech-rule-engine/lib/mathmaps/hi.json"),
365
+ it: () => require("speech-rule-engine/lib/mathmaps/it.json"),
366
+ ko: () => require("speech-rule-engine/lib/mathmaps/ko.json"),
367
+ nb: () => require("speech-rule-engine/lib/mathmaps/nb.json"),
368
+ nn: () => require("speech-rule-engine/lib/mathmaps/nn.json"),
369
+ sv: () => require("speech-rule-engine/lib/mathmaps/sv.json"),
370
+ }
371
+
372
+ /** What an expression is read in when the caller names no locale. */
373
+ const DEFAULT_LOCALE = "en"
374
+
375
+ /**
376
+ * The locales whose rules actually arrived.
377
+ *
378
+ * A locale resolved away to nothing comes back as an empty object, and once SRE
379
+ * has swallowed one there is no asking about it afterwards: a rule it cannot
380
+ * find for the locale it was asked for is answered out of the default locale's,
381
+ * so the expression is read out perfectly, in the wrong language. This is
382
+ * recorded on the way past, which is the last moment the difference is visible.
383
+ */
384
+ const installed = new Set<string>()
385
+
386
+ /**
387
+ * How SRE is set up, and how it is told to load a locale.
388
+ *
389
+ * The loader returns a *synchronous* thenable rather than a promise, and that
390
+ * is the whole trick to getting speech on the first render. SRE calls
391
+ * `loader(locale).then(install)` directly on whatever comes back, so a `then`
392
+ * that calls its callback in place has the rules installed before the call
393
+ * returns -- where a real promise would install them a microtask later, by
394
+ * which time the label has already been built and handed to the platform.
395
+ */
396
+ function features(locale: string) {
397
+ return {
398
+ mode: "sync",
399
+ locale,
400
+ domain: "mathspeak",
401
+ style: "default",
402
+ modality: "speech",
403
+ custom: (name: string) => ({
404
+ then: (resolve: (maps: unknown) => void) => {
405
+ const maps = MAPS[name]?.() ?? {}
406
+
407
+ if (Object.keys(maps as object).length > 0) {
408
+ installed.add(name)
409
+ }
410
+
411
+ resolve(maps)
412
+ },
413
+ }),
414
+ }
415
+ }
416
+
417
+ const visitor = new SerializedMmlVisitor()
418
+
419
+ /** Something every ruleset can say, used to check the engine came up. */
420
+ const PROBE = '<math xmlns="http://www.w3.org/1998/Math/MathML"><mi>x</mi></math>'
421
+
422
+ /** Whether SRE has been brought as far as its language-independent rules. */
423
+ let started = false
424
+
425
+ /**
426
+ * Three setups rather than one, because SRE loads in steps and each call
427
+ * carries it one step further: the first is the one it reserves for its own
428
+ * initialisation and which loads nothing, the second installs `base`, and the
429
+ * third installs the default locale.
430
+ *
431
+ * The third is not optional even when the default locale is not the one wanted.
432
+ * SRE reaches a locale by walking `base`, then the *default* locale, then the
433
+ * wanted one, and `select` can only make that walk synchronous for promises
434
+ * that already exist when it starts patching. Loading French first would create
435
+ * the English promise halfway through the walk, unpatched, and the walk would
436
+ * stop there -- English installed, French silently not, one microtask short.
437
+ */
438
+ function start() {
439
+ if (started) {
440
+ return
441
+ }
442
+
443
+ // Set first: a setup that throws must not be attempted again on every span.
444
+ started = true
445
+
446
+ setupEngine(features(DEFAULT_LOCALE) as never)
447
+ setupEngine(features(DEFAULT_LOCALE) as never)
448
+ select(DEFAULT_LOCALE)
449
+ }
450
+
451
+ /**
452
+ * Points the engine at one locale, loading its rules if this is the first ask.
453
+ *
454
+ * Every promise SRE is holding is replaced, for the length of the call, by one
455
+ * that calls its callback in place. SRE walks a chain of them before it reaches
456
+ * the locale it was asked for -- `base` first, then the default locale, then
457
+ * the wanted one -- and a resolved promise still defers its callback by a
458
+ * microtask, which is a microtask longer than the render that wants the label.
459
+ * A thenable that calls back in place does not. They are all put back before
460
+ * anything else can see them.
461
+ */
462
+ function select(locale: string) {
463
+ const held = { ...EnginePromise.promises }
464
+
465
+ for (const name of Object.keys(held)) {
466
+ EnginePromise.promises[name] = {
467
+ then: (resolve: (loaded: string) => void) => resolve(name),
468
+ } as unknown as Promise<string>
469
+ }
470
+
471
+ try {
472
+ setupEngine(features(locale) as never)
473
+ } finally {
474
+ for (const [name, promise] of Object.entries(held)) {
475
+ EnginePromise.promises[name] = promise
476
+ }
477
+ }
478
+ }
479
+
480
+ /** Which locale the engine is pointed at, and which locales can be spoken in. */
481
+ let current: string | null = null
482
+ const speakable = new Map<string, boolean>()
483
+
484
+ /**
485
+ * Points the engine at `locale` and says whether anything can be said in it.
486
+ *
487
+ * All of this depends on internals that are not SRE's public API, so the result
488
+ * is checked rather than assumed: the rules have to have arrived and `PROBE`
489
+ * has to come out as speech. When it does not, or when any of this throws, that
490
+ * locale is written off and the caller falls back -- to the default locale, and
491
+ * from there to the TeX.
492
+ */
493
+ function useLocale(locale: string): boolean {
494
+ const known = speakable.get(locale)
495
+
496
+ // The engine holds one locale at a time, so a locale that is already the one
497
+ // selected needs nothing done to it, and one already known to be unspeakable
498
+ // must not be selected -- it would leave the engine pointed somewhere it
499
+ // cannot read from.
500
+ if (current === locale) {
501
+ return known ?? false
502
+ }
503
+
504
+ if (known === false) {
505
+ return false
506
+ }
507
+
508
+ try {
509
+ start()
510
+ select(locale)
511
+ current = locale
512
+
513
+ const speaks = installed.has(locale) && toSpeech(PROBE) !== ""
514
+ speakable.set(locale, speaks)
515
+
516
+ return speaks
517
+ } catch {
518
+ // Left mute: a display falls back to the TeX, which is worse than speech
519
+ // and a great deal better than an unreadable paragraph.
520
+ speakable.set(locale, false)
521
+
522
+ return false
523
+ }
524
+ }
525
+
526
+ /** Locales already complained about, so a document warns once rather than once a span. */
527
+ const warned = new Set<string>()
528
+
529
+ function warn(locale: string) {
530
+ if (warned.has(locale)) {
531
+ return
532
+ }
533
+
534
+ warned.add(locale)
535
+
536
+ const spoken = Object.keys(MAPS)
537
+ .filter((name) => name !== "base")
538
+ .join(", ")
539
+
540
+ console.warn(
541
+ locale in MAPS
542
+ ? `[react-native-marcus] no speech rules for "${locale}" in this bundle. Pass ` +
543
+ `{ locales: ["${DEFAULT_LOCALE}", "${locale}"] } to withMath() in metro.config.js. ` +
544
+ `Maths is being read in "${DEFAULT_LOCALE}".`
545
+ : `[react-native-marcus] "${locale}" is not a locale the speech engine has. It speaks ` +
546
+ `${spoken}. Maths is being read in "${DEFAULT_LOCALE}".`,
547
+ )
548
+ }
549
+
550
+ /** The speech cache. Speaking an expression costs about as much as drawing it. */
551
+ const speech = new Map<string, string | null>()
552
+
553
+ /**
554
+ * TeX in, the expression as a sentence out.
555
+ *
556
+ * Returns nothing for anything it cannot say, which includes TeX that does not
557
+ * parse and a document where the rules never loaded.
558
+ */
559
+ function speakMath(tex: string, display: boolean, locale?: string): string | null {
560
+ const wanted = locale ?? DEFAULT_LOCALE
561
+ const key = `${wanted}\u0000${display ? "D" : "I"}${tex}`
562
+ const hit = speech.get(key)
563
+
564
+ if (hit !== undefined) {
565
+ return hit
566
+ }
567
+
568
+ let speaks = useLocale(wanted)
569
+
570
+ // A language nobody bundled is worth saying out loud once: the expression is
571
+ // still read out, in the default locale, and the fix is one line of Metro
572
+ // config away. Silence would leave a French reader hearing English with no
573
+ // hint as to why. Complained about only once the fallback has answered,
574
+ // because an engine that cannot speak at all is not a bundling mistake and
575
+ // saying it is would send someone to the wrong file.
576
+ if (!speaks && wanted !== DEFAULT_LOCALE) {
577
+ speaks = useLocale(DEFAULT_LOCALE)
578
+
579
+ if (speaks) {
580
+ warn(wanted)
581
+ }
582
+ }
583
+
584
+ let said: string | null = null
585
+
586
+ if (speaks) {
587
+ try {
588
+ const root = doc.convert(tex, { display, end: STATE.COMPILED }) as MmlNode
589
+ const spoken = toSpeech(visitor.visitTree(root))
590
+
591
+ said = spoken === "" ? null : spoken
592
+ } catch {
593
+ // Unsayable, which is not a reason to lose the drawing beside it.
594
+ }
595
+ }
596
+
597
+ speech.set(key, said)
598
+
599
+ return said
600
+ }
601
+
602
+ export type { RenderedMath }
603
+
604
+ export { available, renderMath, speakMath }