react-native-marcus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +227 -0
  3. package/RNMarcus.podspec +78 -0
  4. package/android/build.gradle +220 -0
  5. package/android/proguard-rules.pro +1 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/AndroidManifestNew.xml +2 -0
  8. package/android/src/main/cpp/CMakeLists.txt +41 -0
  9. package/android/src/main/cpp/MarkdownParser.cpp +49 -0
  10. package/android/src/main/cpp/MarkdownParser.h +56 -0
  11. package/android/src/main/cpp/OnLoad.cpp +17 -0
  12. package/android/src/main/java/app/getbullet/marcus/MarcusModule.kt +28 -0
  13. package/android/src/main/java/app/getbullet/marcus/MarcusPackage.kt +31 -0
  14. package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorView.kt +63 -0
  15. package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorViewManager.kt +32 -0
  16. package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorViewManagerSpec.kt +21 -0
  17. package/android/src/main/java/app/getbullet/marcus/MarkdownFormatter.kt +333 -0
  18. package/android/src/main/java/app/getbullet/marcus/MarkdownParseResult.kt +12 -0
  19. package/android/src/main/java/app/getbullet/marcus/MarkdownParser.kt +89 -0
  20. package/android/src/main/java/app/getbullet/marcus/MarkdownRange.kt +11 -0
  21. package/android/src/main/java/app/getbullet/marcus/MarkdownStyle.kt +81 -0
  22. package/android/src/main/java/app/getbullet/marcus/MarkdownTextWatcher.kt +17 -0
  23. package/android/src/main/java/app/getbullet/marcus/MarkdownUtils.kt +84 -0
  24. package/android/src/main/java/app/getbullet/marcus/ReactLineHeight.java +29 -0
  25. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundColorSpan.kt +6 -0
  26. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundSpan.kt +105 -0
  27. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBlockIndentSpan.kt +67 -0
  28. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBoldSpan.kt +6 -0
  29. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontFamilySpan.kt +28 -0
  30. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontSizeSpan.kt +7 -0
  31. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownForegroundColorSpan.kt +6 -0
  32. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownGapSpan.kt +57 -0
  33. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownItalicSpan.kt +6 -0
  34. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownLineHeightSpan.kt +21 -0
  35. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownSpan.kt +7 -0
  36. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownStrikethroughSpan.kt +5 -0
  37. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownUnderlineSpan.kt +5 -0
  38. package/android/src/main/new_arch/CMakeLists.txt +70 -0
  39. package/android/src/main/new_arch/RNMarcusSpec.cpp +43 -0
  40. package/android/src/main/new_arch/RNMarcusSpec.h +36 -0
  41. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorShadowNode.cpp +218 -0
  42. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorShadowNode.h +54 -0
  43. package/android/src/reactNativeVersionPatch/CustomMountingManager/81/app/getbullet/marcus/CustomFabricUIManager.java +156 -0
  44. package/android/src/reactNativeVersionPatch/CustomMountingManager/latest/app/getbullet/marcus/CustomFabricUIManager.java +130 -0
  45. package/android/src/test/java/app/getbullet/marcus/MarkdownFormatterTest.kt +205 -0
  46. package/android/src/test/java/app/getbullet/marcus/RenderModel.kt +78 -0
  47. package/android/src/test/resources/render-model.txt +583 -0
  48. package/apple/MarcusModule.h +8 -0
  49. package/apple/MarcusModule.mm +26 -0
  50. package/apple/MarcusRange.h +15 -0
  51. package/apple/MarcusRange.mm +15 -0
  52. package/apple/MarcusTextInputDecoratorComponentView.h +12 -0
  53. package/apple/MarcusTextInputDecoratorComponentView.mm +121 -0
  54. package/apple/MarcusTextInputDecoratorShadowNode.cpp +251 -0
  55. package/apple/MarcusTextInputDecoratorShadowNode.h +65 -0
  56. package/apple/MarcusTextInputDecoratorShadowNode.mm +204 -0
  57. package/apple/MarcusTextInputDecoratorViewManager.h +8 -0
  58. package/apple/MarcusTextInputDecoratorViewManager.mm +11 -0
  59. package/apple/MarkdownAdaptiveImageGlyph.swift +44 -0
  60. package/apple/MarkdownAttributes.h +31 -0
  61. package/apple/MarkdownAttributes.m +9 -0
  62. package/apple/MarkdownBackedTextInputDelegate.swift +117 -0
  63. package/apple/MarkdownDecorator.swift +316 -0
  64. package/apple/MarkdownFonts.swift +21 -0
  65. package/apple/MarkdownFormatter+React.swift +51 -0
  66. package/apple/MarkdownFormatter.swift +509 -0
  67. package/apple/MarkdownLog.h +10 -0
  68. package/apple/MarkdownLog.mm +6 -0
  69. package/apple/MarkdownParser.swift +200 -0
  70. package/apple/MarkdownSwiftInterop.h +23 -0
  71. package/apple/MarkdownTextBackground.swift +17 -0
  72. package/apple/MarkdownTextFieldObserver.swift +90 -0
  73. package/apple/MarkdownTextLayoutFragment.swift +205 -0
  74. package/apple/MarkdownTextLayoutManagerDelegate.swift +84 -0
  75. package/apple/MarkdownTextStorageDelegate.swift +32 -0
  76. package/apple/MarkdownTextViewObserver.swift +30 -0
  77. package/apple/RCTMarcusStyle+Codegen.h +18 -0
  78. package/apple/RCTMarcusStyle+Codegen.mm +64 -0
  79. package/apple/RCTMarcusStyle.h +41 -0
  80. package/apple/RCTMarcusStyle.m +5 -0
  81. package/apple/RCTMarcusUtils.h +26 -0
  82. package/apple/RCTMarcusUtils.mm +26 -0
  83. package/apple/RCTTextInput+AdaptiveImageGlyph.h +16 -0
  84. package/apple/RCTTextInput+AdaptiveImageGlyph.mm +79 -0
  85. package/apple/RCTTextInputComponentView+Marcus.h +11 -0
  86. package/apple/RCTTextInputComponentView+Marcus.mm +79 -0
  87. package/cpp/MarkdownCxx.modulemap +8 -0
  88. package/cpp/MarkdownGlobal.cpp +48 -0
  89. package/cpp/MarkdownGlobal.h +33 -0
  90. package/cpp/MarkdownWorkletParser.cpp +93 -0
  91. package/cpp/MarkdownWorkletParser.h +45 -0
  92. package/cpp/RuntimeDecorator.cpp +39 -0
  93. package/cpp/RuntimeDecorator.h +14 -0
  94. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorState.h +33 -0
  95. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorViewComponentDescriptor.h +17 -0
  96. package/lib/commonjs/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
  97. package/lib/commonjs/MarkdownTextInput.js +99 -0
  98. package/lib/commonjs/MarkdownTextInput.js.map +1 -0
  99. package/lib/commonjs/MarkdownTextInput.web.js +618 -0
  100. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -0
  101. package/lib/commonjs/NativeMarcusModule.js +9 -0
  102. package/lib/commonjs/NativeMarcusModule.js.map +1 -0
  103. package/lib/commonjs/commonTypes.js +2 -0
  104. package/lib/commonjs/commonTypes.js.map +1 -0
  105. package/lib/commonjs/global.d.js +2 -0
  106. package/lib/commonjs/global.d.js.map +1 -0
  107. package/lib/commonjs/index.js +27 -0
  108. package/lib/commonjs/index.js.map +1 -0
  109. package/lib/commonjs/package.json +1 -0
  110. package/lib/commonjs/parser/index.js +7030 -0
  111. package/lib/commonjs/parser/index.js.map +7 -0
  112. package/lib/commonjs/rangeUtils.js +71 -0
  113. package/lib/commonjs/rangeUtils.js.map +1 -0
  114. package/lib/commonjs/react-native-web.d.js +2 -0
  115. package/lib/commonjs/react-native-web.d.js.map +1 -0
  116. package/lib/commonjs/styleUtils.js +91 -0
  117. package/lib/commonjs/styleUtils.js.map +1 -0
  118. package/lib/commonjs/web/InputHistory.js +111 -0
  119. package/lib/commonjs/web/InputHistory.js.map +1 -0
  120. package/lib/commonjs/web/MarkdownTextInput.css +72 -0
  121. package/lib/commonjs/web/utils/blockLayout.js +132 -0
  122. package/lib/commonjs/web/utils/blockLayout.js.map +1 -0
  123. package/lib/commonjs/web/utils/blockUtils.js +219 -0
  124. package/lib/commonjs/web/utils/blockUtils.js.map +1 -0
  125. package/lib/commonjs/web/utils/browserUtils.js +40 -0
  126. package/lib/commonjs/web/utils/browserUtils.js.map +1 -0
  127. package/lib/commonjs/web/utils/cursorUtils.js +157 -0
  128. package/lib/commonjs/web/utils/cursorUtils.js.map +1 -0
  129. package/lib/commonjs/web/utils/inputUtils.js +152 -0
  130. package/lib/commonjs/web/utils/inputUtils.js.map +1 -0
  131. package/lib/commonjs/web/utils/measureUtils.js +0 -0
  132. package/lib/commonjs/web/utils/measureUtils.js.map +1 -0
  133. package/lib/commonjs/web/utils/parserUtils.js +552 -0
  134. package/lib/commonjs/web/utils/parserUtils.js.map +1 -0
  135. package/lib/commonjs/web/utils/treeUtils.js +109 -0
  136. package/lib/commonjs/web/utils/treeUtils.js.map +1 -0
  137. package/lib/commonjs/web/utils/webStyleUtils.js +59 -0
  138. package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -0
  139. package/lib/module/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
  140. package/lib/module/MarkdownTextInput.js +94 -0
  141. package/lib/module/MarkdownTextInput.js.map +1 -0
  142. package/lib/module/MarkdownTextInput.web.js +612 -0
  143. package/lib/module/MarkdownTextInput.web.js.map +1 -0
  144. package/lib/module/NativeMarcusModule.js +5 -0
  145. package/lib/module/NativeMarcusModule.js.map +1 -0
  146. package/lib/module/commonTypes.js +2 -0
  147. package/lib/module/commonTypes.js.map +1 -0
  148. package/lib/module/global.d.js +2 -0
  149. package/lib/module/global.d.js.map +1 -0
  150. package/lib/module/index.js +5 -0
  151. package/lib/module/index.js.map +1 -0
  152. package/lib/module/package.json +1 -0
  153. package/lib/module/parser/index.js +7029 -0
  154. package/lib/module/parser/index.js.map +7 -0
  155. package/lib/module/rangeUtils.js +66 -0
  156. package/lib/module/rangeUtils.js.map +1 -0
  157. package/lib/module/react-native-web.d.js +2 -0
  158. package/lib/module/react-native-web.d.js.map +1 -0
  159. package/lib/module/styleUtils.js +88 -0
  160. package/lib/module/styleUtils.js.map +1 -0
  161. package/lib/module/web/InputHistory.js +106 -0
  162. package/lib/module/web/InputHistory.js.map +1 -0
  163. package/lib/module/web/MarkdownTextInput.css +72 -0
  164. package/lib/module/web/utils/blockLayout.js +128 -0
  165. package/lib/module/web/utils/blockLayout.js.map +1 -0
  166. package/lib/module/web/utils/blockUtils.js +210 -0
  167. package/lib/module/web/utils/blockUtils.js.map +1 -0
  168. package/lib/module/web/utils/browserUtils.js +36 -0
  169. package/lib/module/web/utils/browserUtils.js.map +1 -0
  170. package/lib/module/web/utils/cursorUtils.js +150 -0
  171. package/lib/module/web/utils/cursorUtils.js.map +1 -0
  172. package/lib/module/web/utils/inputUtils.js +144 -0
  173. package/lib/module/web/utils/inputUtils.js.map +1 -0
  174. package/lib/module/web/utils/measureUtils.js +0 -0
  175. package/lib/module/web/utils/measureUtils.js.map +1 -0
  176. package/lib/module/web/utils/parserUtils.js +547 -0
  177. package/lib/module/web/utils/parserUtils.js.map +1 -0
  178. package/lib/module/web/utils/treeUtils.js +103 -0
  179. package/lib/module/web/utils/treeUtils.js.map +1 -0
  180. package/lib/module/web/utils/webStyleUtils.js +54 -0
  181. package/lib/module/web/utils/webStyleUtils.js.map +1 -0
  182. package/lib/typescript/commonjs/e2e/checks.test.d.ts +2 -0
  183. package/lib/typescript/commonjs/e2e/checks.test.d.ts.map +1 -0
  184. package/lib/typescript/commonjs/e2e/editing.test.d.ts +2 -0
  185. package/lib/typescript/commonjs/e2e/editing.test.d.ts.map +1 -0
  186. package/lib/typescript/commonjs/e2e/helpers.d.ts +23 -0
  187. package/lib/typescript/commonjs/e2e/helpers.d.ts.map +1 -0
  188. package/lib/typescript/commonjs/e2e/smoke.test.d.ts +2 -0
  189. package/lib/typescript/commonjs/e2e/smoke.test.d.ts.map +1 -0
  190. package/lib/typescript/commonjs/package.json +1 -0
  191. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +63 -0
  192. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
  193. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts +22 -0
  194. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts.map +1 -0
  195. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts +41 -0
  196. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts.map +1 -0
  197. package/lib/typescript/commonjs/src/NativeMarcusModule.d.ts +7 -0
  198. package/lib/typescript/commonjs/src/NativeMarcusModule.d.ts.map +1 -0
  199. package/lib/typescript/commonjs/src/commonTypes.d.ts +10 -0
  200. package/lib/typescript/commonjs/src/commonTypes.d.ts.map +1 -0
  201. package/lib/typescript/commonjs/src/index.d.ts +5 -0
  202. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  203. package/lib/typescript/commonjs/src/parser/index.d.ts +8 -0
  204. package/lib/typescript/commonjs/src/parser/index.d.ts.map +1 -0
  205. package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts +8 -0
  206. package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts.map +1 -0
  207. package/lib/typescript/commonjs/src/rangeUtils.d.ts +6 -0
  208. package/lib/typescript/commonjs/src/rangeUtils.d.ts.map +1 -0
  209. package/lib/typescript/commonjs/src/styleUtils.d.ts +8 -0
  210. package/lib/typescript/commonjs/src/styleUtils.d.ts.map +1 -0
  211. package/lib/typescript/commonjs/src/web/InputHistory.d.ts +24 -0
  212. package/lib/typescript/commonjs/src/web/InputHistory.d.ts.map +1 -0
  213. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts +41 -0
  214. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts.map +1 -0
  215. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts +33 -0
  216. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts.map +1 -0
  217. package/lib/typescript/commonjs/src/web/utils/browserUtils.d.ts +7 -0
  218. package/lib/typescript/commonjs/src/web/utils/browserUtils.d.ts.map +1 -0
  219. package/lib/typescript/commonjs/src/web/utils/cursorUtils.d.ts +12 -0
  220. package/lib/typescript/commonjs/src/web/utils/cursorUtils.d.ts.map +1 -0
  221. package/lib/typescript/commonjs/src/web/utils/inputUtils.d.ts +12 -0
  222. package/lib/typescript/commonjs/src/web/utils/inputUtils.d.ts.map +1 -0
  223. package/lib/typescript/commonjs/src/web/utils/measureUtils.d.ts +22 -0
  224. package/lib/typescript/commonjs/src/web/utils/measureUtils.d.ts.map +1 -0
  225. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts +44 -0
  226. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts.map +1 -0
  227. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts +25 -0
  228. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts.map +1 -0
  229. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts +7 -0
  230. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts.map +1 -0
  231. package/lib/typescript/module/e2e/checks.test.d.ts +2 -0
  232. package/lib/typescript/module/e2e/checks.test.d.ts.map +1 -0
  233. package/lib/typescript/module/e2e/editing.test.d.ts +2 -0
  234. package/lib/typescript/module/e2e/editing.test.d.ts.map +1 -0
  235. package/lib/typescript/module/e2e/helpers.d.ts +23 -0
  236. package/lib/typescript/module/e2e/helpers.d.ts.map +1 -0
  237. package/lib/typescript/module/e2e/smoke.test.d.ts +2 -0
  238. package/lib/typescript/module/e2e/smoke.test.d.ts.map +1 -0
  239. package/lib/typescript/module/package.json +1 -0
  240. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +63 -0
  241. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
  242. package/lib/typescript/module/src/MarkdownTextInput.d.ts +22 -0
  243. package/lib/typescript/module/src/MarkdownTextInput.d.ts.map +1 -0
  244. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts +41 -0
  245. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts.map +1 -0
  246. package/lib/typescript/module/src/NativeMarcusModule.d.ts +7 -0
  247. package/lib/typescript/module/src/NativeMarcusModule.d.ts.map +1 -0
  248. package/lib/typescript/module/src/commonTypes.d.ts +10 -0
  249. package/lib/typescript/module/src/commonTypes.d.ts.map +1 -0
  250. package/lib/typescript/module/src/index.d.ts +5 -0
  251. package/lib/typescript/module/src/index.d.ts.map +1 -0
  252. package/lib/typescript/module/src/parser/index.d.ts +8 -0
  253. package/lib/typescript/module/src/parser/index.d.ts.map +1 -0
  254. package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts +8 -0
  255. package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts.map +1 -0
  256. package/lib/typescript/module/src/rangeUtils.d.ts +6 -0
  257. package/lib/typescript/module/src/rangeUtils.d.ts.map +1 -0
  258. package/lib/typescript/module/src/styleUtils.d.ts +8 -0
  259. package/lib/typescript/module/src/styleUtils.d.ts.map +1 -0
  260. package/lib/typescript/module/src/web/InputHistory.d.ts +24 -0
  261. package/lib/typescript/module/src/web/InputHistory.d.ts.map +1 -0
  262. package/lib/typescript/module/src/web/utils/blockLayout.d.ts +41 -0
  263. package/lib/typescript/module/src/web/utils/blockLayout.d.ts.map +1 -0
  264. package/lib/typescript/module/src/web/utils/blockUtils.d.ts +33 -0
  265. package/lib/typescript/module/src/web/utils/blockUtils.d.ts.map +1 -0
  266. package/lib/typescript/module/src/web/utils/browserUtils.d.ts +7 -0
  267. package/lib/typescript/module/src/web/utils/browserUtils.d.ts.map +1 -0
  268. package/lib/typescript/module/src/web/utils/cursorUtils.d.ts +12 -0
  269. package/lib/typescript/module/src/web/utils/cursorUtils.d.ts.map +1 -0
  270. package/lib/typescript/module/src/web/utils/inputUtils.d.ts +12 -0
  271. package/lib/typescript/module/src/web/utils/inputUtils.d.ts.map +1 -0
  272. package/lib/typescript/module/src/web/utils/measureUtils.d.ts +22 -0
  273. package/lib/typescript/module/src/web/utils/measureUtils.d.ts.map +1 -0
  274. package/lib/typescript/module/src/web/utils/parserUtils.d.ts +44 -0
  275. package/lib/typescript/module/src/web/utils/parserUtils.d.ts.map +1 -0
  276. package/lib/typescript/module/src/web/utils/treeUtils.d.ts +25 -0
  277. package/lib/typescript/module/src/web/utils/treeUtils.d.ts.map +1 -0
  278. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts +7 -0
  279. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts.map +1 -0
  280. package/package.json +167 -0
  281. package/react-native.config.js +10 -0
  282. package/src/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
  283. package/src/MarkdownTextInput.tsx +139 -0
  284. package/src/MarkdownTextInput.web.tsx +958 -0
  285. package/src/NativeMarcusModule.ts +8 -0
  286. package/src/commonTypes.ts +30 -0
  287. package/src/global.d.ts +1 -0
  288. package/src/index.tsx +4 -0
  289. package/src/parser/index.ts +556 -0
  290. package/src/parser/micromark-extension-mentions.ts +82 -0
  291. package/src/rangeUtils.ts +68 -0
  292. package/src/react-native-web.d.ts +1 -0
  293. package/src/styleUtils.ts +100 -0
  294. package/src/web/InputHistory.ts +137 -0
  295. package/src/web/MarkdownTextInput.css +72 -0
  296. package/src/web/utils/blockLayout.ts +163 -0
  297. package/src/web/utils/blockUtils.ts +245 -0
  298. package/src/web/utils/browserUtils.ts +41 -0
  299. package/src/web/utils/cursorUtils.ts +204 -0
  300. package/src/web/utils/inputUtils.ts +206 -0
  301. package/src/web/utils/measureUtils.ts +0 -0
  302. package/src/web/utils/parserUtils.ts +813 -0
  303. package/src/web/utils/treeUtils.ts +149 -0
  304. package/src/web/utils/webStyleUtils.ts +69 -0
