react-native-marcus 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +220 -63
  2. package/RNMarcus.podspec +6 -0
  3. package/android/src/main/java/app/getbullet/marcus/MarcusPackage.kt +2 -1
  4. package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorView.kt +168 -0
  5. package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorViewManager.kt +38 -0
  6. package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorViewManagerSpec.kt +19 -0
  7. package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorView.kt +23 -1
  8. package/android/src/main/java/app/getbullet/marcus/MarkdownBackgroundPainter.kt +147 -0
  9. package/android/src/main/java/app/getbullet/marcus/MarkdownBackgroundPass.kt +19 -0
  10. package/android/src/main/java/app/getbullet/marcus/MarkdownCopy.kt +87 -0
  11. package/android/src/main/java/app/getbullet/marcus/MarkdownFormatter.kt +652 -113
  12. package/android/src/main/java/app/getbullet/marcus/MarkdownRanges.kt +47 -0
  13. package/android/src/main/java/app/getbullet/marcus/MarkdownStyle.kt +140 -61
  14. package/android/src/main/java/app/getbullet/marcus/MarkdownUtils.kt +37 -7
  15. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownAbsoluteLineHeightSpan.kt +48 -0
  16. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundSpan.kt +102 -48
  17. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBlockIndentSpan.kt +22 -4
  18. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBulletSpan.kt +78 -0
  19. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownCodeBlockSpan.kt +177 -0
  20. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontStyleSpan.kt +32 -0
  21. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontWeightSpan.kt +34 -0
  22. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownGapSpan.kt +24 -11
  23. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownLetterSpacingSpan.kt +32 -0
  24. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownTextShadowSpan.kt +32 -0
  25. package/android/src/main/new_arch/CMakeLists.txt +17 -0
  26. package/android/src/main/new_arch/RNMarcusSpec.h +1 -0
  27. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorShadowNode.cpp +235 -0
  28. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorShadowNode.h +62 -0
  29. package/android/src/reactNativeVersionPatch/CustomMountingManager/81/app/getbullet/marcus/CustomFabricUIManager.java +22 -3
  30. package/android/src/reactNativeVersionPatch/CustomMountingManager/latest/app/getbullet/marcus/CustomFabricUIManager.java +22 -3
  31. package/android/src/test/java/app/getbullet/marcus/MarkdownCopyTest.kt +69 -0
  32. package/android/src/test/java/app/getbullet/marcus/MarkdownFormatterTest.kt +170 -25
  33. package/android/src/test/java/app/getbullet/marcus/RenderModel.kt +22 -5
  34. package/android/src/test/java/app/getbullet/marcus/spans/MarkdownCodeBlockSpanTest.kt +91 -0
  35. package/android/src/test/resources/render-model.txt +221 -16
  36. package/apple/MarcusSwizzle.h +25 -0
  37. package/apple/MarcusTextDecoratorComponentView.h +22 -0
  38. package/apple/MarcusTextDecoratorComponentView.mm +72 -0
  39. package/apple/MarcusTextDecoratorShadowNode.h +62 -0
  40. package/apple/MarcusTextDecoratorShadowNode.mm +192 -0
  41. package/apple/MarcusTextDecoratorViewManager.h +4 -0
  42. package/apple/MarcusTextDecoratorViewManager.mm +11 -0
  43. package/apple/MarcusTextInputDecoratorComponentView.mm +1 -1
  44. package/apple/MarcusTextInputDecoratorShadowNode.mm +1 -1
  45. package/apple/MarcusTextLayoutManager.h +40 -0
  46. package/apple/MarcusTextLayoutManager.mm +173 -0
  47. package/apple/MarkdownAttributes.h +23 -2
  48. package/apple/MarkdownAttributes.m +6 -0
  49. package/apple/MarkdownCopy.swift +63 -0
  50. package/apple/MarkdownDecorator.swift +2 -2
  51. package/apple/MarkdownFormatter+React.swift +23 -0
  52. package/apple/MarkdownFormatter.swift +869 -148
  53. package/apple/MarkdownParagraphLayoutManager.swift +455 -0
  54. package/apple/MarkdownSwiftInterop.h +0 -1
  55. package/apple/MarkdownTextBackground.swift +59 -1
  56. package/apple/MarkdownTextFieldObserver.swift +2 -2
  57. package/apple/MarkdownTextLayoutFragment.swift +159 -33
  58. package/apple/MarkdownTextLayoutManagerDelegate.swift +67 -3
  59. package/apple/MarkdownTextStorageDelegate.swift +2 -2
  60. package/apple/MarkdownTextViewObserver.swift +2 -2
  61. package/apple/MarkdownUtils.swift +38 -0
  62. package/apple/MarkdownWritingDirection.swift +132 -0
  63. package/apple/RCTMarcusStyle+Codegen.h +115 -9
  64. package/apple/RCTMarcusStyle.h +128 -31
  65. package/apple/RCTMarcusStyle.m +48 -0
  66. package/apple/RCTParagraphComponentView+Marcus.h +17 -0
  67. package/apple/RCTParagraphComponentView+Marcus.mm +38 -0
  68. package/apple/RCTTextInputComponentView+Marcus.mm +1 -25
  69. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorState.h +37 -0
  70. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorViewComponentDescriptor.h +17 -0
  71. package/lib/commonjs/MarcusTextDecoratorViewNativeComponent.ts +199 -0
  72. package/lib/commonjs/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
  73. package/lib/commonjs/MarkdownText.js +330 -0
  74. package/lib/commonjs/MarkdownText.js.map +1 -0
  75. package/lib/commonjs/MarkdownText.web.js +529 -0
  76. package/lib/commonjs/MarkdownText.web.js.map +1 -0
  77. package/lib/commonjs/MarkdownTextInput.js +22 -27
  78. package/lib/commonjs/MarkdownTextInput.js.map +1 -1
  79. package/lib/commonjs/MarkdownTextInput.web.js +12 -7
  80. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
  81. package/lib/commonjs/atomUtils.js +112 -0
  82. package/lib/commonjs/atomUtils.js.map +1 -0
  83. package/lib/commonjs/childrenUtils.js +36 -0
  84. package/lib/commonjs/childrenUtils.js.map +1 -0
  85. package/lib/commonjs/index.js +8 -7
  86. package/lib/commonjs/index.js.map +1 -1
  87. package/lib/commonjs/math/environment.js +69 -0
  88. package/lib/commonjs/math/environment.js.map +1 -0
  89. package/lib/commonjs/math/index.js +61 -0
  90. package/lib/commonjs/math/index.js.map +1 -0
  91. package/lib/commonjs/math/mathjax.js +548 -0
  92. package/lib/commonjs/math/mathjax.js.map +1 -0
  93. package/lib/commonjs/metro.js +172 -0
  94. package/lib/commonjs/metro.js.map +1 -0
  95. package/lib/commonjs/parser/index.d.ts +3 -0
  96. package/lib/commonjs/parser/index.js +450 -42
  97. package/lib/commonjs/parser/index.js.map +4 -4
  98. package/lib/commonjs/stripUtils.js +743 -0
  99. package/lib/commonjs/stripUtils.js.map +1 -0
  100. package/lib/commonjs/styleUtils.js +194 -23
  101. package/lib/commonjs/styleUtils.js.map +1 -1
  102. package/lib/commonjs/web/MarkdownElements.css +67 -0
  103. package/lib/commonjs/web/MarkdownTextInput.css +9 -7
  104. package/lib/commonjs/web/utils/blockLayout.js +95 -7
  105. package/lib/commonjs/web/utils/blockLayout.js.map +1 -1
  106. package/lib/commonjs/web/utils/blockUtils.js +327 -54
  107. package/lib/commonjs/web/utils/blockUtils.js.map +1 -1
  108. package/lib/commonjs/web/utils/elementUtils.js +80 -0
  109. package/lib/commonjs/web/utils/elementUtils.js.map +1 -0
  110. package/lib/commonjs/web/utils/parserUtils.js +84 -28
  111. package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
  112. package/lib/commonjs/web/utils/treeUtils.js +6 -0
  113. package/lib/commonjs/web/utils/treeUtils.js.map +1 -1
  114. package/lib/commonjs/web/utils/webStyleUtils.js +25 -5
  115. package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -1
  116. package/lib/module/MarcusTextDecoratorViewNativeComponent.ts +199 -0
  117. package/lib/module/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
  118. package/lib/module/MarkdownText.js +325 -0
  119. package/lib/module/MarkdownText.js.map +1 -0
  120. package/lib/module/MarkdownText.web.js +524 -0
  121. package/lib/module/MarkdownText.web.js.map +1 -0
  122. package/lib/module/MarkdownTextInput.js +24 -29
  123. package/lib/module/MarkdownTextInput.js.map +1 -1
  124. package/lib/module/MarkdownTextInput.web.js +12 -7
  125. package/lib/module/MarkdownTextInput.web.js.map +1 -1
  126. package/lib/module/atomUtils.js +106 -0
  127. package/lib/module/atomUtils.js.map +1 -0
  128. package/lib/module/childrenUtils.js +33 -0
  129. package/lib/module/childrenUtils.js.map +1 -0
  130. package/lib/module/index.js +1 -1
  131. package/lib/module/index.js.map +1 -1
  132. package/lib/module/math/environment.js +67 -0
  133. package/lib/module/math/environment.js.map +1 -0
  134. package/lib/module/math/index.js +56 -0
  135. package/lib/module/math/index.js.map +1 -0
  136. package/lib/module/math/mathjax.js +541 -0
  137. package/lib/module/math/mathjax.js.map +1 -0
  138. package/lib/module/metro.js +169 -0
  139. package/lib/module/metro.js.map +1 -0
  140. package/lib/module/parser/index.d.ts +3 -0
  141. package/lib/module/parser/index.js +450 -42
  142. package/lib/module/parser/index.js.map +4 -4
  143. package/lib/module/stripUtils.js +739 -0
  144. package/lib/module/stripUtils.js.map +1 -0
  145. package/lib/module/styleUtils.js +196 -25
  146. package/lib/module/styleUtils.js.map +1 -1
  147. package/lib/module/web/MarkdownElements.css +67 -0
  148. package/lib/module/web/MarkdownTextInput.css +9 -7
  149. package/lib/module/web/utils/blockLayout.js +94 -8
  150. package/lib/module/web/utils/blockLayout.js.map +1 -1
  151. package/lib/module/web/utils/blockUtils.js +328 -56
  152. package/lib/module/web/utils/blockUtils.js.map +1 -1
  153. package/lib/module/web/utils/elementUtils.js +75 -0
  154. package/lib/module/web/utils/elementUtils.js.map +1 -0
  155. package/lib/module/web/utils/parserUtils.js +86 -30
  156. package/lib/module/web/utils/parserUtils.js.map +1 -1
  157. package/lib/module/web/utils/treeUtils.js +6 -0
  158. package/lib/module/web/utils/treeUtils.js.map +1 -1
  159. package/lib/module/web/utils/webStyleUtils.js +25 -5
  160. package/lib/module/web/utils/webStyleUtils.js.map +1 -1
  161. package/lib/typescript/commonjs/e2e/links.test.d.ts +2 -0
  162. package/lib/typescript/commonjs/e2e/links.test.d.ts.map +1 -0
  163. package/lib/typescript/commonjs/src/MarcusTextDecoratorViewNativeComponent.d.ts +93 -0
  164. package/lib/typescript/commonjs/src/MarcusTextDecoratorViewNativeComponent.d.ts.map +1 -0
  165. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +64 -51
  166. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/src/MarkdownText.d.ts +127 -0
  168. package/lib/typescript/commonjs/src/MarkdownText.d.ts.map +1 -0
  169. package/lib/typescript/commonjs/src/MarkdownText.web.d.ts +126 -0
  170. package/lib/typescript/commonjs/src/MarkdownText.web.d.ts.map +1 -0
  171. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts +23 -1
  172. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts +26 -4
  174. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts.map +1 -1
  175. package/lib/typescript/commonjs/src/atomUtils.d.ts +58 -0
  176. package/lib/typescript/commonjs/src/atomUtils.d.ts.map +1 -0
  177. package/lib/typescript/commonjs/src/childrenUtils.d.ts +12 -0
  178. package/lib/typescript/commonjs/src/childrenUtils.d.ts.map +1 -0
  179. package/lib/typescript/commonjs/src/commonTypes.d.ts +39 -3
  180. package/lib/typescript/commonjs/src/commonTypes.d.ts.map +1 -1
  181. package/lib/typescript/commonjs/src/index.d.ts +2 -1
  182. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  183. package/lib/typescript/commonjs/src/math/environment.d.ts +7 -0
  184. package/lib/typescript/commonjs/src/math/environment.d.ts.map +1 -0
  185. package/lib/typescript/commonjs/src/math/index.d.ts +64 -0
  186. package/lib/typescript/commonjs/src/math/index.d.ts.map +1 -0
  187. package/lib/typescript/commonjs/src/math/mathjax.d.ts +63 -0
  188. package/lib/typescript/commonjs/src/math/mathjax.d.ts.map +1 -0
  189. package/lib/typescript/commonjs/src/metro.d.ts +59 -0
  190. package/lib/typescript/commonjs/src/metro.d.ts.map +1 -0
  191. package/lib/typescript/commonjs/src/parser/index.d.ts +3 -3
  192. package/lib/typescript/commonjs/src/parser/index.d.ts.map +1 -1
  193. package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts.map +1 -1
  194. package/lib/typescript/commonjs/src/rangeUtils.d.ts +1 -1
  195. package/lib/typescript/commonjs/src/stripUtils.d.ts +108 -0
  196. package/lib/typescript/commonjs/src/stripUtils.d.ts.map +1 -0
  197. package/lib/typescript/commonjs/src/styleUtils.d.ts +62 -4
  198. package/lib/typescript/commonjs/src/styleUtils.d.ts.map +1 -1
  199. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts +32 -4
  200. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts.map +1 -1
  201. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts +20 -8
  202. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts.map +1 -1
  203. package/lib/typescript/commonjs/src/web/utils/elementUtils.d.ts +37 -0
  204. package/lib/typescript/commonjs/src/web/utils/elementUtils.d.ts.map +1 -0
  205. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts +5 -5
  206. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts.map +1 -1
  207. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts +6 -1
  208. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts.map +1 -1
  209. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts +2 -2
  210. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts.map +1 -1
  211. package/lib/typescript/module/e2e/links.test.d.ts +2 -0
  212. package/lib/typescript/module/e2e/links.test.d.ts.map +1 -0
  213. package/lib/typescript/module/src/MarcusTextDecoratorViewNativeComponent.d.ts +93 -0
  214. package/lib/typescript/module/src/MarcusTextDecoratorViewNativeComponent.d.ts.map +1 -0
  215. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +64 -51
  216. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
  217. package/lib/typescript/module/src/MarkdownText.d.ts +127 -0
  218. package/lib/typescript/module/src/MarkdownText.d.ts.map +1 -0
  219. package/lib/typescript/module/src/MarkdownText.web.d.ts +126 -0
  220. package/lib/typescript/module/src/MarkdownText.web.d.ts.map +1 -0
  221. package/lib/typescript/module/src/MarkdownTextInput.d.ts +23 -1
  222. package/lib/typescript/module/src/MarkdownTextInput.d.ts.map +1 -1
  223. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts +26 -4
  224. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts.map +1 -1
  225. package/lib/typescript/module/src/atomUtils.d.ts +58 -0
  226. package/lib/typescript/module/src/atomUtils.d.ts.map +1 -0
  227. package/lib/typescript/module/src/childrenUtils.d.ts +12 -0
  228. package/lib/typescript/module/src/childrenUtils.d.ts.map +1 -0
  229. package/lib/typescript/module/src/commonTypes.d.ts +39 -3
  230. package/lib/typescript/module/src/commonTypes.d.ts.map +1 -1
  231. package/lib/typescript/module/src/index.d.ts +2 -1
  232. package/lib/typescript/module/src/index.d.ts.map +1 -1
  233. package/lib/typescript/module/src/math/environment.d.ts +7 -0
  234. package/lib/typescript/module/src/math/environment.d.ts.map +1 -0
  235. package/lib/typescript/module/src/math/index.d.ts +64 -0
  236. package/lib/typescript/module/src/math/index.d.ts.map +1 -0
  237. package/lib/typescript/module/src/math/mathjax.d.ts +63 -0
  238. package/lib/typescript/module/src/math/mathjax.d.ts.map +1 -0
  239. package/lib/typescript/module/src/metro.d.ts +59 -0
  240. package/lib/typescript/module/src/metro.d.ts.map +1 -0
  241. package/lib/typescript/module/src/parser/index.d.ts +3 -3
  242. package/lib/typescript/module/src/parser/index.d.ts.map +1 -1
  243. package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts.map +1 -1
  244. package/lib/typescript/module/src/rangeUtils.d.ts +1 -1
  245. package/lib/typescript/module/src/stripUtils.d.ts +108 -0
  246. package/lib/typescript/module/src/stripUtils.d.ts.map +1 -0
  247. package/lib/typescript/module/src/styleUtils.d.ts +62 -4
  248. package/lib/typescript/module/src/styleUtils.d.ts.map +1 -1
  249. package/lib/typescript/module/src/web/utils/blockLayout.d.ts +32 -4
  250. package/lib/typescript/module/src/web/utils/blockLayout.d.ts.map +1 -1
  251. package/lib/typescript/module/src/web/utils/blockUtils.d.ts +20 -8
  252. package/lib/typescript/module/src/web/utils/blockUtils.d.ts.map +1 -1
  253. package/lib/typescript/module/src/web/utils/elementUtils.d.ts +37 -0
  254. package/lib/typescript/module/src/web/utils/elementUtils.d.ts.map +1 -0
  255. package/lib/typescript/module/src/web/utils/parserUtils.d.ts +5 -5
  256. package/lib/typescript/module/src/web/utils/parserUtils.d.ts.map +1 -1
  257. package/lib/typescript/module/src/web/utils/treeUtils.d.ts +6 -1
  258. package/lib/typescript/module/src/web/utils/treeUtils.d.ts.map +1 -1
  259. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts +2 -2
  260. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts.map +1 -1
  261. package/package.json +29 -2
  262. package/react-native.config.js +8 -1
  263. package/src/MarcusTextDecoratorViewNativeComponent.ts +199 -0
  264. package/src/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
  265. package/src/MarkdownText.tsx +472 -0
  266. package/src/MarkdownText.web.tsx +668 -0
  267. package/src/MarkdownTextInput.tsx +42 -38
  268. package/src/MarkdownTextInput.web.tsx +36 -13
  269. package/src/atomUtils.ts +112 -0
  270. package/src/childrenUtils.ts +39 -0
  271. package/src/commonTypes.ts +69 -9
  272. package/src/index.tsx +2 -1
  273. package/src/math/environment.ts +68 -0
  274. package/src/math/index.ts +78 -0
  275. package/src/math/mathjax.ts +604 -0
  276. package/src/metro.ts +238 -0
  277. package/src/parser/index.ts +175 -10
  278. package/src/parser/micromark-extension-mentions.ts +104 -11
  279. package/src/stripUtils.ts +919 -0
  280. package/src/styleUtils.ts +290 -26
  281. package/src/web/MarkdownElements.css +67 -0
  282. package/src/web/MarkdownTextInput.css +9 -7
  283. package/src/web/utils/blockLayout.ts +124 -14
  284. package/src/web/utils/blockUtils.ts +365 -59
  285. package/src/web/utils/elementUtils.ts +87 -0
  286. package/src/web/utils/parserUtils.ts +119 -31
  287. package/src/web/utils/treeUtils.ts +6 -1
  288. package/src/web/utils/webStyleUtils.ts +32 -8
  289. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundColorSpan.kt +0 -6
  290. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBoldSpan.kt +0 -6
  291. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownItalicSpan.kt +0 -6
  292. package/apple/RCTMarcusStyle+Codegen.mm +0 -64
  293. package/apple/RCTMarcusUtils.h +0 -26
  294. package/apple/RCTMarcusUtils.mm +0 -26
