react-native-marcus 0.1.1 → 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,668 @@
1
+ import { StyleSheet, Text } from "react-native"
2
+ import React from "react"
3
+ import { createPortal } from "react-dom"
4
+ import type { TextProps } from "react-native"
5
+
6
+ import "./web/MarkdownElements.css"
7
+
8
+ import { atomsOf, NO_ATOMS, sourceOf } from "./atomUtils"
9
+ import { available as mathAvailable, renderMath, speakMath } from "./math"
10
+ import { flattenText } from "./childrenUtils"
11
+ import { OBJECT_REPLACEMENT, stripSyntax } from "./stripUtils"
12
+ import { createTextMeasurer } from "./web/utils/measureUtils"
13
+ import { parseRangesToHTMLNodes } from "./web/utils/parserUtils"
14
+ import { processMarkdownStyle } from "./web/utils/webStyleUtils"
15
+ import { createParser } from "./parser"
16
+ import type { PartialMarkdownStyle } from "./styleUtils"
17
+ import type { MarkdownLink } from "./stripUtils"
18
+ import type { TreeNode } from "./web/utils/treeUtils"
19
+
20
+ /**
21
+ * What to show in place of an image.
22
+ *
23
+ * The native contract asks for a view with a definite size, because the text
24
+ * layout has to know how much room to leave before it lays the line out. The
25
+ * web has no such requirement -- the browser reflows around whatever turns up
26
+ * -- but the same callback runs on both, so write it for the stricter one.
27
+ *
28
+ * `inline` says whether the image shares its line with anything else, which is
29
+ * usually what decides how big it should be: alone on a line it is a figure,
30
+ * and in the middle of a sentence it is a badge. A block marker does not count
31
+ * as company, and another image does.
32
+ *
33
+ * Every string is always a string: an image written without a title is handed
34
+ * an empty one rather than nothing, so a caller never has to tell the two
35
+ * apart.
36
+ */
37
+ type RenderEmbed = (
38
+ uri: string,
39
+ alt: string,
40
+ title: string,
41
+ inline: boolean,
42
+ ) => React.ReactElement | null | undefined
43
+
44
+ /**
45
+ * What to draw an SVG with.
46
+ *
47
+ * Handed a complete SVG document and the box it belongs in, and expected to
48
+ * return a view of exactly that size. There is deliberately nothing else in the
49
+ * signature: everything a picture needs -- the colour, the glyph outlines, the
50
+ * baseline it sits on -- is either inside the XML already or has been taken
51
+ * care of by the time this is called, so `<SvgXml xml={xml} width={width}
52
+ * height={height} />` is the whole of a correct implementation.
53
+ *
54
+ * It exists because a renderer for SVG is a dependency, and which one is not
55
+ * this library's business: `react-native-svg` on native, whatever you like on
56
+ * the web.
57
+ *
58
+ * Returning nothing leaves the drawing out, and the span it stood for still
59
+ * costs its one character either way.
60
+ */
61
+ type RenderSVG = (
62
+ xml: string,
63
+ width: number,
64
+ height: number,
65
+ ) => React.ReactElement | null | undefined
66
+
67
+ /**
68
+ * What to do when a link is pressed.
69
+ *
70
+ * The label is the text the link was rendered as, which is all a reader saw of
71
+ * it: the destination is not in the document any more. Reference links are not
72
+ * covered -- their destination is written somewhere else and is not resolved
73
+ * yet -- so they render as links and do nothing.
74
+ *
75
+ * Opening the URL is deliberately not done for you: what a link means is the
76
+ * application's to decide, and a message from a stranger is the last place to
77
+ * hand an arbitrary URL to `Linking.openURL` without looking at it first.
78
+ *
79
+ * Every string is always a string: a link written without a title is handed an
80
+ * empty one rather than nothing.
81
+ */
82
+ type OnLinkPress = (uri: string, label: string, title: string) => void
83
+
84
+ interface MarkdownTextProps extends TextProps {
85
+ markdownStyle?: PartialMarkdownStyle
86
+ /**
87
+ * GFM's strikethrough (`~~a~~`) and bare autolinks (`www.example.com`).
88
+ *
89
+ * @default false
90
+ */
91
+ gfm?: boolean
92
+ /**
93
+ * `@name` mentions, which no flavour of markdown has a notion of.
94
+ *
95
+ * @default false
96
+ */
97
+ mention?: boolean
98
+ /**
99
+ * LaTeX between double dollar signs: `$$x^2$$` inline, and the same on lines
100
+ * of its own for a block.
101
+ *
102
+ * @default false
103
+ */
104
+ math?: boolean
105
+ renderEmbed?: RenderEmbed
106
+ /**
107
+ * What to draw a maths span with. Nothing is drawn without it, and `$$x^2$$`
108
+ * is left on the screen as it was written.
109
+ */
110
+ renderSVG?: RenderSVG
111
+ /**
112
+ * The language a drawn equation is read out in, as the speech engine names
113
+ * it: `af`, `ca`, `da`, `de`, `en`, `es`, `fr`, `hi`, `it`, `ko`, `nb`, `nn`
114
+ * or `sv`.
115
+ *
116
+ * What a document is written in and what its reader speaks are two different
117
+ * questions, and only the application knows the answer to the second, so this
118
+ * is usually whatever the rest of the interface is translated into. It
119
+ * changes only how the maths is *said*: the prose around it is read in the
120
+ * language it was written in, and the drawing itself is the same picture in
121
+ * every language.
122
+ *
123
+ * The rules for one language are around 300KB, so only the ones named in
124
+ * `withMath(config, { locales })` are in the bundle. A language that is not
125
+ * is read in English, with a warning in the console saying which line of
126
+ * Metro config would fix it.
127
+ *
128
+ * @default "en"
129
+ */
130
+ mathAccessibilityLocale?: string
131
+ onLinkPress?: OnLinkPress
132
+ }
133
+
134
+ type MarkdownText = Text & React.Component<MarkdownTextProps>
135
+
136
+ // `useLayoutEffect` warns when it runs during a server render, and there is no layout to be before
137
+ // on a server anyway. Same seam the input uses.
138
+ const useClientEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect
139
+
140
+ const NO_SLOTS: HTMLElement[] = []
141
+
142
+ /**
143
+ * What maths is sized against when the caller set no size at all, matching what
144
+ * the native component assumes so the two agree.
145
+ */
146
+ const DEFAULT_FONT_SIZE = 14
147
+
148
+ /**
149
+ * Replaces each placeholder character in the built markup with an empty element
150
+ * to render an embed or a maths span into.
151
+ *
152
+ * The elements have to be made here rather than rendered, because everything
153
+ * under the host is raw DOM this component built and React knows nothing about
154
+ * -- so the embeds are portalled into these afterwards, which is the one way to
155
+ * put a React subtree somewhere React did not put it.
156
+ *
157
+ * Text nodes are collected before any of them is split: splitting rewrites the
158
+ * tree the walker is walking.
159
+ */
160
+ function mountSlots(host: HTMLElement): HTMLElement[] {
161
+ const walker = document.createTreeWalker(host, NodeFilter.SHOW_TEXT)
162
+ // `globalThis` because `Text` in this file is the react-native one.
163
+ const texts: globalThis.Text[] = []
164
+
165
+ while (walker.nextNode()) {
166
+ texts.push(walker.currentNode as globalThis.Text)
167
+ }
168
+
169
+ const slots: HTMLElement[] = []
170
+
171
+ for (const text of texts) {
172
+ let node = text
173
+ let index = node.data.indexOf(OBJECT_REPLACEMENT)
174
+
175
+ while (index !== -1) {
176
+ const placeholder = node.splitText(index)
177
+ const rest = placeholder.splitText(OBJECT_REPLACEMENT.length)
178
+ const slot = document.createElement("span")
179
+
180
+ // Inline-block so the embed sits in the line the way an attachment does
181
+ // on native, on the baseline and taking the width it asks for.
182
+ slot.style.display = "inline-block"
183
+ // `text-indent` is inherited, and an inline-block lays out its own first
184
+ // line -- so a list item's hanging indent, which is a negative
185
+ // `text-indent` on the item, shifted the drawing left out of its box and
186
+ // over the word before it. Nothing in here is a first line of anything.
187
+ slot.style.textIndent = "0"
188
+ slot.setAttribute("data-atom", String(slots.length))
189
+
190
+ placeholder.replaceWith(slot)
191
+ slots.push(slot)
192
+
193
+ node = rest
194
+ index = node.data.indexOf(OBJECT_REPLACEMENT)
195
+ }
196
+ }
197
+
198
+ return slots.length > 0 ? slots : NO_SLOTS
199
+ }
200
+
201
+ /**
202
+ * How the list markers are drawn, read off the element they will be drawn in.
203
+ *
204
+ * A display renders them rather than showing them, and their size comes from the base font --
205
+ * whatever the wrapped `Text` resolved to, so it is asked rather than assumed.
206
+ */
207
+ function markerRendering(host: HTMLElement) {
208
+ return { display: true, fontSize: parseFloat(window.getComputedStyle(host).fontSize) || 0 }
209
+ }
210
+
211
+ /** Anything with a scheme this does not name is left without an `href`. */
212
+ const SAFE_SCHEME = /^(?:https?|mailto|tel):/i
213
+ const SCHEME = /^[a-z][a-z0-9+.\-]*:/i
214
+
215
+ /**
216
+ * The destination as an `href`, or nothing if it must not become one.
217
+ *
218
+ * A browser strips whitespace and control characters before it reads a scheme,
219
+ * so `java\nscript:alert(1)` is `javascript:` as far as it is concerned, and it
220
+ * has to be here too. Anything carrying a scheme that is not one of the four
221
+ * safe ones is refused outright rather than guessed at; a destination with no
222
+ * scheme is a relative URL and is left to resolve the way any other relative
223
+ * link on the page does.
224
+ */
225
+ function hrefFor(uri: string) {
226
+ const bare = uri.replace(/[\u0000-\u0020]/g, "")
227
+
228
+ if (bare === "" || (SCHEME.test(bare) && !SAFE_SCHEME.test(bare))) {
229
+ return null
230
+ }
231
+
232
+ return uri
233
+ }
234
+
235
+ /**
236
+ * Marks the elements a link was built into with the link they belong to, and hands them the
237
+ * destination when it is one a browser may follow.
238
+ *
239
+ * The markup is not one element per link: a label split over two lines is built
240
+ * into one element per line, and a label with emphasis in it is an element with
241
+ * more elements inside. So the press is caught on the host and walked back up to
242
+ * the nearest marked ancestor, and all this has to do is put the mark on every
243
+ * element that a link's own range produced.
244
+ *
245
+ * They are anchors already -- that is the tag `link` is built as, in the input
246
+ * too, where a link being typed has no destination yet. What is added here is
247
+ * the `href`, which comes from the document's destinations rather than from its
248
+ * ranges. With one, the browser shows the destination in the status bar, offers
249
+ * it to the context menu, opens it in a new tab on a modified click, and a
250
+ * screen reader announces it; the click handler stops it navigating, because
251
+ * what a link means is the application's to decide. A destination that cannot
252
+ * safely become an href gets none -- and an anchor without one is neither in the
253
+ * tab order nor a link to a screen reader, so those two are said outright.
254
+ */
255
+ function markLinks(node: TreeNode, links: MarkdownLink[]) {
256
+ if (node.type === "link") {
257
+ const index = links.findIndex(
258
+ (link) => node.start >= link.start && node.start < link.start + link.length,
259
+ )
260
+
261
+ // A reference link has no destination yet, so its label is styled as a link
262
+ // and left inert rather than marked.
263
+ if (index !== -1) {
264
+ const link = links[index]!
265
+ const href = hrefFor(link.uri)
266
+ const element = node.element
267
+
268
+ if (href !== null) {
269
+ element.setAttribute("href", href)
270
+ // Untrusted markdown opened in a new tab has no business reaching back.
271
+ element.setAttribute("rel", "noopener noreferrer")
272
+
273
+ if (link.title !== "") {
274
+ element.setAttribute("title", link.title)
275
+ }
276
+ } else {
277
+ // Nothing else on the page is reachable by keyboard, and a link a
278
+ // pointer can press and a keyboard cannot is not a link.
279
+ element.setAttribute("role", "link")
280
+ element.setAttribute("tabindex", "0")
281
+ element.style.cursor = "pointer"
282
+ }
283
+
284
+ element.setAttribute("data-link", String(index))
285
+ }
286
+ }
287
+
288
+ node.childNodes.forEach((child) => markLinks(child, links))
289
+ }
290
+
291
+ /**
292
+ * `Text`, with the markdown in its content rendered rather than shown.
293
+ *
294
+ * Built by the same DOM builder the input uses, from the same ranges, so a message and the composer
295
+ * it was typed in agree by construction -- which on the web means agreeing about indents, about
296
+ * ribbons drawn as background layers, and about the marker widths those are measured from. The only
297
+ * difference is the input, where the syntax is still there to be edited.
298
+ *
299
+ * Rendered as a real `Text` whose children are then replaced. React only ever writes the stripped
300
+ * string into the host element -- a lone string child is set with `textContent`, never as a node it
301
+ * holds on to -- so the effect below is free to put the built markup there instead, and a render
302
+ * that changes the text simply wipes it and lets the effect rebuild before the frame is painted.
303
+ * Rendering the plain string rather than nothing also means a prerendered page carries the prose:
304
+ * `expo export` runs every route through Node, where there is no DOM to build markup with.
305
+ */
306
+ const MarkdownText = React.forwardRef<MarkdownText, MarkdownTextProps>((props, ref) => {
307
+ const {
308
+ markdownStyle: markdownStyleProp,
309
+ gfm,
310
+ mention,
311
+ math,
312
+ renderEmbed,
313
+ renderSVG,
314
+ mathAccessibilityLocale,
315
+ onLinkPress,
316
+ children,
317
+ ...textProps
318
+ } = props
319
+
320
+ const hostRef = React.useRef<HTMLElement | null>(null)
321
+ const [slots, setSlots] = React.useState<HTMLElement[]>(NO_SLOTS)
322
+
323
+ // Whether there is a handler, not which one: a caller who writes the callback
324
+ // inline would otherwise re-parse the document on every render.
325
+ const withEmbeds = renderEmbed !== undefined
326
+ // Maths is only taken out of the text when there is both something to draw it
327
+ // with and a backend to typeset it -- see `./math`. Left in, `$$x^2$$` is
328
+ // shown as it was written.
329
+ const withMath = renderSVG !== undefined && mathAvailable
330
+ const withLinks = onLinkPress !== undefined
331
+
332
+ // The same worklet for as long as the flavour is the same one, so the
333
+ // document is only re-parsed when the document itself changes.
334
+ const parser = createParser({ gfm, mention, math })
335
+
336
+ const markdown = React.useMemo(() => {
337
+ const text = flattenText(children)
338
+
339
+ return text === null
340
+ ? null
341
+ : stripSyntax(text, parser(text), {
342
+ embeds: withEmbeds,
343
+ math: withMath,
344
+ links: withLinks,
345
+ })
346
+ }, [children, parser, withEmbeds, withMath, withLinks])
347
+
348
+ // Both kinds of placeholder in the order they stand in the text, which is the
349
+ // order `mountSlots` makes the slots in.
350
+ const atoms = React.useMemo(
351
+ () => (markdown === null ? NO_ATOMS : atomsOf(markdown)),
352
+ [markdown],
353
+ )
354
+
355
+ // Read off this element's own style rather than the computed one, so the
356
+ // numbers the maths is drawn at are the numbers the native component would
357
+ // draw it at.
358
+ const { fontSize, color } = React.useMemo(() => {
359
+ const flattened = StyleSheet.flatten(textProps.style)
360
+
361
+ return {
362
+ fontSize:
363
+ typeof flattened?.fontSize === "number" ? flattened.fontSize : DEFAULT_FONT_SIZE,
364
+ color: typeof flattened?.color === "string" ? flattened.color : undefined,
365
+ }
366
+ }, [textProps.style])
367
+
368
+ // Drawn during render rather than at portal time, because the depth a maths
369
+ // span reports has to be on its slot before the frame is painted.
370
+ const drawn = React.useMemo(
371
+ () =>
372
+ atoms.map((atom) => {
373
+ if (atom.kind === "embed") {
374
+ return {
375
+ element: renderEmbed?.(atom.uri, atom.alt, atom.title, atom.inline) ?? null,
376
+ depth: 0,
377
+ // Whatever the caller's element measures: only it knows how big an
378
+ // embed is, and that is what `renderEmbed` promises to say.
379
+ box: null as { width: number; height: number } | null,
380
+ // An image's `alt` is its accessible name by every spec there is,
381
+ // and left empty it declares the image decorative.
382
+ label: atom.alt,
383
+ role: "img",
384
+ // Nothing: an image's alt text is prose, written in the language
385
+ // the document is written in, and saying otherwise would have a
386
+ // screen reader read it in the wrong voice.
387
+ lang: "",
388
+ source: sourceOf(atom),
389
+ }
390
+ }
391
+
392
+ const rendered = renderMath(atom.tex, atom.display, fontSize, color)
393
+
394
+ return {
395
+ element:
396
+ rendered === null
397
+ ? null
398
+ : renderSVG?.(rendered.svg, rendered.width, rendered.height),
399
+ depth: rendered?.depth ?? 0,
400
+ box: rendered === null ? null : { width: rendered.width, height: rendered.height },
401
+ // What the picture says, rather than the TeX it was written as: a
402
+ // reader who asked for a document did not ask for its source code.
403
+ label: speakMath(atom.tex, atom.display, mathAccessibilityLocale) ?? atom.tex,
404
+ role: "math",
405
+ // The language the label is in, which is the one thing on the page
406
+ // that may differ from the document's own: a screen reader picks its
407
+ // voice from this, and French read in an English voice is worse than
408
+ // no voice at all.
409
+ lang: mathAccessibilityLocale ?? "",
410
+ // What a copy puts on the clipboard in place of the picture: the
411
+ // markdown it was written as, so that pasting it back gives the
412
+ // document it came from rather than a hole where an equation was.
413
+ source: sourceOf(atom),
414
+ }
415
+ }),
416
+ [atoms, renderEmbed, renderSVG, fontSize, color, mathAccessibilityLocale],
417
+ )
418
+
419
+ const markdownStyle = React.useMemo(
420
+ () => processMarkdownStyle(markdownStyleProp),
421
+ [markdownStyleProp],
422
+ )
423
+
424
+ // What React writes into the host, and so what a prerendered page carries. The placeholders go:
425
+ // they only mean anything to the markup the effect builds, and a page rendered in Node -- where
426
+ // there is no DOM to build that markup with -- would otherwise show a row of tofu.
427
+ const content = React.useMemo(() => {
428
+ if (markdown === null) {
429
+ return null
430
+ }
431
+
432
+ return markdown.text.includes(OBJECT_REPLACEMENT)
433
+ ? markdown.text.split(OBJECT_REPLACEMENT).join("")
434
+ : markdown.text
435
+ }, [markdown])
436
+
437
+ useClientEffect(() => {
438
+ const host = hostRef.current
439
+ if (!host || markdown === null) {
440
+ return
441
+ }
442
+
443
+ const { dom, tree } = parseRangesToHTMLNodes(
444
+ markdown.text,
445
+ markdown.ranges,
446
+ true,
447
+ markdownStyle,
448
+ false,
449
+ // Measured against this element, so marker widths come out in the font the text is actually
450
+ // drawn in rather than the document's default.
451
+ createTextMeasurer(host),
452
+ // A display renders the list markers rather than showing them, sized from the font this
453
+ // element renders at: the base font, which is the wrapped `Text`'s own.
454
+ markerRendering(host),
455
+ )
456
+
457
+ if (markdown.links.length > 0) {
458
+ markLinks(tree, markdown.links)
459
+ }
460
+
461
+ host.replaceChildren(...Array.from(dom.childNodes))
462
+
463
+ const next = atoms.length > 0 ? mountSlots(host) : NO_SLOTS
464
+
465
+ // Both empty is the overwhelmingly common case, and setting state there would re-render every
466
+ // document that has no embeds in it for nothing.
467
+ setSlots((previous) => (previous === NO_SLOTS && next === NO_SLOTS ? previous : next))
468
+ // React wipes the host with `textContent` whenever the string child changes, which is exactly
469
+ // when `markdown` does, so these two are all it takes to be sure the markup is back before the
470
+ // frame is painted.
471
+ }, [markdown, markdownStyle, atoms])
472
+
473
+ // Everything about a slot that is the slot's own business rather than the
474
+ // caller's: it is the inline-level box, so aligning anything inside it would
475
+ // only move that box's contents, and it is the element in the accessibility
476
+ // tree, so it is the one thing that can be given a name. Its own effect, so
477
+ // that a caller redrawing from state brings all three with it.
478
+ useClientEffect(() => {
479
+ slots.forEach((slot, index) => {
480
+ const content = drawn[index]
481
+ const depth = content?.depth ?? 0
482
+
483
+ // The depth the maths hangs below the baseline, which on the web is the
484
+ // same displacement by another name.
485
+ slot.style.verticalAlign = depth === 0 ? "" : `${-depth}px`
486
+
487
+ // The box the drawing was measured for, stated rather than shrink-wrapped.
488
+ // An inline-block with no width of its own is shrink-to-fit, and
489
+ // shrink-to-fit is capped by what is left of the line -- so a wide
490
+ // expression near a margin got a box narrower than its own picture and the
491
+ // picture hung out of it, over the words beside it. Native reserves the
492
+ // measured box; this is the same thing said in CSS.
493
+ if (content?.box) {
494
+ slot.style.width = `${content.box.width}px`
495
+ slot.style.height = `${content.box.height}px`
496
+ } else {
497
+ slot.style.width = ""
498
+ slot.style.height = ""
499
+ }
500
+
501
+ if (content === undefined || content.label === "") {
502
+ slot.removeAttribute("role")
503
+ slot.removeAttribute("aria-label")
504
+ } else {
505
+ // `math` and `img` both take a name from the author, so the label wins
506
+ // over whatever glyph outlines are underneath it.
507
+ slot.setAttribute("role", content.role)
508
+ slot.setAttribute("aria-label", content.label)
509
+ }
510
+
511
+ if (content?.lang) {
512
+ slot.setAttribute("lang", content.lang)
513
+ } else {
514
+ slot.removeAttribute("lang")
515
+ }
516
+
517
+ if (content?.source) {
518
+ slot.setAttribute("data-tex", content.source)
519
+ } else {
520
+ slot.removeAttribute("data-tex")
521
+ }
522
+ })
523
+ }, [slots, drawn])
524
+
525
+ // Copying maths gives back the markdown it was written as.
526
+ //
527
+ // A selection dragged across a drawing copies whatever text is under it,
528
+ // which for an SVG is nothing at all -- so a message copied out of a chat
529
+ // arrives with a hole where its equation was. The clipboard is the one place
530
+ // the source is worth more than the picture, and this is the only hook the
531
+ // web offers: the copy is intercepted, the fragment the browser was about to
532
+ // serialise is walked, and every slot in it is swapped for the `$$…$$` it
533
+ // stands for.
534
+ //
535
+ // On the document rather than the host, because that is where a copy lands
536
+ // when the selection reaches past this element. It is left alone unless the
537
+ // fragment actually holds one of these slots.
538
+ useClientEffect(() => {
539
+ const host = hostRef.current
540
+
541
+ if (!host || !drawn.some((content) => content.source !== "")) {
542
+ return undefined
543
+ }
544
+
545
+ const onCopy = (event: ClipboardEvent) => {
546
+ const selection = window.getSelection()
547
+
548
+ if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
549
+ return
550
+ }
551
+
552
+ const fragment = selection.getRangeAt(0).cloneContents()
553
+ const maths = fragment.querySelectorAll("[data-tex]")
554
+
555
+ if (maths.length === 0) {
556
+ return
557
+ }
558
+
559
+ for (const element of Array.from(maths)) {
560
+ element.replaceWith(document.createTextNode(element.getAttribute("data-tex") ?? ""))
561
+ }
562
+
563
+ event.clipboardData?.setData("text/plain", fragment.textContent ?? "")
564
+ event.preventDefault()
565
+ }
566
+
567
+ document.addEventListener("copy", onCopy)
568
+
569
+ return () => document.removeEventListener("copy", onCopy)
570
+ }, [drawn])
571
+
572
+ // On the host rather than on each link: the markup underneath is rebuilt from
573
+ // scratch whenever the text changes, and a listener per element would have to
574
+ // be attached again every time.
575
+ useClientEffect(() => {
576
+ const host = hostRef.current
577
+
578
+ if (
579
+ !host ||
580
+ markdown === null ||
581
+ onLinkPress === undefined ||
582
+ markdown.links.length === 0
583
+ ) {
584
+ return undefined
585
+ }
586
+
587
+ const { links } = markdown
588
+
589
+ const linkAt = (target: EventTarget | null) => {
590
+ const element = target instanceof Element ? target.closest("[data-link]") : null
591
+
592
+ return element === null ? undefined : links[Number(element.getAttribute("data-link"))]
593
+ }
594
+
595
+ const press = (event: MouseEvent | KeyboardEvent) => {
596
+ const link = linkAt(event.target)
597
+
598
+ if (link === undefined) {
599
+ return
600
+ }
601
+
602
+ // A modified click belongs to the browser: on an anchor it opens the
603
+ // destination in a new tab or a new window, which is what the reader
604
+ // asked for and something no callback can do for them.
605
+ if (
606
+ "button" in event &&
607
+ (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
608
+ ) {
609
+ return
610
+ }
611
+
612
+ event.preventDefault()
613
+ onLinkPress(link.uri, link.label, link.title)
614
+ }
615
+
616
+ const onKeyDown = (event: KeyboardEvent) => {
617
+ // Enter and not Space: Space activates a button, and scrolls everywhere
618
+ // else, which is what a reader who has tabbed onto a link expects of it.
619
+ if (event.key === "Enter") {
620
+ press(event)
621
+ }
622
+ }
623
+
624
+ host.addEventListener("click", press as EventListener)
625
+ host.addEventListener("keydown", onKeyDown as EventListener)
626
+
627
+ return () => {
628
+ host.removeEventListener("click", press as EventListener)
629
+ host.removeEventListener("keydown", onKeyDown as EventListener)
630
+ }
631
+ }, [markdown, onLinkPress])
632
+
633
+ const setRef = React.useCallback(
634
+ (node: unknown) => {
635
+ hostRef.current = node as HTMLElement | null
636
+
637
+ if (typeof ref === "function") {
638
+ ref(node as MarkdownText)
639
+ } else if (ref) {
640
+ // eslint-disable-next-line no-param-reassign
641
+ ref.current = node as MarkdownText
642
+ }
643
+ },
644
+ [ref],
645
+ )
646
+
647
+ return (
648
+ <>
649
+ <Text {...textProps} ref={setRef as never}>
650
+ {markdown === null ? children : content}
651
+ </Text>
652
+ {slots.map((slot, index) => {
653
+ const content = drawn[index]
654
+
655
+ // The slots were made from this very `markdown`, so they line up -- but a render between a
656
+ // text change and the effect that follows it can still see the previous set, one entry
657
+ // longer than the new document has placeholders.
658
+ return content?.element == null
659
+ ? null
660
+ : createPortal(content.element, slot, String(index))
661
+ })}
662
+ </>
663
+ )
664
+ })
665
+
666
+ export type { MarkdownTextProps, OnLinkPress, RenderEmbed, RenderSVG }
667
+
668
+ export default MarkdownText