@@ -0,0 +1,200 @@
1
+ import CxxStdlib
2
+ import Foundation
3
+ import MarkdownCxx
4
+
5
+ /// Caches markdown ranges and keeps the parser off the main thread.
6
+ ///
7
+ /// Running the worklet waits on the markdown runtime. Waiting for it on the main
8
+ /// thread during a Yoga measure could outlast the watchdog, so callers on the
9
+ /// main thread use `cachedRanges(forText:withParserId:)` and fall back to
10
+ /// `warmCacheAsync(forText:withParserId:completion:)`.
11
+ @objc public final class MarkdownParser: NSObject {
12
+
13
+ /// One shared instance, so the view layer and the shadow node's measure pass
14
+ /// hit the same cache. With an instance each, every keystroke was parsed
15
+ /// twice: once when the text view reformatted, and again when the shadow node
16
+ /// measured and found its own cache empty.
17
+ @objc public static let sharedParser = MarkdownParser()
18
+
19
+ private struct CacheEntry {
20
+ let text: String
21
+ let parserId: Int
22
+ let ranges: [MarcusRange]
23
+ }
24
+
25
+ /// One entry is not enough once the main thread is restricted to cached
26
+ /// ranges: two texts alternating (typing then undoing, or an input echoing
27
+ /// older text back) evict each other and the main thread never finds what it
28
+ /// needs. The cache is also shared by every markdown input on screen, so it
29
+ /// needs room for a few distinct texts -- still short enough to scan linearly,
30
+ /// and entries only hold range objects.
31
+ private static let cacheCapacity = 8
32
+
33
+ /// Serial on purpose: the markdown runtime runs one parse at a time anyway,
34
+ /// and serialising here avoids doing the same work twice.
35
+ private static let warmupQueue = DispatchQueue(
36
+ label: "app.getbullet.marcus.parser-cache-warmup",
37
+ qos: .userInitiated
38
+ )
39
+
40
+ /// Guards everything below, and is never held across a parse. Holding a lock
41
+ /// while waiting on the markdown runtime is what let a background parse block
42
+ /// the main thread until iOS killed the app.
43
+ private let lock = NSLock()
44
+
45
+ /// Newest entry first.
46
+ private var cache: [CacheEntry] = []
47
+
48
+ private var pendingText: String?
49
+ private var pendingParserId: NSNumber?
50
+ private var pendingCompletion: (() -> Void)?
51
+ private var warmupScheduled = false
52
+
53
+ // MARK: - Cache
54
+
55
+ /// Returns the ranges for (text, parserId) only if they are already cached.
56
+ /// Never runs the parser, so it is safe on the main thread.
57
+ @objc public func cachedRanges(
58
+ forText text: String,
59
+ withParserId parserId: NSNumber
60
+ ) -> [MarcusRange]? {
61
+ lock.lock()
62
+ defer { lock.unlock() }
63
+
64
+ guard
65
+ let index = cache.firstIndex(where: {
66
+ $0.parserId == parserId.intValue && $0.text == text
67
+ })
68
+ else { return nil }
69
+
70
+ guard index != 0 else { return cache[0].ranges }
71
+
72
+ // Promote, so text asked for repeatedly is not the next evicted.
73
+ let entry = cache.remove(at: index)
74
+ cache.insert(entry, at: 0)
75
+ return entry.ranges
76
+ }
77
+
78
+ private func store(
79
+ _ ranges: [MarcusRange],
80
+ forText text: String,
81
+ parserId: NSNumber
82
+ ) {
83
+ lock.lock()
84
+ defer { lock.unlock() }
85
+
86
+ cache.removeAll { $0.parserId == parserId.intValue && $0.text == text }
87
+ cache.insert(
88
+ CacheEntry(text: text, parserId: parserId.intValue, ranges: ranges),
89
+ at: 0
90
+ )
91
+ if cache.count > Self.cacheCapacity {
92
+ cache.removeLast(cache.count - Self.cacheCapacity)
93
+ }
94
+ }
95
+
96
+ // MARK: - Parsing
97
+
98
+ /// Returns cached ranges when there are any, otherwise parses and caches.
99
+ /// Never call this from the main thread on a layout path.
100
+ @objc public func parse(_ text: String, withParserId parserId: NSNumber)
101
+ -> [MarcusRange]
102
+ {
103
+ if let cached = cachedRanges(forText: text, withParserId: parserId) {
104
+ return cached
105
+ }
106
+
107
+ // Deliberately parsed with no lock held. Two threads may parse the same text
108
+ // concurrently, which is harmless -- they produce the same result.
109
+ let ranges = parseUncached(text, parserId: parserId)
110
+ store(ranges, forText: text, parserId: parserId)
111
+ return ranges
112
+ }
113
+
114
+ private func parseUncached(_ text: String, parserId: NSNumber)
115
+ -> [MarcusRange]
116
+ {
117
+ // `utf16.count` is the unit the worklet reports range offsets in.
118
+ let result = bulletpoint.marcus.parseMarkdown(
119
+ std.string(text),
120
+ text.utf16.count,
121
+ Int32(parserId.intValue)
122
+ )
123
+
124
+ let schemaError = String(result.schemaError)
125
+ if !schemaError.isEmpty {
126
+ markdownLogWarn(
127
+ "[react-native-marcus] Incorrect schema of worklet parser output: \(schemaError)"
128
+ )
129
+ }
130
+
131
+ return result.ranges.map { range in
132
+ MarcusRange(
133
+ type: String(range.type),
134
+ range: NSRange(location: Int(range.start), length: Int(range.length)),
135
+ depth: UInt(range.depth)
136
+ )
137
+ }
138
+ }
139
+
140
+ // MARK: - Background warm-up
141
+
142
+ /// Parses on a background queue and caches the result, so the caller never
143
+ /// waits on the markdown runtime.
144
+ ///
145
+ /// Only the newest request survives: a later call replaces one that is still
146
+ /// queued. A parse already in flight cannot be cancelled, but the newest text
147
+ /// is picked up as soon as it finishes.
148
+ ///
149
+ /// `completion` runs on the background queue once the ranges are cached, and
150
+ /// is skipped when a newer request replaced this one -- that request reports
151
+ /// instead.
152
+ @objc public func warmCacheAsync(
153
+ forText text: String,
154
+ withParserId parserId: NSNumber,
155
+ completion: (() -> Void)?
156
+ ) {
157
+ lock.lock()
158
+ // Keep only the newest request, so stale text can never win over newer text.
159
+ // The replaced completion goes with it; the newer request reports instead.
160
+ pendingText = text
161
+ pendingParserId = parserId
162
+ pendingCompletion = completion
163
+ let alreadyScheduled = warmupScheduled
164
+ warmupScheduled = true
165
+ lock.unlock()
166
+
167
+ // A drain loop is already running and will pick this up.
168
+ guard !alreadyScheduled else { return }
169
+
170
+ Self.warmupQueue.async { [weak self] in
171
+ self?.drainPendingWarmups()
172
+ }
173
+ }
174
+
175
+ private func drainPendingWarmups() {
176
+ while true {
177
+ lock.lock()
178
+ guard let text = pendingText, let parserId = pendingParserId else {
179
+ warmupScheduled = false
180
+ lock.unlock()
181
+ return
182
+ }
183
+ let completion = pendingCompletion
184
+ pendingText = nil
185
+ pendingParserId = nil
186
+ pendingCompletion = nil
187
+ lock.unlock()
188
+
189
+ _ = parse(text, withParserId: parserId)
190
+
191
+ lock.lock()
192
+ let superseded = pendingText != nil
193
+ lock.unlock()
194
+
195
+ if !superseded {
196
+ completion?()
197
+ }
198
+ }
199
+ }
200
+ }
@@ -0,0 +1,23 @@
1
+ #pragma once
2
+
3
+ // Single entry point for Objective-C++ code that needs the Swift half of the
4
+ // module.
5
+ //
6
+ // The generated Swift interface header references Objective-C types from other
7
+ // modules without importing them, so those have to be in scope first. It also
8
+ // lands in a different place depending on how the pod is linked: inside the
9
+ // framework with use_frameworks!, and in the target's DerivedSources otherwise.
10
+
11
+ #import <React/RCTBackedTextInputDelegate.h>
12
+ #import <React/RCTUITextField.h>
13
+ #import <React/RCTUITextView.h>
14
+
15
+ #import <RNMarcus/MarcusRange.h>
16
+ #import <RNMarcus/RCTMarcusStyle.h>
17
+ #import <RNMarcus/RCTMarcusUtils.h>
18
+
19
+ #if __has_include(<RNMarcus/RNMarcus-Swift.h>)
20
+ #import <RNMarcus/RNMarcus-Swift.h>
21
+ #else
22
+ #import "RNMarcus-Swift.h"
23
+ #endif
@@ -0,0 +1,17 @@
1
+ import UIKit
2
+
3
+ /// The rounded background drawn behind a mention.
4
+ ///
5
+ /// Stored as an attributed-string attribute value under
6
+ /// `.marcusTextBackground`, so it stays an object rather than a struct.
7
+ final class MarkdownTextBackground: NSObject {
8
+ var color: UIColor = .clear
9
+ var borderRadius: CGFloat = 0
10
+ }
11
+
12
+ /// A `MarkdownTextBackground` paired with the range it applies to. Only ever a
13
+ /// local pairing while drawing, never stored in an attributed string.
14
+ struct MarkdownTextBackgroundWithRange {
15
+ let textBackground: MarkdownTextBackground
16
+ let range: NSRange
17
+ }
@@ -0,0 +1,90 @@
1
+ import React
2
+ import UIKit
3
+
4
+ /// Keeps a single-line text field formatted. Unlike the multiline case there is
5
+ /// no text storage to hook, so this reacts to control events and KVO instead and
6
+ /// rewrites `attributedText` in place.
7
+ @objc public final class MarkdownTextFieldObserver: NSObject {
8
+
9
+ // Strong on purpose -- see MarkdownTextStorageDelegate.
10
+ private let textField: RCTUITextField
11
+ private let markdownUtils: RCTMarcusUtils
12
+
13
+ /// Guards against the KVO observer re-entering while we assign `attributedText`.
14
+ private var active = true
15
+
16
+ @objc public init(textField: RCTUITextField, markdownUtils: RCTMarcusUtils)
17
+ {
18
+ self.textField = textField
19
+ self.markdownUtils = markdownUtils
20
+ super.init()
21
+ }
22
+
23
+ public override func observeValue(
24
+ forKeyPath keyPath: String?,
25
+ of object: Any?,
26
+ change: [NSKeyValueChangeKey: Any]?,
27
+ context: UnsafeMutableRawPointer?
28
+ ) {
29
+ guard active, keyPath == "text" || keyPath == "attributedText" else {
30
+ return
31
+ }
32
+ applyMarkdownFormatting()
33
+ }
34
+
35
+ @objc public func textFieldDidChange(_ textField: UITextField) {
36
+ applyMarkdownFormatting()
37
+ }
38
+
39
+ @objc public func textFieldDidEndEditing(_ textField: UITextField) {
40
+ // Stops iOS underlining the whole field on blur when the text ends with a
41
+ // link: `defaultTextAttributes` does not carry NSUnderline yet at this
42
+ // point, so pushing a fresh value forces it to be picked up. The setter
43
+ // compares deeply, so the value has to actually differ each time -- hence
44
+ // the counter.
45
+ var defaultTextAttributes = self.textField.defaultTextAttributes ?? [:]
46
+ defaultTextAttributes[Self.forceUpdateAttributeName] =
47
+ Self.nextForceUpdateToken()
48
+ self.textField.defaultTextAttributes = defaultTextAttributes
49
+
50
+ applyMarkdownFormatting()
51
+ }
52
+
53
+ private static let forceUpdateAttributeName = NSAttributedString.Key(
54
+ "RCTMarcusForceUpdate"
55
+ )
56
+ private static var forceUpdateCounter: UInt = 0
57
+
58
+ private static func nextForceUpdateToken() -> NSNumber {
59
+ defer { forceUpdateCounter &+= 1 }
60
+ return NSNumber(value: forceUpdateCounter)
61
+ }
62
+
63
+ private func applyMarkdownFormatting() {
64
+ // Skip during multi-stage input (e.g. Japanese kana conversion), otherwise
65
+ // rewriting the text breaks the input method's internal state.
66
+ guard textField.markedTextRange == nil else { return }
67
+
68
+ guard
69
+ let attributedText = textField.attributedText?.mutableCopy()
70
+ as? NSMutableAttributedString
71
+ else { return }
72
+
73
+ markdownUtils.applyMarkdownFormatting(
74
+ attributedText,
75
+ withDefaultTextAttributes: textField.defaultTextAttributes ?? [:]
76
+ )
77
+
78
+ let selectedTextRange = textField.selectedTextRange
79
+
80
+ active = false
81
+ textField.attributedText = attributedText
82
+ active = true
83
+
84
+ textField.setSelectedTextRange(selectedTextRange, notifyDelegate: false)
85
+
86
+ // Prevents underline flicker while typing when the previous text ends with
87
+ // a link.
88
+ textField.typingAttributes = textField.defaultTextAttributes
89
+ }
90
+ }
@@ -0,0 +1,205 @@
1
+ import UIKit
2
+
3
+ /// Draws the parts of markdown rendering that plain attributed-string attributes
4
+ /// can't express: blockquote ribbons down the left edge, and rounded background
5
+ /// pills behind mentions.
6
+ ///
7
+ /// Only constructed by `MarkdownTextLayoutManagerDelegate`, so it stays internal
8
+ /// to the Swift half of the module.
9
+ final class MarkdownTextLayoutFragment: NSTextLayoutFragment {
10
+
11
+ var markdownStyle: RCTMarcusStyle?
12
+ var depth: Int = 0
13
+ /// Distance from the paragraph's left edge to the blockquote's own box:
14
+ /// whatever containers nesting outside it indent by, plus the room left for a
15
+ /// list marker drawn in front of the ribbons.
16
+ var outerIndent: CGFloat = 0
17
+ var mentions: [MarkdownTextBackgroundWithRange] = []
18
+
19
+ // MARK: - NSTextLayoutFragment
20
+
21
+ override var renderingSurfaceBounds: CGRect {
22
+ guard depth > 0 else { return super.renderingSurfaceBounds }
23
+ return boundingRect.union(super.renderingSurfaceBounds)
24
+ }
25
+
26
+ override func draw(at point: CGPoint, in context: CGContext) {
27
+ guard !textLineFragments.isEmpty else {
28
+ super.draw(at: point, in: context)
29
+ return
30
+ }
31
+
32
+ drawBlockquoteRibbons()
33
+ drawMentions()
34
+
35
+ super.draw(at: point, in: context)
36
+ }
37
+
38
+ // MARK: - Custom elements
39
+
40
+ private func drawBlockquoteRibbons() {
41
+ guard depth > 0, let style = markdownStyle else { return }
42
+
43
+ let borderWidth = style.blockquoteBorderWidth
44
+ let shift =
45
+ style.blockquoteMarginLeft + borderWidth + style.blockquotePaddingLeft
46
+
47
+ style.blockquoteBorderColor.setFill()
48
+
49
+ let bounds = boundingRect
50
+ for level in 0..<depth {
51
+ let x = bounds.origin.x + CGFloat(level) * shift
52
+ UIRectFill(
53
+ CGRect(
54
+ x: x,
55
+ y: bounds.origin.y,
56
+ width: borderWidth,
57
+ height: bounds.size.height
58
+ )
59
+ )
60
+ }
61
+ }
62
+
63
+ private func drawMentions() {
64
+ guard !mentions.isEmpty else { return }
65
+
66
+ let isSingleline = textLineFragments.count == 1
67
+
68
+ // Carried across line fragments: mentions are ordered, so each line resumes
69
+ // scanning where the previous one left off rather than restarting.
70
+ var mentionIndex = 0
71
+
72
+ for lineFragment in textLineFragments {
73
+ let lineRange = lineFragment.characterRange
74
+ if lineRange.length == 0 { continue }
75
+
76
+ let lineBounds = lineFragment.typographicBounds
77
+ // Absolute index into the source string, not a fragment-relative offset.
78
+ let lineEndLocation = lineFragment.locationForCharacter(
79
+ at: NSMaxRange(lineRange)
80
+ )
81
+
82
+ while mentionIndex < mentions.count,
83
+ NSMaxRange(mentions[mentionIndex].range) <= lineRange.location
84
+ {
85
+ mentionIndex += 1
86
+ }
87
+
88
+ for i in mentionIndex..<mentions.count {
89
+ let mention = mentions[i]
90
+ if mention.range.location >= NSMaxRange(lineRange) { break }
91
+
92
+ // Soft wrapping can split a mention across line fragments, so only the
93
+ // part falling on this line is drawn here; the corners are rounded on
94
+ // whichever side actually terminates the mention.
95
+ let intersection = NSIntersectionRange(lineRange, mention.range)
96
+ let startLocation = lineFragment.locationForCharacter(
97
+ at: intersection.location
98
+ )
99
+
100
+ // Singleline: the mention starts off screen, nothing to draw. Only
101
+ // happens while the text input is unfocused.
102
+ if isSingleline, startLocation.x == 0, intersection.location > 0 {
103
+ continue
104
+ }
105
+
106
+ var endLocation = lineFragment.locationForCharacter(
107
+ at: intersection.location + intersection.length
108
+ )
109
+
110
+ // Singleline: the mention is only partially visible -- it either starts
111
+ // mid-line or at the very beginning of the line.
112
+ if isSingleline,
113
+ startLocation.x > endLocation.x
114
+ || (startLocation.x == endLocation.x && intersection.location == 0)
115
+ {
116
+ endLocation = lineEndLocation
117
+ }
118
+
119
+ guard
120
+ let font = lineFragment.attributedString.attribute(
121
+ .font,
122
+ at: intersection.location,
123
+ effectiveRange: nil
124
+ ) as? UIFont
125
+ else { continue }
126
+
127
+ let backgroundRect = CGRect(
128
+ x: lineBounds.origin.x + startLocation.x,
129
+ y: lineBounds.origin.y + startLocation.y - font.ascender,
130
+ width: endLocation.x - startLocation.x,
131
+ height: font.lineHeight
132
+ )
133
+
134
+ var corners: UIRectCorner = []
135
+ if intersection.location == mention.range.location {
136
+ corners.insert([.topLeft, .bottomLeft])
137
+ }
138
+ if NSMaxRange(intersection) == NSMaxRange(mention.range) {
139
+ corners.insert([.topRight, .bottomRight])
140
+ }
141
+
142
+ let radius = mention.textBackground.borderRadius
143
+ let path =
144
+ corners.isEmpty
145
+ ? UIBezierPath(rect: backgroundRect)
146
+ : UIBezierPath(
147
+ roundedRect: backgroundRect,
148
+ byRoundingCorners: corners,
149
+ cornerRadii: CGSize(width: radius, height: radius)
150
+ )
151
+
152
+ mention.textBackground.color.setFill()
153
+ path.fill()
154
+ }
155
+ }
156
+ }
157
+
158
+ // MARK: - Helpers
159
+
160
+ /// The strip to the left of the text that the blockquote ribbons occupy.
161
+ private var boundingRect: CGRect {
162
+ var bounds = CGRect.null
163
+ for lineFragment in textLineFragments
164
+ where lineFragment.characterRange.length != 0 {
165
+ bounds =
166
+ bounds.isNull
167
+ ? lineFragment.typographicBounds
168
+ : bounds.union(lineFragment.typographicBounds)
169
+ }
170
+
171
+ // A null rect has an infinite origin; offsetting it would yield an
172
+ // infinite rendering surface rather than an empty one.
173
+ guard !bounds.isNull, let style = markdownStyle else { return bounds }
174
+
175
+ let borderWidth = style.blockquoteBorderWidth
176
+ let shift =
177
+ style.blockquoteMarginLeft + borderWidth + style.blockquotePaddingLeft
178
+
179
+ // The ribbons sit at a fixed offset from the paragraph's own left edge,
180
+ // which is the text origin less however far this line is indented. Walking
181
+ // back from the text by the quote's own indent instead would move the bar
182
+ // whenever the line's indent changes -- a list nested inside the quote adds
183
+ // some, a marker drawn in front of the ribbon takes some away -- and split
184
+ // the bar between the lines of one quote.
185
+ let indent =
186
+ firstLineParagraphStyle?.firstLineHeadIndent ?? shift * CGFloat(depth)
187
+
188
+ bounds.origin.x -= indent - (outerIndent + style.blockquoteMarginLeft)
189
+ bounds.size.width = borderWidth + shift * CGFloat(depth - 1)
190
+ return bounds
191
+ }
192
+
193
+ /// Paragraph style of the fragment's first non-empty line.
194
+ private var firstLineParagraphStyle: NSParagraphStyle? {
195
+ for lineFragment in textLineFragments
196
+ where lineFragment.characterRange.length != 0 {
197
+ return lineFragment.attributedString.attribute(
198
+ .paragraphStyle,
199
+ at: lineFragment.characterRange.location,
200
+ effectiveRange: nil
201
+ ) as? NSParagraphStyle
202
+ }
203
+ return nil
204
+ }
205
+ }
@@ -0,0 +1,84 @@
1
+ import UIKit
2
+
3
+ /// Vends a `MarkdownTextLayoutFragment` for paragraphs that carry markdown
4
+ /// attributes needing custom drawing, and a plain fragment for everything else.
5
+ @objc
6
+ public final class MarkdownTextLayoutManagerDelegate: NSObject,
7
+ NSTextLayoutManagerDelegate
8
+ {
9
+
10
+ private let textStorage: NSTextStorage
11
+ private let markdownUtils: RCTMarcusUtils
12
+
13
+ @objc public init(textStorage: NSTextStorage, markdownUtils: RCTMarcusUtils)
14
+ {
15
+ self.textStorage = textStorage
16
+ self.markdownUtils = markdownUtils
17
+ super.init()
18
+ }
19
+
20
+ public func textLayoutManager(
21
+ _ textLayoutManager: NSTextLayoutManager,
22
+ textLayoutFragmentFor location: NSTextLocation,
23
+ in textElement: NSTextElement
24
+ ) -> NSTextLayoutFragment {
25
+ let plain = {
26
+ NSTextLayoutFragment(
27
+ textElement: textElement,
28
+ range: textElement.elementRange
29
+ )
30
+ }
31
+
32
+ let index = textLayoutManager.offset(
33
+ from: textLayoutManager.documentRange.location,
34
+ to: location
35
+ )
36
+ guard index < textStorage.length else { return plain() }
37
+
38
+ let depth =
39
+ textStorage.attribute(
40
+ .marcusBlockquoteDepth,
41
+ at: index,
42
+ effectiveRange: nil
43
+ ) as? NSNumber
44
+ let outerIndent =
45
+ textStorage.attribute(
46
+ .marcusBlockquoteIndent,
47
+ at: index,
48
+ effectiveRange: nil
49
+ ) as? NSNumber
50
+
51
+ guard let paragraph = textElement as? NSTextParagraph else {
52
+ return plain()
53
+ }
54
+ let attributedString = paragraph.attributedString
55
+
56
+ var mentions: [MarkdownTextBackgroundWithRange] = []
57
+ attributedString.enumerateAttribute(
58
+ .marcusTextBackground,
59
+ in: NSRange(location: 0, length: attributedString.length)
60
+ ) { value, range, _ in
61
+ guard let textBackground = value as? MarkdownTextBackground else {
62
+ return
63
+ }
64
+ mentions.append(
65
+ MarkdownTextBackgroundWithRange(
66
+ textBackground: textBackground,
67
+ range: range
68
+ )
69
+ )
70
+ }
71
+
72
+ guard depth != nil || !mentions.isEmpty else { return plain() }
73
+
74
+ let fragment = MarkdownTextLayoutFragment(
75
+ textElement: textElement,
76
+ range: textElement.elementRange
77
+ )
78
+ fragment.markdownStyle = markdownUtils.markdownStyle
79
+ fragment.depth = depth?.intValue ?? 0
80
+ fragment.outerIndent = CGFloat(outerIndent?.doubleValue ?? 0)
81
+ fragment.mentions = mentions
82
+ return fragment
83
+ }
84
+ }
@@ -0,0 +1,32 @@
1
+ import React
2
+ import UIKit
3
+
4
+ /// Reformats the text view's storage after every edit.
5
+ @objc
6
+ public final class MarkdownTextStorageDelegate: NSObject, NSTextStorageDelegate
7
+ {
8
+
9
+ // Strong on purpose, matching the Objective-C original. Making it weak would
10
+ // let the text view deallocate while this object is still installed as its
11
+ // storage delegate and while KVO observers are registered on it.
12
+ private let textView: RCTUITextView
13
+ private let markdownUtils: RCTMarcusUtils
14
+
15
+ @objc public init(textView: RCTUITextView, markdownUtils: RCTMarcusUtils) {
16
+ self.textView = textView
17
+ self.markdownUtils = markdownUtils
18
+ super.init()
19
+ }
20
+
21
+ public func textStorage(
22
+ _ textStorage: NSTextStorage,
23
+ didProcessEditing editedMask: NSTextStorage.EditActions,
24
+ range editedRange: NSRange,
25
+ changeInLength delta: Int
26
+ ) {
27
+ markdownUtils.applyMarkdownFormatting(
28
+ textStorage,
29
+ withDefaultTextAttributes: textView.defaultTextAttributes ?? [:]
30
+ )
31
+ }
32
+ }
@@ -0,0 +1,30 @@
1
+ import React
2
+ import UIKit
3
+
4
+ /// Re-applies formatting when the text view's default text attributes change,
5
+ /// which happens when style props such as colour or font size are updated.
6
+ @objc public final class MarkdownTextViewObserver: NSObject {
7
+
8
+ // Strong on purpose -- see MarkdownTextStorageDelegate.
9
+ private let textView: RCTUITextView
10
+ private let markdownUtils: RCTMarcusUtils
11
+
12
+ @objc public init(textView: RCTUITextView, markdownUtils: RCTMarcusUtils) {
13
+ self.textView = textView
14
+ self.markdownUtils = markdownUtils
15
+ super.init()
16
+ }
17
+
18
+ public override func observeValue(
19
+ forKeyPath keyPath: String?,
20
+ of object: Any?,
21
+ change: [NSKeyValueChangeKey: Any]?,
22
+ context: UnsafeMutableRawPointer?
23
+ ) {
24
+ guard keyPath == "defaultTextAttributes" else { return }
25
+ // Round-tripping through textStorage re-triggers the storage delegate,
26
+ // which is what actually re-applies the formatting.
27
+ guard let attributedText = textView.attributedText else { return }
28
+ textView.textStorage.setAttributedString(attributedText)
29
+ }
30
+ }