@@ -0,0 +1,47 @@
1
+ package app.getbullet.marcus
2
+
3
+ import com.facebook.react.bridge.ReadableArray
4
+ import com.facebook.react.bridge.ReadableType
5
+
6
+ /**
7
+ * Reads the `ranges` prop into what the formatter wants.
8
+ *
9
+ * The JavaScript side has already stripped the syntax and remapped every offset onto the text the
10
+ * child `Text` is rendering, so nothing here validates or reorders: emission order is the contract
11
+ * the formatter walks, and reordering it would break the pairing of a block prefix with the
12
+ * container that follows it.
13
+ *
14
+ * An object with `@JvmStatic` rather than a top-level function, because `CustomFabricUIManager` is
15
+ * Java and Kotlin mangles the names of top-level internal ones.
16
+ */
17
+ object MarkdownRanges {
18
+
19
+ @JvmStatic
20
+ fun read(array: ReadableArray?): List<MarkdownRange> {
21
+ if (array == null || array.size() == 0) {
22
+ return emptyList()
23
+ }
24
+
25
+ val ranges = ArrayList<MarkdownRange>(array.size())
26
+
27
+ for (i in 0 until array.size()) {
28
+ if (array.getType(i) != ReadableType.Map) {
29
+ continue
30
+ }
31
+
32
+ val map = array.getMap(i) ?: continue
33
+ val type = map.getString("type") ?: continue
34
+
35
+ ranges.add(
36
+ MarkdownRange(
37
+ type = type,
38
+ start = map.getInt("start"),
39
+ length = map.getInt("length"),
40
+ depth = map.getInt("depth")
41
+ )
42
+ )
43
+ }
44
+
45
+ return ranges
46
+ }
47
+ }
@@ -7,75 +7,154 @@ import com.facebook.react.bridge.JSApplicationCausedNativeException
7
7
  import com.facebook.react.bridge.ReadableMap
