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,333 @@
1
+ package app.getbullet.marcus
2
+
3
+ import android.content.res.AssetManager
4
+ import android.os.Trace
5
+ import android.text.SpannableStringBuilder
6
+ import android.text.Spanned
7
+ import android.text.TextPaint
8
+ import android.text.style.MetricAffectingSpan
9
+ import app.getbullet.marcus.spans.MarkdownBackgroundColorSpan
10
+ import app.getbullet.marcus.spans.MarkdownBackgroundSpan
11
+ import app.getbullet.marcus.spans.MarkdownBlockIndentSpan
12
+ import app.getbullet.marcus.spans.MarkdownBoldSpan
13
+ import app.getbullet.marcus.spans.MarkdownFontFamilySpan
14
+ import app.getbullet.marcus.spans.MarkdownFontSizeSpan
15
+ import app.getbullet.marcus.spans.MarkdownForegroundColorSpan
16
+ import app.getbullet.marcus.spans.MarkdownGapSpan
17
+ import app.getbullet.marcus.spans.MarkdownItalicSpan
18
+ import app.getbullet.marcus.spans.MarkdownLineHeightSpan
19
+ import app.getbullet.marcus.spans.MarkdownSpan
20
+ import app.getbullet.marcus.spans.MarkdownStrikethroughSpan
21
+ import app.getbullet.marcus.spans.MarkdownUnderlineSpan
22
+ import com.facebook.react.uimanager.PixelUtil
23
+
24
+ class MarkdownFormatter(private val assetManager: AssetManager) {
25
+
26
+ fun format(
27
+ ssb: SpannableStringBuilder,
28
+ markdownRanges: List<MarkdownRange>,
29
+ markdownStyle: MarkdownStyle,
30
+ textPaint: TextPaint?
31
+ ) {
32
+ try {
33
+ Trace.beginSection("format")
34
+ removeSpans(ssb)
35
+ applyRanges(ssb, markdownRanges, markdownStyle, textPaint)
36
+ } finally {
37
+ Trace.endSection()
38
+ }
39
+ }
40
+
41
+ private fun removeSpans(ssb: SpannableStringBuilder) {
42
+ try {
43
+ Trace.beginSection("removeSpans")
44
+ // We shouldn't use `removeSpans()` because it also removes SpellcheckSpan, SuggestionSpan etc.
45
+ for (span in ssb.getSpans(0, ssb.length, MarkdownSpan::class.java)) {
46
+ ssb.removeSpan(span)
47
+ }
48
+ } finally {
49
+ Trace.endSection()
50
+ }
51
+ }
52
+
53
+ private fun applyRanges(
54
+ ssb: SpannableStringBuilder,
55
+ markdownRanges: List<MarkdownRange>,
56
+ markdownStyle: MarkdownStyle,
57
+ textPaint: TextPaint?
58
+ ) {
59
+ try {
60
+ Trace.beginSection("applyRanges")
61
+ // Containers arrive per line, outermost first, each preceded by the run of
62
+ // text its own marker takes up on that line.
63
+ val line = BlockLayout(markdownStyle, textPaint)
64
+ var prefix: MarkdownRange? = null
65
+
66
+ for (markdownRange in markdownRanges) {
67
+ if (markdownRange.type == "block-prefix") {
68
+ prefix = markdownRange
69
+ continue
70
+ }
71
+
72
+ val gutter = gutterOf(markdownRange, markdownStyle)
73
+
74
+ if (gutter != null) {
75
+ line.add(ssb, markdownRange, gutter, prefix)
76
+ prefix = null
77
+ continue
78
+ }
79
+
80
+ applyRange(ssb, markdownRange, markdownStyle)
81
+ }
82
+ } finally {
83
+ Trace.endSection()
84
+ }
85
+ }
86
+
87
+ /** The gutter a container reserves for itself, or null if it is not one. */
88
+ private fun gutterOf(markdownRange: MarkdownRange, markdownStyle: MarkdownStyle): Float? {
89
+ val step = when (markdownRange.type) {
90
+ "blockquote" ->
91
+ markdownStyle.blockquoteMarginLeft +
92
+ markdownStyle.blockquoteBorderWidth +
93
+ markdownStyle.blockquotePaddingLeft
94
+
95
+ "list-ordered" ->
96
+ markdownStyle.orderedListMarginLeft + markdownStyle.orderedListPaddingLeft
97
+
98
+ "list-unordered" ->
99
+ markdownStyle.unorderedListMarginLeft + markdownStyle.unorderedListPaddingLeft
100
+
101
+ else -> return null
102
+ }
103
+
104
+ return PixelUtil.toPixelFromDIP(step) * markdownRange.depth
105
+ }
106
+
107
+ /**
108
+ * Places the containers of one line, left to right.
109
+ *
110
+ * Each container reserves a gutter and is then followed by its own marker,
111
+ * which is text and so has to be stepped over rather than reserved: the
112
+ * container nested inside it starts after the marker, not in front of it. That
113
+ * is what puts a quote's ribbons after a list bullet, and a list's indent
114
+ * after a quote's `>`.
115
+ *
116
+ * A line continuing a block carries no marker of its own, and reuses the one
117
+ * that opened it -- otherwise its text, and any ribbon beside it, would sit at
118
+ * a different offset than the line above and break the block in two.
119
+ */
120
+ private class BlockLayout(
121
+ private val markdownStyle: MarkdownStyle,
122
+ private val textPaint: TextPaint?
123
+ ) {
124
+ /** Scratch paint, reset from [textPaint] before each measurement. */
125
+ private val measurePaint = TextPaint()
126
+
127
+ /** Last marker seen for each container type, for the lines that continue it. */
128
+ private val markers = HashMap<String, MarkdownRange>()
129
+ private var lineStart = -1
130
+ /** Offset reached so far, from the line's own left edge. */
131
+ private var offset = 0f
132
+ /**
133
+ * Where the line's first marker begins, if it has one. The text starts
134
+ * there; everything past it is held open with padding instead.
135
+ */
136
+ private var textStart = -1f
137
+ /** Marker of the container placed most recently, if it is on this line. */
138
+ private var padded: MarkdownRange? = null
139
+ private var span: MarkdownBlockIndentSpan? = null
140
+
141
+ fun add(
142
+ ssb: SpannableStringBuilder,
143
+ markdownRange: MarkdownRange,
144
+ gutter: Float,
145
+ prefix: MarkdownRange?
146
+ ) {
147
+ if (markdownRange.start != lineStart) {
148
+ lineStart = markdownRange.start
149
+ offset = 0f
150
+ textStart = -1f
151
+ padded = null
152
+ span = MarkdownBlockIndentSpan(
153
+ markdownStyle.blockquoteBorderColor,
154
+ PixelUtil.toPixelFromDIP(markdownStyle.blockquoteBorderWidth),
155
+ PixelUtil.toPixelFromDIP(
156
+ markdownStyle.blockquoteMarginLeft +
157
+ markdownStyle.blockquoteBorderWidth +
158
+ markdownStyle.blockquotePaddingLeft
159
+ )
160
+ ).also {
161
+ ssb.setSpan(it, markdownRange.start, markdownRange.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
162
+ }
163
+ }
164
+
165
+ val indent = span ?: return
166
+
167
+ padded?.let {
168
+ ssb.setSpan(
169
+ MarkdownGapSpan(gutter, markdownStyle.syntaxColor),
170
+ it.end - 1,
171
+ it.end,
172
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
173
+ )
174
+ }
175
+
176
+ if (markdownRange.type == "blockquote") {
177
+ indent.ribbonOffset = offset + PixelUtil.toPixelFromDIP(markdownStyle.blockquoteMarginLeft)
178
+ indent.depth = markdownRange.depth
179
+ }
180
+
181
+ offset += gutter
182
+
183
+ if (prefix != null) {
184
+ markers[markdownRange.type] = prefix
185
+ if (textStart < 0f) {
186
+ textStart = offset
187
+ }
188
+ }
189
+
190
+ (prefix ?: markers[markdownRange.type])?.let {
191
+ offset += measure(ssb, it.start, it.end)
192
+ }
193
+
194
+ padded = prefix
195
+ indent.firstLineIndent = if (textStart >= 0f) textStart else offset
196
+ indent.indent = offset
197
+ }
198
+
199
+ /**
200
+ * Width a marker renders at.
201
+ *
202
+ * The view's paint carries the text size while the measure path has none and
203
+ * leaves it on the spannable, so the metric spans are folded in either way.
204
+ * Padding added after a marker is a replacement span, which contributes
205
+ * nothing here -- it belongs to the container that follows, not the marker.
206
+ */
207
+ private fun measure(ssb: SpannableStringBuilder, start: Int, end: Int): Float {
208
+ if (textPaint != null) {
209
+ measurePaint.set(textPaint)
210
+ } else {
211
+ measurePaint.reset()
212
+ }
213
+
214
+ for (span in ssb.getSpans(start, end, MetricAffectingSpan::class.java)) {
215
+ span.updateMeasureState(measurePaint)
216
+ }
217
+
218
+ return measurePaint.measureText(ssb, start, end)
219
+ }
220
+ }
221
+
222
+ private fun applyRange(
223
+ ssb: SpannableStringBuilder,
224
+ markdownRange: MarkdownRange,
225
+ markdownStyle: MarkdownStyle
226
+ ) {
227
+ val start = markdownRange.start
228
+ val end = markdownRange.end
229
+ when (markdownRange.type) {
230
+ "bold" -> setSpan(ssb, MarkdownBoldSpan(), start, end)
231
+
232
+ "italic" -> setSpan(ssb, MarkdownItalicSpan(), start, end)
233
+
234
+ "strikethrough" -> setSpan(ssb, MarkdownStrikethroughSpan(), start, end)
235
+
236
+ "emoji" -> {
237
+ setSpan(
238
+ ssb,
239
+ MarkdownFontFamilySpan(markdownStyle.emojiFontFamily, assetManager),
240
+ start,
241
+ end
242
+ )
243
+ setSpan(ssb, MarkdownFontSizeSpan(markdownStyle.emojiFontSize), start, end)
244
+ }
245
+
246
+ "mention-here" -> {
247
+ setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.mentionHereColor), start, end)
248
+ setSpan(
249
+ ssb,
250
+ MarkdownBackgroundSpan(
251
+ markdownStyle.mentionHereBackgroundColor,
252
+ markdownStyle.mentionHereBorderRadius,
253
+ start,
254
+ end
255
+ ),
256
+ start,
257
+ end
258
+ )
259
+ }
260
+
261
+ "mention-user" -> {
262
+ // TODO: change mention color when it mentions current user
263
+ setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.mentionUserColor), start, end)
264
+ setSpan(
265
+ ssb,
266
+ MarkdownBackgroundSpan(
267
+ markdownStyle.mentionUserBackgroundColor,
268
+ markdownStyle.mentionUserBorderRadius,
269
+ start,
270
+ end
271
+ ),
272
+ start,
273
+ end
274
+ )
275
+ }
276
+
277
+ "mention-report" -> {
278
+ setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.mentionReportColor), start, end)
279
+ setSpan(
280
+ ssb,
281
+ MarkdownBackgroundSpan(
282
+ markdownStyle.mentionReportBackgroundColor,
283
+ markdownStyle.mentionReportBorderRadius,
284
+ start,
285
+ end
286
+ ),
287
+ start,
288
+ end
289
+ )
290
+ }
291
+
292
+ "syntax" -> setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.syntaxColor), start, end)
293
+
294
+ "link" -> {
295
+ setSpan(ssb, MarkdownUnderlineSpan(), start, end)
296
+ setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.linkColor), start, end)
297
+ }
298
+
299
+ "code" -> {
300
+ setSpan(ssb, MarkdownFontFamilySpan(markdownStyle.codeFontFamily, assetManager), start, end)
301
+ setSpan(ssb, MarkdownFontSizeSpan(markdownStyle.codeFontSize), start, end)
302
+ setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.codeColor), start, end)
303
+ setSpan(ssb, MarkdownBackgroundColorSpan(markdownStyle.codeBackgroundColor), start, end)
304
+ }
305
+
306
+ "pre" -> {
307
+ setSpan(ssb, MarkdownFontFamilySpan(markdownStyle.preFontFamily, assetManager), start, end)
308
+ setSpan(ssb, MarkdownFontSizeSpan(markdownStyle.preFontSize), start, end)
309
+ setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.preColor), start, end)
310
+ setSpan(ssb, MarkdownBackgroundColorSpan(markdownStyle.preBackgroundColor), start, end)
311
+ }
312
+
313
+ "heading" -> {
314
+ setSpan(ssb, MarkdownBoldSpan(), start, end)
315
+ // Level N is the base size scaled N-1 times, so a single pair of style
316
+ // values covers all six.
317
+ var fontSize = markdownStyle.headingFontSize
318
+ repeat(maxOf(markdownRange.depth, 1) - 1) { fontSize *= markdownStyle.headingScale }
319
+ val lineHeight = ReactLineHeight.find(ssb)
320
+ if (lineHeight >= 0) {
321
+ setSpan(ssb, MarkdownLineHeightSpan(lineHeight * (fontSize / markdownStyle.headingFontSize) * 1.5f), start, end)
322
+ }
323
+ // NOTE: size span must be set after line height span to avoid height jumps
324
+ setSpan(ssb, MarkdownFontSizeSpan(fontSize), start, end)
325
+ }
326
+
327
+ }
328
+ }
329
+
330
+ private fun setSpan(ssb: SpannableStringBuilder, span: MarkdownSpan, start: Int, end: Int) {
331
+ ssb.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
332
+ }
333
+ }
@@ -0,0 +1,12 @@
1
+ package app.getbullet.marcus
2
+
3
+ /**
4
+ * What [MarkdownParser.nativeParse] hands back across JNI. Constructed from C++ via fbjni
5
+ * `newInstance`, so the constructor signature is load-bearing:
6
+ * `([Lapp/getbullet/marcus/MarkdownRange;Ljava/lang/String;)V`.
7
+ */
8
+ class MarkdownParseResult(
9
+ @JvmField val ranges: Array<MarkdownRange>,
10
+ /** Non-null when the worklet returned something that did not match the expected schema. */
11
+ @JvmField val schemaError: String?
12
+ )
@@ -0,0 +1,89 @@
1
+ package app.getbullet.marcus
2
+
3
+ import android.os.Trace
4
+ import app.getbullet.marcus.MarkdownParser.Companion.shared
5
+ import com.facebook.soloader.SoLoader
6
+
7
+ /**
8
+ * Runs the registered markdown parser worklet and caches its output.
9
+ *
10
+ * Use [shared]. Each markdown input has two independent callers, the decorator view's
11
+ * TextWatcher and CustomFabricUIManager's measureText, and one cache shared between them
12
+ * beats one cache each. Note this is not a 2x saving: tracing shows a keystroke produces
13
+ * four parse calls but only one worklet run, because they all ask for the same text.
14
+ *
15
+ * Knows nothing about ReactContext or logging: it outlives every reload, so holding a context
16
+ * would leak it, and reporting is left to the caller via [onSchemaError].
17
+ */
18
+ class MarkdownParser private constructor() {
19
+
20
+ private data class CacheKey(val text: String, val parserId: Int)
21
+
22
+ private val lock = Any()
23
+
24
+ private val cache =
25
+ object : LinkedHashMap<CacheKey, List<MarkdownRange>>(CACHE_CAPACITY, 0.75f, true) {
26
+ override fun removeEldestEntry(
27
+ eldest: MutableMap.MutableEntry<CacheKey, List<MarkdownRange>>
28
+ ): Boolean = size > CACHE_CAPACITY
29
+ }
30
+
31
+ // Registered from C++ by JMarkdownParser::registerNatives in MarkdownParser.cpp. Must stay
32
+ // an instance method to match the `alias_ref<JMarkdownParser>` first parameter there.
33
+ private external fun nativeParse(text: String, parserId: Int): MarkdownParseResult
34
+
35
+ fun parse(
36
+ text: String,
37
+ parserId: Int,
38
+ onSchemaError: (String) -> Unit
39
+ ): List<MarkdownRange> {
40
+ try {
41
+ Trace.beginSection("parse")
42
+
43
+ val key = CacheKey(text, parserId)
44
+ synchronized(lock) { cache[key] }?.let { return it }
45
+
46
+ // Deliberately outside the lock. nativeParse waits on the markdown worklet runtime,
47
+ // and holding a lock across that wait is what let a background parse block the main
48
+ // thread until iOS killed the app (Expensify/react-native-live-markdown#772). Android
49
+ // has no watchdog to make it fatal, but it is the same defect.
50
+ val ranges = parseUncached(text, parserId, onSchemaError)
51
+
52
+ synchronized(lock) { cache[key] = ranges }
53
+ return ranges
54
+ } finally {
55
+ Trace.endSection()
56
+ }
57
+ }
58
+
59
+ private fun parseUncached(
60
+ text: String,
61
+ parserId: Int,
62
+ onSchemaError: (String) -> Unit
63
+ ): List<MarkdownRange> {
64
+ val result = try {
65
+ Trace.beginSection("nativeParse")
66
+ nativeParse(text, parserId)
67
+ } catch (e: Exception) {
68
+ // Skip formatting, runGuarded will show the error in LogBox
69
+ return emptyList()
70
+ } finally {
71
+ Trace.endSection()
72
+ }
73
+
74
+ result.schemaError?.let(onSchemaError)
75
+
76
+ return result.ranges.asList()
77
+ }
78
+
79
+ companion object {
80
+ private const val CACHE_CAPACITY = 8
81
+
82
+ init {
83
+ SoLoader.loadLibrary("marcus")
84
+ }
85
+
86
+ @JvmStatic
87
+ val shared: MarkdownParser = MarkdownParser()
88
+ }
89
+ }
@@ -0,0 +1,11 @@
1
+ package app.getbullet.marcus
2
+
3
+ data class MarkdownRange(
4
+ val type: String,
5
+ val start: Int,
6
+ val length: Int,
7
+ val depth: Int
8
+ ) {
9
+ val end: Int
10
+ get() = start + length
11
+ }
@@ -0,0 +1,81 @@
1
+ package app.getbullet.marcus
2
+
3
+ import android.content.Context
4
+ import androidx.annotation.ColorInt
5
+ import com.facebook.react.bridge.ColorPropConverter
6
+ import com.facebook.react.bridge.JSApplicationCausedNativeException
7
+ import com.facebook.react.bridge.ReadableMap
8
+ import com.facebook.react.bridge.ReadableType
9
+
10
+ class MarkdownStyle(map: ReadableMap, context: Context) {
11
+ private val screenDensity = context.resources.displayMetrics.density
12
+
13
+ @ColorInt val syntaxColor = parseColor(map, "syntax", "color", context)
14
+ @ColorInt val linkColor = parseColor(map, "link", "color", context)
15
+
16
+ val headingFontSize = parseFloat(map, "heading", "fontSize")
17
+
18
+ val headingScale = parseFloat(map, "heading", "scale")
19
+
20
+ val emojiFontSize = parseFloat(map, "emoji", "fontSize")
21
+ val emojiFontFamily = parseString(map, "emoji", "fontFamily")
22
+
23
+ @ColorInt val blockquoteBorderColor = parseColor(map, "blockquote", "borderColor", context)
24
+ val blockquoteBorderWidth = parseFloat(map, "blockquote", "borderWidth")
25
+ val blockquoteMarginLeft = parseFloat(map, "blockquote", "marginLeft")
26
+ val blockquotePaddingLeft = parseFloat(map, "blockquote", "paddingLeft")
27
+
28
+ val orderedListMarginLeft = parseFloat(map, "orderedList", "marginLeft")
29
+ val orderedListPaddingLeft = parseFloat(map, "orderedList", "paddingLeft")
30
+
31
+ val unorderedListMarginLeft = parseFloat(map, "unorderedList", "marginLeft")
32
+ val unorderedListPaddingLeft = parseFloat(map, "unorderedList", "paddingLeft")
33
+
34
+ val codeFontFamily = parseString(map, "code", "fontFamily")
35
+ val codeFontSize = parseFloat(map, "code", "fontSize")
36
+ @ColorInt val codeColor = parseColor(map, "code", "color", context)
37
+ @ColorInt val codeBackgroundColor = parseColor(map, "code", "backgroundColor", context)
38
+
39
+ val preFontFamily = parseString(map, "pre", "fontFamily")
40
+ val preFontSize = parseFloat(map, "pre", "fontSize")
41
+ @ColorInt val preColor = parseColor(map, "pre", "color", context)
42
+ @ColorInt val preBackgroundColor = parseColor(map, "pre", "backgroundColor", context)
43
+
44
+ @ColorInt val mentionHereColor = parseColor(map, "mentionHere", "color", context)
45
+ @ColorInt val mentionHereBackgroundColor = parseColor(map, "mentionHere", "backgroundColor", context)
46
+ val mentionHereBorderRadius = parseFloat(map, "mentionHere", "borderRadius") * screenDensity
47
+
48
+ @ColorInt val mentionUserColor = parseColor(map, "mentionUser", "color", context)
49
+ @ColorInt val mentionUserBackgroundColor = parseColor(map, "mentionUser", "backgroundColor", context)
50
+ val mentionUserBorderRadius = parseFloat(map, "mentionUser", "borderRadius") * screenDensity
51
+
52
+ @ColorInt val mentionReportColor = parseColor(map, "mentionReport", "color", context)
53
+ @ColorInt val mentionReportBackgroundColor = parseColor(map, "mentionReport", "backgroundColor", context)
54
+ val mentionReportBorderRadius = parseFloat(map, "mentionReport", "borderRadius") * screenDensity
55
+
56
+ private companion object {
57
+ fun style(map: ReadableMap, key: String): ReadableMap =
58
+ requireNotNull(map.getMap(key)) { "[react-native-marcus] markdownStyle.$key is missing" }
59
+
60
+ @ColorInt
61
+ fun parseColor(map: ReadableMap, key: String, prop: String, context: Context): Int {
62
+ val value = style(map, key).getDynamic(prop)
63
+ val color = when (value.type) {
64
+ ReadableType.Number -> ColorPropConverter.getColor(value.asDouble(), context)
65
+ ReadableType.Map -> ColorPropConverter.getColor(value.asMap(), context)
66
+ else -> throw JSApplicationCausedNativeException("ColorValue: the value must be a number or Object.")
67
+ }
68
+ return requireNotNull(color) {
69
+ "[react-native-marcus] markdownStyle.$key.$prop could not be resolved to a color"
70
+ }
71
+ }
72
+
73
+ fun parseFloat(map: ReadableMap, key: String, prop: String): Float =
74
+ style(map, key).getDouble(prop).toFloat()
75
+
76
+ fun parseString(map: ReadableMap, key: String, prop: String): String =
77
+ requireNotNull(style(map, key).getString(prop)) {
78
+ "[react-native-marcus] markdownStyle.$key.$prop is missing"
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,17 @@
1
+ package app.getbullet.marcus
2
+
3
+ import android.text.Editable
4
+ import android.text.SpannableStringBuilder
5
+ import android.text.TextWatcher
6
+
7
+ class MarkdownTextWatcher(private val markdownUtils: MarkdownUtils) : TextWatcher {
8
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
9
+
10
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
11
+
12
+ override fun afterTextChanged(editable: Editable) {
13
+ if (editable is SpannableStringBuilder) {
14
+ markdownUtils.applyMarkdownFormatting(editable)
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,84 @@
1
+ package app.getbullet.marcus
2
+
3
+ import android.os.Trace
4
+ import android.text.SpannableStringBuilder
5
+ import android.text.Spanned
6
+ import android.text.TextPaint
7
+ import android.text.TextUtils
8
+ import com.facebook.react.bridge.ReactContext
9
+ import com.facebook.react.util.RNLog
10
+
11
+ /**
12
+ * @param textPaint the view's own paint, held live rather than copied: the
13
+ * formatter measures block markers with it to work out where the containers
14
+ * nested inside them start, and has to see the size the text is drawn at.
15
+ * Null on the measure path, which has no view yet and carries the size on the
16
+ * spannable instead.
17
+ */
18
+ class MarkdownUtils @JvmOverloads constructor(
19
+ private val reactContext: ReactContext,
20
+ private val textPaint: TextPaint? = null
21
+ ) {
22
+ private val markdownFormatter = MarkdownFormatter(reactContext.assets)
23
+
24
+ // Hoisted rather than built per call: applyMarkdownFormatting runs several times per
25
+ // keystroke, and this is only ever needed for a warning that normally never fires.
26
+ private val onSchemaError: (String) -> Unit = { error ->
27
+ RNLog.w(reactContext, "[react-native-marcus] Incorrect schema of worklet parser output: $error")
28
+ }
29
+
30
+ var markdownStyle: MarkdownStyle? = null
31
+ var parserId: Int = 0
32
+
33
+ /**
34
+ * Records what the markdown spans currently on a spannable were built from.
35
+ *
36
+ * Held as a span rather than a field so that it is invalidated by exactly the things that
37
+ * invalidate the formatting itself. Anything that replaces the spannable's content drops
38
+ * this along with the markdown spans, and the next pass rebuilds both. A field would
39
+ * survive that and we would skip a pass that was actually needed.
40
+ */
41
+ private class Fingerprint(val text: String, val parserId: Int, val style: MarkdownStyle)
42
+
43
+ fun applyMarkdownFormatting(ssb: SpannableStringBuilder) {
44
+ try {
45
+ Trace.beginSection("applyMarkdownFormatting")
46
+ val style = checkNotNull(markdownStyle) { "[react-native-marcus] markdownStyle is null" }
47
+
48
+ // A single keystroke drives four passes: two from the edit itself and two from React
49
+ // Native's state round-trip. They all ask for the same text, so only the first has any
50
+ // work to do.
51
+ if (isUpToDate(ssb, style)) {
52
+ return
53
+ }
54
+
55
+ val text = ssb.toString()
56
+ val markdownRanges = MarkdownParser.shared.parse(text, parserId, onSchemaError)
57
+ markdownFormatter.format(ssb, markdownRanges, style, textPaint)
58
+ stamp(ssb, text, style)
59
+ } finally {
60
+ Trace.endSection()
61
+ }
62
+ }
63
+
64
+ private fun isUpToDate(ssb: SpannableStringBuilder, style: MarkdownStyle): Boolean {
65
+ val fingerprint =
66
+ ssb.getSpans(0, ssb.length, Fingerprint::class.java).singleOrNull() ?: return false
67
+ return fingerprint.parserId == parserId &&
68
+ // Identity, not equality: MarkdownStyle is rebuilt whenever the prop arrives, so a
69
+ // different instance means the style may have changed and we re-apply to be safe.
70
+ fingerprint.style === style &&
71
+ // Compares the CharSequence in place. ssb.toString() would copy the whole document on
72
+ // a path whose entire purpose is to do no work.
73
+ TextUtils.equals(ssb, fingerprint.text)
74
+ }
75
+
76
+ private fun stamp(ssb: SpannableStringBuilder, text: String, style: MarkdownStyle) {
77
+ // Fingerprint is not a MarkdownSpan, so MarkdownFormatter.removeSpans leaves it alone
78
+ // and stale ones have to be cleared here or they would accumulate.
79
+ for (stale in ssb.getSpans(0, ssb.length, Fingerprint::class.java)) {
80
+ ssb.removeSpan(stale)
81
+ }
82
+ ssb.setSpan(Fingerprint(text, parserId, style), 0, ssb.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
83
+ }
84
+ }
@@ -0,0 +1,29 @@
1
+ package app.getbullet.marcus;
2
+
3
+ import android.text.Spanned;
4
+
5
+ import com.facebook.react.views.text.internal.span.CustomLineHeightSpan;
6
+
7
+ /**
8
+ * React Native's {@code CustomLineHeightSpan} is declared {@code internal} in Kotlin. It stays
9
+ * public in the bytecode, so Java can still reach it, but the Kotlin compiler refuses to. This
10
+ * shim keeps that one lookup in Java.
11
+ *
12
+ * <p>The alternatives were worse: reflection breaks silently under R8, and deriving the height by
13
+ * probing the public {@code LineHeightSpan.chooseHeight} couples us to React Native's internal
14
+ * metrics maths, which would mis-space h1 lines without failing. If React Native ever renames or
15
+ * removes the class, this file stops compiling, which is the failure mode we want.
16
+ */
17
+ public final class ReactLineHeight {
18
+ private ReactLineHeight() {
19
+ }
20
+
21
+ /**
22
+ * The line height React Native applied to {@code spanned}, or -1 if it applied none.
23
+ */
24
+ public static int find(Spanned spanned) {
25
+ CustomLineHeightSpan[] spans =
26
+ spanned.getSpans(0, spanned.length(), CustomLineHeightSpan.class);
27
+ return spans.length >= 1 ? spans[0].getLineHeight() : -1;
28
+ }
29
+ }
@@ -0,0 +1,6 @@
1
+ package app.getbullet.marcus.spans
2
+
3
+ import android.text.style.BackgroundColorSpan
4
+ import androidx.annotation.ColorInt
5
+
6
+ class MarkdownBackgroundColorSpan(@ColorInt color: Int) : BackgroundColorSpan(color), MarkdownSpan