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
@@ -3,66 +3,145 @@ import type { ColorValue, ViewProps } from "react-native"
3
3
 
4
4
  import type { Float, Int32 } from "react-native/Libraries/Types/CodegenTypes"
5
5
 
6
- // Font, colour and background, and nothing else: neither formatter draws a
7
- // border or reserves padding around a code run, so there is nothing to
8
- // configure.
9
- interface CodeBlockStyle {
10
- fontFamily: string
11
- fontSize: Float
12
- color: ColorValue
13
- backgroundColor: ColorValue
6
+ // The text attributes one styled run can carry: the subset of React Native's
7
+ // `TextStyle` that survives as an `NSAttributedString` attribute on iOS, as a
8
+ // span on Android, and as a declaration on an inline element on the web.
9
+ //
10
+ // Everything here is optional and everything unset is inherited, which is what a
11
+ // run of markdown wants -- a mention that only names a colour keeps the face,
12
+ // the size and the weight of the prose it sits in. Native reads "unset" off the
13
+ // zero value: an empty string, a null colour, a zero length. That costs nothing,
14
+ // because a zero-length `letterSpacing`, `lineHeight` or `fontSize` means the
15
+ // same as leaving it out.
16
+ //
17
+ // `fontWeight`, `fontStyle`, `textDecorationLine` and `textDecorationStyle`
18
+ // arrive as plain strings rather than as unions. Codegen turns a union of string
19
+ // literals into an enum named after the prop it was found on, so the same
20
+ // spelling on twelve entries would become twelve unrelated C++ types and the one
21
+ // conversion template in `RCTMarcusStyle+Codegen.h` could not read any of them.
22
+ // The public type in `styleUtils` is the narrow one; this is the wire.
23
+ interface MarkdownTextStyle {
24
+ color?: ColorValue
25
+ fontFamily?: string
26
+ fontSize?: Float
27
+ fontStyle?: string
28
+ fontWeight?: string
29
+ letterSpacing?: Float
30
+ // Applies to whole lines: the platforms set it on the paragraph, so a range
31
+ // covering part of a line raises the height of all of it.
32
+ lineHeight?: Float
33
+ textDecorationLine?: string
34
+ // iOS and web only. Android draws an underline and a strikethrough in the text
35
+ // colour and offers no way to say otherwise.
36
+ textDecorationColor?: ColorValue
37
+ textDecorationStyle?: string
38
+ textShadowColor?: ColorValue
39
+ textShadowOffset?: { width: Float; height: Float }
40
+ textShadowRadius?: Float
41
+ }
42
+
43
+ // A run drawn as a box rather than as a background behind the glyphs: an inline
44
+ // or fenced run of code, or a mention's pill. Rounded, optionally bordered, with
45
+ // room inside it and around it, on top of whatever text attributes it carries.
46
+ //
47
+ // What the room holds open differs, because a block owns its lines and a run
48
+ // shares one. A block's padding and margin are held open on the leading edge and
49
+ // vertically, and a line long enough to wrap reaches the padding on the trailing
50
+ // edge rather than stopping in front of it: neither platform can reserve a
51
+ // trailing indent per paragraph. An inline run holds its padding and margin open
52
+ // on both sides, by widening the characters it starts and ends with, and grows
53
+ // into the line's own spacing above and below rather than pushing the lines
54
+ // apart.
55
+ //
56
+ // `padding` is the fallback for both axes; `paddingHorizontal` and
57
+ // `paddingVertical` win over it where they are set, and `margin` the same. A
58
+ // fenced block usually wants more room above and below than beside it, and one
59
+ // number cannot say that.
60
+ interface MarkdownBoxStyle extends MarkdownTextStyle {
61
+ backgroundColor?: ColorValue
62
+ borderColor?: ColorValue
63
+ borderWidth?: Float
64
+ borderRadius?: Float
65
+ padding?: Float
66
+ paddingHorizontal?: Float
67
+ paddingVertical?: Float
68
+ margin?: Float
69
+ marginHorizontal?: Float
70
+ marginVertical?: Float
71
+ }
72
+
73
+ interface MarkdownHeadingStyle extends MarkdownTextStyle {
74
+ // Each level down multiplies the one above, so level N is
75
+ // fontSize * scale^(N - 1). Six levels, matching HTML.
76
+ scale?: Float
77
+ }
78
+
79
+ // The bar beside a quoted block, the indent it opens, and the text inside it.
80
+ //
81
+ // `marginStart` and `paddingStart` rather than `marginLeft`/`paddingLeft`
82
+ // because the gutter opens on the line's leading edge, which is the right-hand
83
+ // side of a right-to-left paragraph. Both platforms already indent from the
84
+ // leading edge -- `NSParagraphStyle`'s head indent and Android's leading margin
85
+ // are both writing-direction relative -- so the names now say what was always
86
+ // drawn, and the web builder resolves them to `padding-inline-start`.
87
+ interface MarkdownBlockquoteStyle extends MarkdownTextStyle {
88
+ borderColor?: ColorValue
89
+ borderWidth?: Float
90
+ // Rounds the bar's ends. A bar whose radius is half its width comes out a
91
+ // capsule.
92
+ borderRadius?: Float
93
+ marginStart?: Float
94
+ paddingStart?: Float
95
+ }
96
+
97
+ // Indent, and the marker that sits in the gutter it opens. Kept as two separate
98
+ // entries so a renderer can style ordered and unordered lists differently, which
99
+ // is exactly what `markerScale` ends up meaning: an ordered item's number is
100
+ // text, an unordered item's bullet is a shape.
101
+ //
102
+ // Both markers are sized from the base font -- the size the wrapped `Text`
103
+ // renders at -- rather than from a size of their own, so a list marks itself in
104
+ // proportion to the prose it marks however that prose is sized. `marker` styles
105
+ // the marker itself, and what it leaves unset falls back to `syntax`, which is
106
+ // where a marker's colour came from before this entry existed.
107
+ //
108
+ // Only `MarkdownText` draws them. An input shows the marker you typed, in the
109
+ // base font, because it is text you are editing rather than a rendering of it.
110
+ interface MarkdownListStyle {
111
+ marginStart?: Float
112
+ paddingStart?: Float
113
+ // An ordered item's number is drawn at this fraction of the base font size; an
114
+ // unordered item's bullet takes it as a diameter, as a fraction of the same.
115
+ markerScale?: Float
116
+ // Room held open either side of the marker, between the gutter and the text.
117
+ markerPadding?: Float
118
+ marker?: MarkdownTextStyle
119
+ }
120
+
121
+ // Left as the two values it always was rather than folded into
122
+ // `MarkdownTextStyle`: an emoji run is swapped onto a font that has the glyph
123
+ // and sized to sit with the prose, and nothing else about it is worth saying.
124
+ interface MarkdownEmojiStyle {
125
+ fontSize?: Float
126
+ fontFamily?: string
14
127
  }
15
128
 
16
129
  interface MarkdownStyle {
17
- syntax: {
18
- color: ColorValue
19
- }
20
- emoji: {
21
- fontSize: Float
22
- fontFamily: string
23
- }
24
- link: {
25
- color: ColorValue
26
- }
27
- heading: {
28
- fontSize: Float
29
- // Each level down multiplies the one above, so level N is
30
- // fontSize * scale^(N - 1). Six levels, matching HTML.
31
- scale: Float
32
- }
33
- blockquote: {
34
- borderColor: ColorValue
35
- borderWidth: Float
36
- marginLeft: Float
37
- paddingLeft: Float
38
- }
39
- // Indent only, no gutter decoration. Kept as two separate entries so a
40
- // renderer can style ordered and unordered lists differently.
41
- orderedList: {
42
- marginLeft: Float
43
- paddingLeft: Float
44
- }
45
- unorderedList: {
46
- marginLeft: Float
47
- paddingLeft: Float
48
- }
49
- code: CodeBlockStyle
50
- pre: CodeBlockStyle
51
- mentionHere: {
52
- color: ColorValue
53
- backgroundColor: ColorValue
54
- borderRadius?: Float
55
- }
56
- mentionUser: {
57
- color: ColorValue
58
- backgroundColor: ColorValue
59
- borderRadius?: Float
60
- }
61
- mentionReport: {
62
- color: ColorValue
63
- backgroundColor: ColorValue
64
- borderRadius?: Float
65
- }
130
+ syntax: MarkdownTextStyle
131
+ emoji: MarkdownEmojiStyle
132
+ link: MarkdownTextStyle
133
+ strong: MarkdownTextStyle
134
+ emphasis: MarkdownTextStyle
135
+ strikethrough: MarkdownTextStyle
136
+ heading: MarkdownHeadingStyle
137
+ blockquote: MarkdownBlockquoteStyle
138
+ orderedList: MarkdownListStyle
139
+ unorderedList: MarkdownListStyle
140
+ code: MarkdownBoxStyle
141
+ pre: MarkdownBoxStyle
142
+ // A name with an `@` in front of it, drawn as a pill: the same inline box an
143
+ // inline run of code sits in, and so the same lengths.
144
+ mention: MarkdownBoxStyle
66
145
  }
67
146
 
68
147
  interface NativeProps extends ViewProps {
@@ -74,4 +153,12 @@ export default codegenNativeComponent<NativeProps>("MarcusTextInputDecoratorView
74
153
  interfaceOnly: true,
75
154
  })
76
155
 
77
- export type { MarkdownStyle }
156
+ export type {
157
+ MarkdownStyle,
158
+ MarkdownTextStyle,
159
+ MarkdownEmojiStyle,
160
+ MarkdownBoxStyle,
161
+ MarkdownHeadingStyle,
162
+ MarkdownBlockquoteStyle,
163
+ MarkdownListStyle,
164
+ }
@@ -0,0 +1,472 @@
1
+ import { StyleSheet, Text, View } from "react-native"
2
+ import React from "react"
3
+ import type { TextProps } from "react-native"
4
+
5
+ import MarcusTextDecoratorViewNativeComponent from "./MarcusTextDecoratorViewNativeComponent"
6
+ import type {
7
+ MarkdownRangeStruct,
8
+ MarkdownSubstitutionStruct,
9
+ } from "./MarcusTextDecoratorViewNativeComponent"
10
+ import { processMarkdownStyle } from "./styleUtils"
11
+ import type { PartialMarkdownStyle } from "./styleUtils"
12
+ import { stripSyntax } from "./stripUtils"
13
+ import type { StrippedMarkdown } from "./stripUtils"
14
+ import { atomsOf, describeAtoms, sourceOf } from "./atomUtils"
15
+ import type { Atom } from "./atomUtils"
16
+ import { available as mathAvailable, renderMath, speakMath } from "./math"
17
+ import { flattenText } from "./childrenUtils"
18
+ import { createParser } from "./parser"
19
+ import type { MarkdownRange } from "./commonTypes"
20
+
21
+ /**
22
+ * What to show in place of an image.
23
+ *
24
+ * Must return a single view with a definite width and height, or nothing: the
25
+ * text layout has to know how much room to leave before it can lay the line
26
+ * out, and markdown carries no dimensions. Anything that reports no size --
27
+ * a bare `Image` with a remote source, most obviously -- measures zero and
28
+ * disappears.
29
+ *
30
+ * The alt text is passed through rather than used: what it is good for, an
31
+ * accessibility label most of all, is a question only the caller can answer.
32
+ *
33
+ * `inline` says whether the image shares its line with anything else, which is
34
+ * usually what decides how big it should be: alone on a line it is a figure,
35
+ * and in the middle of a sentence it is a badge. A block marker does not count
36
+ * as company, and another image does.
37
+ *
38
+ * Every string is always a string: an image written without a title is handed
39
+ * an empty one rather than nothing, so a caller never has to tell the two
40
+ * apart.
41
+ */
42
+ type RenderEmbed = (
43
+ uri: string,
44
+ alt: string,
45
+ title: string,
46
+ inline: boolean,
47
+ ) => React.ReactElement | null | undefined
48
+
49
+ /**
50
+ * What to draw an SVG with.
51
+ *
52
+ * Handed a complete SVG document and the box it belongs in, and expected to
53
+ * return a view of exactly that size. There is deliberately nothing else in the
54
+ * signature: everything a picture needs -- the colour, the glyph outlines, the
55
+ * baseline it sits on -- is either inside the XML already or has been taken
56
+ * care of by the time this is called, so `<SvgXml xml={xml} width={width}
57
+ * height={height} />` is the whole of a correct implementation.
58
+ *
59
+ * It exists because a renderer for SVG is a dependency, and which one is not
60
+ * this library's business: `react-native-svg` on native, whatever you like on
61
+ * the web.
62
+ *
63
+ * Returning nothing leaves the drawing out, and the span it stood for still
64
+ * costs its one character either way.
65
+ */
66
+ type RenderSVG = (
67
+ xml: string,
68
+ width: number,
69
+ height: number,
70
+ ) => React.ReactElement | null | undefined
71
+
72
+ /**
73
+ * What to do when a link is pressed.
74
+ *
75
+ * The label is the text the link was rendered as, which is all a reader saw of
76
+ * it: the destination is not in the document any more. Reference links are not
77
+ * covered -- their destination is written somewhere else and is not resolved
78
+ * yet -- so they render as links and do nothing.
79
+ *
80
+ * Opening the URL is deliberately not done for you: what a link means is the
81
+ * application's to decide, and a message from a stranger is the last place to
82
+ * hand an arbitrary URL to `Linking.openURL` without looking at it first.
83
+ *
84
+ * Every string is always a string: a link written without a title is handed an
85
+ * empty one rather than nothing.
86
+ */
87
+ type OnLinkPress = (uri: string, label: string, title: string) => void
88
+
89
+ interface MarkdownTextProps extends TextProps {
90
+ markdownStyle?: PartialMarkdownStyle
91
+ /**
92
+ * GFM's strikethrough (`~~a~~`) and bare autolinks (`www.example.com`).
93
+ *
94
+ * @default false
95
+ */
96
+ gfm?: boolean
97
+ /**
98
+ * `@name` mentions, which no flavour of markdown has a notion of.
99
+ *
100
+ * @default false
101
+ */
102
+ mention?: boolean
103
+ /**
104
+ * LaTeX between double dollar signs: `$$x^2$$` inline, and the same on lines
105
+ * of its own for a block.
106
+ *
107
+ * @default false
108
+ */
109
+ math?: boolean
110
+ renderEmbed?: RenderEmbed
111
+ /**
112
+ * What to draw a maths span with. Nothing is drawn without it, and `$$x^2$$`
113
+ * is left on the screen as it was written.
114
+ */
115
+ renderSVG?: RenderSVG
116
+ /**
117
+ * The language a drawn equation is read out in, as the speech engine names
118
+ * it: `af`, `ca`, `da`, `de`, `en`, `es`, `fr`, `hi`, `it`, `ko`, `nb`, `nn`
119
+ * or `sv`.
120
+ *
121
+ * What a document is written in and what its reader speaks are two different
122
+ * questions, and only the application knows the answer to the second, so this
123
+ * is usually whatever the rest of the interface is translated into. It
124
+ * changes only how the maths is *said*: the prose around it is read in the
125
+ * language it was written in, and the drawing itself is the same picture in
126
+ * every language.
127
+ *
128
+ * The rules for one language are around 300KB, so only the ones named in
129
+ * `withMath(config, { locales })` are in the bundle. A language that is not
130
+ * is read in English, with a warning in the console saying which line of
131
+ * Metro config would fix it.
132
+ *
133
+ * @default "en"
134
+ */
135
+ mathAccessibilityLocale?: string
136
+ onLinkPress?: OnLinkPress
137
+ }
138
+
139
+ type MarkdownText = Text & React.Component<MarkdownTextProps>
140
+
141
+ /**
142
+ * The stripped text with the embeds spliced back in and the links wrapped, as
143
+ * the children of one `Text`.
144
+ *
145
+ * A view nested in a `Text` is one character to the platform -- iOS and Android
146
+ * both put a single U+FFFC in the string their formatters index -- and it is
147
+ * the very character the strip left behind, so the ranges need no adjusting and
148
+ * an embed inside a blockquote is laid out inside the quote's indent by the
149
+ * same code that indents the prose around it.
150
+ *
151
+ * A nested `Text` costs no characters at all: its content goes into that same
152
+ * string where it stands. So a link can be wrapped in one to be pressed without
153
+ * moving anything either, and the decorator styles it through the ranges the
154
+ * way it styles a link nobody can press.
155
+ */
156
+ function textChildren(
157
+ markdown: StrippedMarkdown,
158
+ renderEmbed: RenderEmbed | undefined,
159
+ renderSVG: RenderSVG | undefined,
160
+ fontSize: number,
161
+ color: string | undefined,
162
+ locale: string | undefined,
163
+ onLinkPress: React.RefObject<OnLinkPress | undefined>,
164
+ ) {
165
+ const { text, links } = markdown
166
+ const children: React.ReactNode[] = []
167
+ const atoms = atomsOf(markdown)
168
+ // Atoms are in text order and each is used once, whether it falls inside a
169
+ // link or between two, so one walk covers both.
170
+ let next = 0
171
+
172
+ /** What stands where one placeholder character is. */
173
+ function draw(atom: Atom) {
174
+ if (atom.kind === "embed") {
175
+ return renderEmbed?.(atom.uri, atom.alt, atom.title, atom.inline)
176
+ }
177
+
178
+ const drawn = renderMath(atom.tex, atom.display, fontSize, color)
179
+
180
+ if (drawn === null) {
181
+ return null
182
+ }
183
+
184
+ const element = renderSVG?.(drawn.svg, drawn.width, drawn.height)
185
+
186
+ if (!element) {
187
+ return null
188
+ }
189
+
190
+ // Wrapped rather than cloned: what came back is the caller's element and
191
+ // its own props are not ours to rewrite.
192
+ //
193
+ // The wrapper does two things. It carries the label -- what the picture
194
+ // says, read out the way a screen reader reads maths on the web, because a
195
+ // drawing is the one thing in a document that cannot be read for itself.
196
+ // And it shifts the picture down by its depth -- both platforms put the
197
+ // *bottom* edge of a view mounted in a `Text` on the baseline, so without
198
+ // this a fraction floats above the line. The shift happens after
199
+ // measurement, so the line keeps the height the maths asked for.
200
+ return (
201
+ <View
202
+ accessible
203
+ accessibilityLabel={speakMath(atom.tex, atom.display, locale) ?? atom.tex}
204
+ style={drawn.depth ? { transform: [{ translateY: drawn.depth }] } : undefined}
205
+ >
206
+ {element}
207
+ </View>
208
+ )
209
+ }
210
+
211
+ /** The text between two offsets, with any atoms in it spliced in. */
212
+ function content(from: number, to: number) {
213
+ const between: React.ReactNode[] = []
214
+ let cursor = from
215
+
216
+ while (next < atoms.length && atoms[next]!.index < to) {
217
+ const atom = atoms[next]!
218
+
219
+ if (atom.index > cursor) {
220
+ between.push(text.slice(cursor, atom.index))
221
+ }
222
+
223
+ between.push(
224
+ <React.Fragment key={`atom-${atom.index}`}>
225
+ {/* Something has to stand in the placeholder's place even when the
226
+ caller declines to render anything, or every range after it would
227
+ be one character out. An empty view is the cheapest attachment
228
+ there is, and takes up as much room as the nothing it draws. */}
229
+ {draw(atom) ?? <View />}
230
+ </React.Fragment>,
231
+ )
232
+
233
+ cursor = atom.index + 1
234
+ next++
235
+ }
236
+
237
+ if (cursor < to) {
238
+ between.push(text.slice(cursor, to))
239
+ }
240
+
241
+ return between
242
+ }
243
+
244
+ let cursor = 0
245
+
246
+ for (const link of links) {
247
+ if (link.start > cursor) {
248
+ children.push(...content(cursor, link.start))
249
+ }
250
+
251
+ const end = link.start + link.length
252
+
253
+ children.push(
254
+ <Text
255
+ key={`link-${link.start}`}
256
+ role="link"
257
+ onPress={() => onLinkPress.current?.(link.uri, link.label, link.title)}
258
+ >
259
+ {content(link.start, end)}
260
+ </Text>,
261
+ )
262
+
263
+ cursor = end
264
+ }
265
+
266
+ children.push(...content(cursor, text.length))
267
+
268
+ return children
269
+ }
270
+
271
+ const EMPTY_RANGES: MarkdownRangeStruct[] = []
272
+ const NO_SUBSTITUTIONS: MarkdownSubstitutionStruct[] = []
273
+
274
+ /**
275
+ * What maths is sized against when the caller set no size at all, which is
276
+ * React Native's own default for a `Text`.
277
+ */
278
+ const DEFAULT_FONT_SIZE = 14
279
+
280
+ function toRangeStructs(ranges: MarkdownRange[]): MarkdownRangeStruct[] {
281
+ return ranges.map((range) => ({
282
+ type: range.type,
283
+ start: range.start,
284
+ length: range.length,
285
+ depth: range.depth ?? 0,
286
+ }))
287
+ }
288
+
289
+ /**
290
+ * `Text`, with the markdown in its content rendered rather than shown.
291
+ *
292
+ * The syntax is removed in JavaScript before the text ever reaches the shadow
293
+ * tree, so what gets measured, drawn, selected, copied and read out is the
294
+ * finished prose. What is left over -- which run is bold, where a quote starts
295
+ * -- is handed to the native decorator as ranges over that stripped string, and
296
+ * styled by the same formatter the input uses, so a message and the composer it
297
+ * was typed in agree by construction.
298
+ *
299
+ * List markers are the exception that stays: `1.` numbers an item and `-`
300
+ * stands in for a bullet, so they are laid out rather than removed.
301
+ */
302
+ const MarkdownText = React.forwardRef<MarkdownText, MarkdownTextProps>((props, ref) => {
303
+ const {
304
+ markdownStyle: markdownStyleProp,
305
+ gfm,
306
+ mention,
307
+ math,
308
+ renderEmbed,
309
+ renderSVG,
310
+ mathAccessibilityLocale,
311
+ onLinkPress,
312
+ children,
313
+ ...textProps
314
+ } = props
315
+
316
+ const markdownStyle = React.useMemo(
317
+ () => processMarkdownStyle(markdownStyleProp),
318
+ [markdownStyleProp],
319
+ )
320
+
321
+ // Whether there is a handler, not which one: a caller who writes the callback
322
+ // inline would otherwise re-parse the document on every render.
323
+ const withEmbeds = renderEmbed !== undefined
324
+ // Maths is only taken out of the text when there is both something to draw it
325
+ // with and a backend to typeset it -- see `./math`. Left in, `$$x^2$$` is
326
+ // shown as it was written, which beats an empty box where an equation should
327
+ // be and is the one hint an application gets that its Metro config is a line
328
+ // short.
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
+ // What the maths has to be sized against. Read off the style the caller gave
349
+ // this element, because that is the only size known here -- a size inherited
350
+ // from an enclosing `Text` is resolved by the platform, long after this.
351
+ const { fontSize, color } = React.useMemo(() => {
352
+ const flattened = StyleSheet.flatten(textProps.style)
353
+
354
+ return {
355
+ fontSize:
356
+ typeof flattened?.fontSize === "number" ? flattened.fontSize : DEFAULT_FONT_SIZE,
357
+ // MathJax paints in `currentColor`, and a view mounted in a `Text`
358
+ // inherits none of its text attributes on either platform, so the colour
359
+ // is painted into the SVG before it is handed over. Left unset, maths is
360
+ // drawn in whatever the SVG renderer makes of `currentColor` -- so a
361
+ // display that means to be readable in the dark should say what colour it
362
+ // is, which it had to do for its own text anyway.
363
+ color: typeof flattened?.color === "string" ? flattened.color : undefined,
364
+ }
365
+ }, [textProps.style])
366
+
367
+ // The handler as the caller last wrote it, kept beside the text rather than
368
+ // baked into it. Written inline -- which is how a handler that reaches for
369
+ // component state has to be written -- it is a different function on every
370
+ // render, and rebuilding the children for each press means rebuilding the
371
+ // string the platform lays out, on the main thread, for every press.
372
+ const pressHandler = React.useRef(onLinkPress)
373
+
374
+ React.useEffect(() => {
375
+ pressHandler.current = onLinkPress
376
+ }, [onLinkPress])
377
+
378
+ // Rebuilt when the document changes, or when a caller hands over a different
379
+ // renderer -- one that draws from state has to be able to draw again.
380
+ const spliced = React.useMemo(
381
+ () =>
382
+ markdown === null ||
383
+ (markdown.embeds.length === 0 &&
384
+ markdown.maths.length === 0 &&
385
+ markdown.links.length === 0)
386
+ ? null
387
+ : textChildren(
388
+ markdown,
389
+ renderEmbed,
390
+ renderSVG,
391
+ fontSize,
392
+ color,
393
+ mathAccessibilityLocale,
394
+ pressHandler,
395
+ ),
396
+ [markdown, renderEmbed, renderSVG, fontSize, color, mathAccessibilityLocale],
397
+ )
398
+
399
+ // What the paragraph reads as. A drawing is one U+FFFC in the string the
400
+ // platform lays out -- that is what a view nested in a `Text` costs -- and
401
+ // there is nothing in that character for a screen reader to say, so the whole
402
+ // paragraph is described instead with each placeholder written back out as
403
+ // the alt text or the spoken form of the equation it stands for. Only when
404
+ // the caller has not described it themselves, and only when there is
405
+ // something to describe.
406
+ const accessibilityLabel = React.useMemo(
407
+ () =>
408
+ textProps.accessibilityLabel ??
409
+ (markdown === null || (markdown.embeds.length === 0 && markdown.maths.length === 0)
410
+ ? undefined
411
+ : describeAtoms(markdown, speakMath, mathAccessibilityLocale)),
412
+ [markdown, textProps.accessibilityLabel, mathAccessibilityLocale],
413
+ )
414
+
415
+ // What each of those placeholders copies as, which is the markdown it was
416
+ // written as. Handed to the decorator rather than used here: the copy is the
417
+ // platform's, made from the string it laid out, and only the platform can
418
+ // catch it -- Android at the moment the action mode's copy is pressed, iOS
419
+ // when the paragraph is asked for its own `copy:`.
420
+ const substitutions = React.useMemo(
421
+ () =>
422
+ markdown === null || (markdown.embeds.length === 0 && markdown.maths.length === 0)
423
+ ? NO_SUBSTITUTIONS
424
+ : atomsOf(markdown).map((atom) => ({ index: atom.index, text: sourceOf(atom) })),
425
+ [markdown],
426
+ )
427
+
428
+ // The same array while the document is the same one: a new one is a new props
429
+ // object to the mounting layer, and every press that a caller answers with a
430
+ // `setState` would otherwise have the decorator format the paragraph again.
431
+ const ranges = React.useMemo(
432
+ () => (markdown === null ? EMPTY_RANGES : toRangeStructs(markdown.ranges)),
433
+ [markdown],
434
+ )
435
+
436
+ if (markdown === null) {
437
+ if (__DEV__) {
438
+ console.warn(
439
+ "[react-native-marcus] `MarkdownText` was given children other than text and is rendering them unformatted.",
440
+ )
441
+ }
442
+
443
+ return (
444
+ <Text {...textProps} ref={ref}>
445
+ {children}
446
+ </Text>
447
+ )
448
+ }
449
+
450
+ return (
451
+ <MarcusTextDecoratorViewNativeComponent
452
+ style={styles.displayContents}
453
+ markdownStyle={markdownStyle}
454
+ ranges={ranges}
455
+ substitutions={substitutions}
456
+ >
457
+ <Text {...textProps} accessibilityLabel={accessibilityLabel} ref={ref}>
458
+ {spliced ?? markdown.text}
459
+ </Text>
460
+ </MarcusTextDecoratorViewNativeComponent>
461
+ )
462
+ })
463
+
464
+ const styles = StyleSheet.create({
465
+ displayContents: {
466
+ display: "contents",
467
+ },
468
+ })
469
+
470
+ export type { MarkdownTextProps, OnLinkPress, RenderEmbed, RenderSVG }
471
+
472
+ export default MarkdownText