8
8
  import com.facebook.react.bridge.ReadableType
9
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")
10
+ /**
11
+ * The text attributes one styled run can carry: the subset of React Native's
12
+ * `TextStyle` that survives as a span.
13
+ *
14
+ * Everything is optional and everything unset is inherited, which is what a run
15
+ * of markdown wants -- a mention that only names a colour keeps the face, the
16
+ * size and the weight of the prose it sits in. "Unset" is null for a colour or a
17
+ * name and zero for a length, which costs nothing: a zero-length `fontSize`,
18
+ * `letterSpacing` or `lineHeight` would mean the same as leaving it out.
19
+ *
20
+ * `textDecorationColor` and `textDecorationStyle` are not read. Android draws an
21
+ * underline and a strikethrough in the text's own colour as a solid line and
22
+ * offers no way to say otherwise, so the two are honoured on iOS and the web and
23
+ * documented as such rather than parsed and dropped here.
24
+ */
25
+ open class MarkdownTextStyle(map: ReadableMap?, context: Context) {
26
+ @ColorInt val color = parseColor(map, "color", context)
27
+ val fontFamily = parseString(map, "fontFamily")
28
+ val fontSize = parseFloat(map, "fontSize")
29
+ val fontStyle = parseString(map, "fontStyle")
30
+ val fontWeight = parseString(map, "fontWeight")
31
+ val letterSpacing = parseFloat(map, "letterSpacing")
32
+ val lineHeight = parseFloat(map, "lineHeight")
33
+ val textDecorationLine = parseString(map, "textDecorationLine")
34
+ @ColorInt val textShadowColor = parseColor(map, "textShadowColor", context)
35
+ val textShadowOffsetX = parseFloat(map?.let { style(it, "textShadowOffset") }, "width")
36
+ val textShadowOffsetY = parseFloat(map?.let { style(it, "textShadowOffset") }, "height")
37
+ val textShadowRadius = parseFloat(map, "textShadowRadius")
38
+ }
27
39
 
28
- val orderedListMarginLeft = parseFloat(map, "orderedList", "marginLeft")
29
- val orderedListPaddingLeft = parseFloat(map, "orderedList", "paddingLeft")
40
+ /**
41
+ * A run drawn as a box rather than as a background behind the glyphs: an inline
42
+ * or fenced run of code, or a mention's pill.
43
+ *
44
+ * Only the per-axis lengths are here. A style may be written with a single
45
+ * `padding` or `margin` covering both axes; `mergeMarkdownStyleWithDefault`
46
+ * resolves that into the two before the style is ever sent, so a length of zero
47
+ * here means zero rather than "unset".
48
+ *
49
+ * The lengths are in dp, as they are written in a style. What draws them
50
+ * converts, so a value means the same thing wherever it is read.
51
+ */
52
+ class MarkdownBoxStyle(map: ReadableMap?, context: Context) : MarkdownTextStyle(map, context) {
53
+ @ColorInt val backgroundColor = parseColor(map, "backgroundColor", context)
54
+ @ColorInt val borderColor = parseColor(map, "borderColor", context)
55
+ val borderWidth = parseFloat(map, "borderWidth")
56
+ val borderRadius = parseFloat(map, "borderRadius")
57
+ val paddingHorizontal = parseFloat(map, "paddingHorizontal")
58
+ val paddingVertical = parseFloat(map, "paddingVertical")
59
+ val marginHorizontal = parseFloat(map, "marginHorizontal")
60
+ val marginVertical = parseFloat(map, "marginVertical")
61
+ }
30
62
 
31
- val unorderedListMarginLeft = parseFloat(map, "unorderedList", "marginLeft")
32
- val unorderedListPaddingLeft = parseFloat(map, "unorderedList", "paddingLeft")
63
+ class MarkdownHeadingStyle(map: ReadableMap?, context: Context) : MarkdownTextStyle(map, context) {
64
+ /**
65
+ * Each level down multiplies the one above, so level N is
66
+ * `fontSize * scale^(N - 1)`. Six levels, matching HTML.
67
+ */
68
+ val scale = parseFloat(map, "scale")
69
+ }
33
70
 
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)
71
+ /**
72
+ * The bar beside a quoted block, the indent it opens, and the text inside it.
73
+ *
74
+ * `marginStart` and `paddingStart` rather than left and right: the gutter opens
75
+ * on the line's leading edge, which is what a `LeadingMarginSpan` has always
76
+ * meant, so the names now say what was already drawn.
77
+ */
78
+ class MarkdownBlockquoteStyle(map: ReadableMap?, context: Context) :
79
+ MarkdownTextStyle(map, context) {
80
+ @ColorInt val borderColor = parseColor(map, "borderColor", context)
81
+ val borderWidth = parseFloat(map, "borderWidth")
82
+ /** Rounds the bar's ends. A bar whose radius is half its width is a capsule. */
83
+ val borderRadius = parseFloat(map, "borderRadius")
84
+ val marginStart = parseFloat(map, "marginStart")
85
+ val paddingStart = parseFloat(map, "paddingStart")
86
+ }
38
87
 
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)
88
+ /**
89
+ * Indent, and the marker that sits in the gutter it opens.
90
+ *
91
+ * The marker is sized from the base font -- the size the wrapped `Text` renders
92
+ * at -- rather than from a size of its own, so a list marks itself in proportion
93
+ * to the prose it marks however that prose is sized. [marker] is what it is
94
+ * drawn with, already resolved against `syntax` on the JavaScript side, so
95
+ * nothing here has to know where an unset value came from.
96
+ */
97
+ class MarkdownListStyle(map: ReadableMap?, context: Context) {
98
+ val marginStart = parseFloat(map, "marginStart")
99
+ val paddingStart = parseFloat(map, "paddingStart")
100
+ val markerScale = parseFloat(map, "markerScale")
101
+ val markerPadding = parseFloat(map, "markerPadding")
102
+ val marker = MarkdownTextStyle(map?.let { style(it, "marker") }, context)
103
+ }
43
104
 
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
105
+ class MarkdownEmojiStyle(map: ReadableMap?, context: Context) {
106
+ val fontSize = parseFloat(map, "fontSize")
107
+ val fontFamily = parseString(map, "fontFamily")
108
+ }
47
109
 
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
110
+ class MarkdownStyle(map: ReadableMap, context: Context) {
111
+ val syntax = MarkdownTextStyle(style(map, "syntax"), context)
112
+ val link = MarkdownTextStyle(style(map, "link"), context)
113
+ val strong = MarkdownTextStyle(style(map, "strong"), context)
114
+ val emphasis = MarkdownTextStyle(style(map, "emphasis"), context)
115
+ val strikethrough = MarkdownTextStyle(style(map, "strikethrough"), context)
116
+ val heading = MarkdownHeadingStyle(style(map, "heading"), context)
117
+ val emoji = MarkdownEmojiStyle(style(map, "emoji"), context)
118
+ val blockquote = MarkdownBlockquoteStyle(style(map, "blockquote"), context)
119
+ val orderedList = MarkdownListStyle(style(map, "orderedList"), context)
120
+ val unorderedList = MarkdownListStyle(style(map, "unorderedList"), context)
121
+ val code = MarkdownBoxStyle(style(map, "code"), context)
122
+ val pre = MarkdownBoxStyle(style(map, "pre"), context)
123
+ val mention = MarkdownBoxStyle(style(map, "mention"), context)
124
+ }
51
125
 
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
126
+ /** The nested map under [key], or null where the style says nothing about it. */
127
+ internal fun style(map: ReadableMap, key: String): ReadableMap? =
128
+ if (map.hasKey(key)) map.getMap(key) else null
129
+
130
+ /**
131
+ * Reads a property, or the value that stands for "unset": null for a colour and
132
+ * a name, zero for a length.
133
+ *
134
+ * A missing entry and a missing property are the same thing, which is what lets
135
+ * a caller pass a map it did not have to check for.
136
+ */
137
+ @ColorInt
138
+ internal fun parseColor(map: ReadableMap?, prop: String, context: Context): Int? {
139
+ if (map == null || !map.hasKey(prop) || map.isNull(prop)) {
140
+ return null
141
+ }
55
142
 
56
- private companion object {
57
- fun style(map: ReadableMap, key: String): ReadableMap =
58
- requireNotNull(map.getMap(key)) { "[react-native-marcus] markdownStyle.$key is missing" }
143
+ val value = map.getDynamic(prop)
144
+ val color = when (value.type) {
145
+ ReadableType.Number -> ColorPropConverter.getColor(value.asDouble(), context)
146
+ ReadableType.Map -> ColorPropConverter.getColor(value.asMap(), context)
147
+ else -> throw JSApplicationCausedNativeException(
148
+ "ColorValue: the value must be a number or Object."
149
+ )
150
+ }
59
151
 
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
- }
152
+ return color
153
+ }
72
154
 
73
- fun parseFloat(map: ReadableMap, key: String, prop: String): Float =
74
- style(map, key).getDouble(prop).toFloat()
155
+ internal fun parseFloat(map: ReadableMap?, prop: String): Float =
156
+ if (map == null || !map.hasKey(prop) || map.isNull(prop)) 0f
157
+ else map.getDouble(prop).toFloat()
75
158
 
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
- }
159
+ internal fun parseString(map: ReadableMap?, prop: String): String? =
160
+ if (map == null || !map.hasKey(prop)) null else map.getString(prop)
@@ -14,12 +14,18 @@ import com.facebook.react.util.RNLog
14
14
  * nested inside them start, and has to see the size the text is drawn at.
15
15
  * Null on the measure path, which has no view yet and carries the size on the
16
16
  * spannable instead.
17
+ * @param backgroundPass set for an input, where the view paints the markdown
18
+ * backgrounds itself so that the caret stays visible. Null everywhere else.
19
+ * @param display set for a `Text`, whose list markers are rendered rather than
20
+ * shown; see [MarkdownFormatter].
17
21
  */
18
22
  class MarkdownUtils @JvmOverloads constructor(
19
23
  private val reactContext: ReactContext,
20
- private val textPaint: TextPaint? = null
24
+ private val textPaint: TextPaint? = null,
25
+ backgroundPass: MarkdownBackgroundPass? = null,
26
+ display: Boolean = false
21
27
  ) {
22
- private val markdownFormatter = MarkdownFormatter(reactContext.assets)
28
+ private val markdownFormatter = MarkdownFormatter(reactContext.assets, backgroundPass, display)
23
29
 
24
30
  // Hoisted rather than built per call: applyMarkdownFormatting runs several times per
25
31
  // keystroke, and this is only ever needed for a warning that normally never fires.
@@ -30,6 +36,15 @@ class MarkdownUtils @JvmOverloads constructor(
30
36
  var markdownStyle: MarkdownStyle? = null
31
37
  var parserId: Int = 0
32
38
 
39
+ /**
40
+ * Ranges to format with, for a caller that already has them.
41
+ *
42
+ * A `Text` is parsed in JavaScript -- its content only ever changes by a re-render, so there is
43
+ * nothing for a worklet to react to -- and the result arrives as a prop. A `TextInput` leaves
44
+ * this null and the parser above is used instead.
45
+ */
46
+ var ranges: List<MarkdownRange>? = null
47
+
33
48
  /**
34
49
  * Records what the markdown spans currently on a spannable were built from.
35
50
  *
@@ -38,9 +53,15 @@ class MarkdownUtils @JvmOverloads constructor(
38
53
  * this along with the markdown spans, and the next pass rebuilds both. A field would
39
54
  * survive that and we would skip a pass that was actually needed.
40
55
  */
41
- private class Fingerprint(val text: String, val parserId: Int, val style: MarkdownStyle)
56
+ private class Fingerprint(
57
+ val text: String,
58
+ val parserId: Int,
59
+ val style: MarkdownStyle,
60
+ val ranges: List<MarkdownRange>?
61
+ )
42
62
 
43
- fun applyMarkdownFormatting(ssb: SpannableStringBuilder) {
63
+ /** Whether anything was reformatted, which is what tells a caller to push the result on. */
64
+ fun applyMarkdownFormatting(ssb: SpannableStringBuilder): Boolean {
44
65
  try {
45
66
  Trace.beginSection("applyMarkdownFormatting")
46
67
  val style = checkNotNull(markdownStyle) { "[react-native-marcus] markdownStyle is null" }
@@ -49,13 +70,14 @@ class MarkdownUtils @JvmOverloads constructor(
49
70
  // Native's state round-trip. They all ask for the same text, so only the first has any
50
71
  // work to do.
51
72
  if (isUpToDate(ssb, style)) {
52
- return
73
+ return false
53
74
  }
54
75
 
55
76
  val text = ssb.toString()
56
- val markdownRanges = MarkdownParser.shared.parse(text, parserId, onSchemaError)
77
+ val markdownRanges = ranges ?: MarkdownParser.shared.parse(text, parserId, onSchemaError)
57
78
  markdownFormatter.format(ssb, markdownRanges, style, textPaint)
58
79
  stamp(ssb, text, style)
80
+ return true
59
81
  } finally {
60
82
  Trace.endSection()
61
83
  }
@@ -68,6 +90,9 @@ class MarkdownUtils @JvmOverloads constructor(
68
90
  // Identity, not equality: MarkdownStyle is rebuilt whenever the prop arrives, so a
69
91
  // different instance means the style may have changed and we re-apply to be safe.
70
92
  fingerprint.style === style &&
93
+ // Same reasoning for the ranges, which are rebuilt from the prop on every commit that
94
+ // carries them.
95
+ fingerprint.ranges === ranges &&
71
96
  // Compares the CharSequence in place. ssb.toString() would copy the whole document on
72
97
  // a path whose entire purpose is to do no work.
73
98
  TextUtils.equals(ssb, fingerprint.text)
@@ -79,6 +104,11 @@ class MarkdownUtils @JvmOverloads constructor(
79
104
  for (stale in ssb.getSpans(0, ssb.length, Fingerprint::class.java)) {
80
105
  ssb.removeSpan(stale)
81
106
  }
82
- ssb.setSpan(Fingerprint(text, parserId, style), 0, ssb.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
107
+ ssb.setSpan(
108
+ Fingerprint(text, parserId, style, ranges),
109
+ 0,
110
+ ssb.length,
111
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
112
+ )
83
113
  }
84
114
  }
@@ -0,0 +1,48 @@
1
+ package app.getbullet.marcus.spans
2
+
3
+ import android.graphics.Paint
4
+ import android.text.style.LineHeightSpan
5
+ import com.facebook.react.uimanager.PixelUtil
6
+ import kotlin.math.roundToInt
7
+
8
+ /**
9
+ * Draws the lines a run touches at an exact height.
10
+ *
11
+ * A line height is a property of a line rather than of a run, so a run that asks
12
+ * for one raises the whole line it sits on. iOS puts the same value on the
13
+ * paragraph style for the same reason, and the public type says as much.
14
+ *
15
+ * The difference is taken off the ascent and the descent in the proportion they
16
+ * already stood in, which is what React Native's own line-height span does and
17
+ * what keeps the text centred in the taller line rather than sitting on its
18
+ * floor.
19
+ *
20
+ * @param lineHeight in dp, as it is written in a style
21
+ */
22
+ class MarkdownAbsoluteLineHeightSpan(lineHeight: Float) : MarkdownSpan, LineHeightSpan {
23
+
24
+ private val height = PixelUtil.toPixelFromDIP(lineHeight).roundToInt()
25
+
26
+ override fun chooseHeight(
27
+ text: CharSequence?,
28
+ start: Int,
29
+ end: Int,
30
+ spanstartv: Int,
31
+ lineHeight: Int,
32
+ fm: Paint.FontMetricsInt
33
+ ) {
34
+ if (height <= 0) {
35
+ return
36
+ }
37
+
38
+ val current = fm.descent - fm.ascent
39
+ if (current <= 0) {
40
+ return
41
+ }
42
+
43
+ fm.descent = (fm.descent.toFloat() * height / current).roundToInt()
44
+ fm.ascent = fm.descent - height
45
+ fm.top = fm.ascent
46
+ fm.bottom = fm.descent
47
+ }
48
+ }
@@ -8,16 +8,50 @@ import android.text.StaticLayout
8
8
  import android.text.TextPaint
9
9
  import android.text.style.LineBackgroundSpan
10
10
  import androidx.annotation.ColorInt
11
-
11
+ import app.getbullet.marcus.MarkdownBackgroundPass
12
+
13
+ /**
14
+ * The rounded box behind a run of text: a mention's pill, or an inline run of
15
+ * code.
16
+ *
17
+ * Drawn a line at a time, because soft wrapping can split a run, and only the
18
+ * sides that actually terminate it are rounded -- the break itself stays square,
19
+ * so the two halves read as one run. iOS draws the same shape the same way, in
20
+ * `MarkdownTextLayoutFragment` and `MarkdownParagraphLayoutManager`.
21
+ *
22
+ * The padding is the room between the glyphs and the box, the margin the room
23
+ * between the box and the text either side of it. Horizontally both are held
24
+ * open by the `MarkdownGapSpan`s the formatter puts on the run's first and last
25
+ * characters, so what is left to do here is place the box's edges inside the
26
+ * space those reserved. Vertically only the padding is used, and the box grows
27
+ * out of the font's own box into the line's spacing, which is what an inline box
28
+ * does everywhere else: a line cannot be pushed apart for one run on it, so
29
+ * there is nothing a vertical margin could hold open.
30
+ *
31
+ * Only the vertical padding is a parameter: the horizontal room was already
32
+ * taken by the gap spans, so all this has to know is how far inside it the box's
33
+ * edge sits, which is the margin.
34
+ *
35
+ * @param runStart first character of the run, for the corners and the insets
36
+ * @param runEnd one past its last character
37
+ * @param pass set in an input, where the view paints the backgrounds itself so
38
+ * that the caret stays visible; null in a `Text`, which has no caret and lets
39
+ * the layout paint them
40
+ */
12
41
  class MarkdownBackgroundSpan(
13
42
  @ColorInt private val backgroundColor: Int,
14
43
  private val borderRadius: Float,
15
- private val mentionStart: Int,
16
- private val mentionEnd: Int
44
+ private val runStart: Int,
45
+ private val runEnd: Int,
46
+ private val paddingVertical: Float = 0f,
47
+ private val marginHorizontal: Float = 0f,
48
+ @ColorInt private val borderColor: Int? = null,
49
+ private val borderWidth: Float = 0f,
50
+ private val pass: MarkdownBackgroundPass? = null
17
51
  ) : MarkdownSpan, LineBackgroundSpan {
18
52
 
19
- private var layout: StaticLayout? = null
20
- private val backgroundPath = Path()
53
+ private val rect = RectF()
54
+ private val path = Path()
21
55
 
22
56
  override fun drawBackground(
23
57
  canvas: Canvas,
@@ -32,58 +66,78 @@ class MarkdownBackgroundSpan(
32
66
  end: Int,
33
67
  lnum: Int
34
68
  ) {
69
+ if (pass != null && !pass.painting) {
70
+ return
71
+ }
72
+
73
+ // The line on its own, measured as it was laid out. `drawBackground` is
74
+ // handed the line's characters and its paint but not the layout that
75
+ // positioned them, so where the run sits on it has to be measured again.
35
76
  val lineText = text.subSequence(start, end)
36
- val cachedLayout = layout
37
- // `!==` on purpose: the Java this replaces compared the layout's text by
38
- // reference, not by value.
39
- if (cachedLayout == null ||
40
- cachedLayout.text !== lineText ||
41
- cachedLayout.width != right ||
42
- cachedLayout.getLineEnd(0) != lineText.length
43
- ) {
44
- val currentLineStart = 0
45
- val currentLineEnd = lineText.length
46
- // Create layout for the current line only
47
- val newLayout = StaticLayout.Builder
48
- .obtain(lineText, currentLineStart, currentLineEnd, paint as TextPaint, right)
49
- .build()
50
- layout = newLayout
51
-
52
- val relativeMentionStart = mentionStart - start
53
- val relativeMentionEnd = mentionEnd - start
54
-
55
- val mentionStartsInCurrentLine = currentLineStart <= relativeMentionStart
56
- val mentionEndsInCurrentLine = currentLineEnd >= relativeMentionEnd
57
-
58
- val startX = newLayout.getPrimaryHorizontal(
59
- if (mentionStartsInCurrentLine) relativeMentionStart else currentLineStart
60
- )
61
- val endX = newLayout.getPrimaryHorizontal(
62
- if (mentionEndsInCurrentLine) relativeMentionEnd else currentLineEnd
63
- )
64
-
65
- val fm = paint.getFontMetrics()
66
- val startY = baseline + fm.ascent
67
- val endY = baseline + fm.descent
68
-
69
- val lineRect = RectF(startX, startY, endX, endY)
70
- backgroundPath.reset()
71
- backgroundPath.addRoundRect(
72
- lineRect,
73
- createRadii(mentionStartsInCurrentLine, mentionEndsInCurrentLine),
74
- Path.Direction.CW
75
- )
77
+ val layout = StaticLayout.Builder
78
+ .obtain(lineText, 0, lineText.length, paint as TextPaint, right)
79
+ .build()
80
+
81
+ val opens = runStart >= start
82
+ val closes = runEnd <= end
83
+
84
+ val startX = layout.getPrimaryHorizontal(if (opens) runStart - start else 0)
85
+ val endX = layout.getPrimaryHorizontal(if (closes) runEnd - start else lineText.length)
86
+
87
+ // The gap spans reserved padding and margin inside the first and last
88
+ // characters, so the box's edge is the margin's width inside what they took.
89
+ val metrics = paint.fontMetrics
90
+ rect.set(
91
+ startX + if (opens) marginHorizontal else 0f,
92
+ baseline + metrics.ascent - paddingVertical,
93
+ endX - if (closes) marginHorizontal else 0f,
94
+ baseline + metrics.descent + paddingVertical,
95
+ )
96
+
97
+ if (rect.width() <= 0f || rect.height() <= 0f) {
98
+ return
76
99
  }
77
100
 
101
+ path.reset()
102
+ path.addRoundRect(rect, radii(opens, closes), Path.Direction.CW)
103
+
104
+ fill(canvas, paint, path)
105
+ }
106
+
107
+ /**
108
+ * Fills the box and, where the style asked for one, draws its border.
109
+ *
110
+ * The border is stroked at twice its width and clipped to the box, so the
111
+ * outer half is trimmed away and a bordered run covers exactly the room an
112
+ * unbordered one does. `MarkdownFormatter.swift` draws its border the same
113
+ * way, and it is what a border on an element does on the web.
114
+ */
115
+ private fun fill(canvas: Canvas, paint: Paint, path: Path) {
78
116
  val originalColor = paint.color
79
- paint.color = backgroundColor
117
+ val originalStyle = paint.style
118
+ val originalWidth = paint.strokeWidth
80
119
 
81
- canvas.drawPath(backgroundPath, paint)
120
+ paint.style = Paint.Style.FILL
121
+ paint.color = backgroundColor
122
+ canvas.drawPath(path, paint)
123
+
124
+ if (borderColor != null && borderWidth > 0f) {
125
+ canvas.save()
126
+ canvas.clipPath(path)
127
+ paint.style = Paint.Style.STROKE
128
+ paint.strokeWidth = borderWidth * 2f
129
+ paint.color = borderColor
130
+ canvas.drawPath(path, paint)
131
+ canvas.restore()
132
+ }
82
133
 
83
134
  paint.color = originalColor
135
+ paint.style = originalStyle
136
+ paint.strokeWidth = originalWidth
84
137
  }
85
138
 
86
- private fun createRadii(roundedLeft: Boolean, roundedRight: Boolean): FloatArray {
139
+ /** Corners of one line's rect: only the ends of the run are rounded. */
140
+ private fun radii(roundedLeft: Boolean, roundedRight: Boolean): FloatArray {
87
141
  val radii = FloatArray(8)
88
142
 
89
143
  if (roundedLeft) {
@@ -13,11 +13,15 @@ import androidx.annotation.ColorInt
13
13
  * One span per line rather than one per container: where a container's gutter
14
14
  * goes depends on the markers of the containers around it, so the offsets are
15
15
  * worked out together by the formatter and handed over already in pixels.
16
+ *
17
+ * The ribbon's own three values default to nothing, for a line that is only
18
+ * indented -- a code block's padding, where no quote is involved.
16
19
  */
17
20
  class MarkdownBlockIndentSpan(
18
- @ColorInt private val borderColor: Int,
19
- private val borderWidth: Float,
20
- private val borderSpacing: Float
21
+ @ColorInt private val borderColor: Int = 0,
22
+ private val borderWidth: Float = 0f,
23
+ private val borderSpacing: Float = 0f,
24
+ private val borderRadius: Float = 0f
21
25
  ) : MarkdownSpan, LeadingMarginSpan {
22
26
 
23
27
  /** Where the line's text starts, and where it resumes when it wraps. */
@@ -58,7 +62,21 @@ class MarkdownBlockIndentSpan(
58
62
 
59
63
  for (level in 0 until depth) {
60
64
  val left = x + dir * (ribbonOffset + borderSpacing * level)
61
- c.drawRect(left, top.toFloat(), left + dir * borderWidth, bottom.toFloat(), p)
65
+ val right = left + dir * borderWidth
66
+
67
+ if (borderRadius > 0f) {
68
+ c.drawRoundRect(
69
+ minOf(left, right),
70
+ top.toFloat(),
71
+ maxOf(left, right),
72
+ bottom.toFloat(),
73
+ borderRadius,
74
+ borderRadius,
75
+ p
76
+ )
77
+ } else {
78
+ c.drawRect(left, top.toFloat(), right, bottom.toFloat(), p)
79
+ }
62
80
  }
63
81
 
64
82
  p.style = originalStyle