react-native-marcus 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +220 -63
  2. package/RNMarcus.podspec +6 -0
  3. package/android/src/main/java/app/getbullet/marcus/MarcusPackage.kt +2 -1
  4. package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorView.kt +168 -0
  5. package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorViewManager.kt +38 -0
  6. package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorViewManagerSpec.kt +19 -0
  7. package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorView.kt +23 -1
  8. package/android/src/main/java/app/getbullet/marcus/MarkdownBackgroundPainter.kt +147 -0
  9. package/android/src/main/java/app/getbullet/marcus/MarkdownBackgroundPass.kt +19 -0
  10. package/android/src/main/java/app/getbullet/marcus/MarkdownCopy.kt +87 -0
  11. package/android/src/main/java/app/getbullet/marcus/MarkdownFormatter.kt +652 -113
  12. package/android/src/main/java/app/getbullet/marcus/MarkdownRanges.kt +47 -0
  13. package/android/src/main/java/app/getbullet/marcus/MarkdownStyle.kt +140 -61
  14. package/android/src/main/java/app/getbullet/marcus/MarkdownUtils.kt +37 -7
  15. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownAbsoluteLineHeightSpan.kt +48 -0
  16. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundSpan.kt +102 -48
  17. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBlockIndentSpan.kt +22 -4
  18. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBulletSpan.kt +78 -0
  19. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownCodeBlockSpan.kt +177 -0
  20. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontStyleSpan.kt +32 -0
  21. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontWeightSpan.kt +34 -0
  22. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownGapSpan.kt +24 -11
  23. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownLetterSpacingSpan.kt +32 -0
  24. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownTextShadowSpan.kt +32 -0
  25. package/android/src/main/new_arch/CMakeLists.txt +17 -0
  26. package/android/src/main/new_arch/RNMarcusSpec.h +1 -0
  27. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorShadowNode.cpp +235 -0
  28. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorShadowNode.h +62 -0
  29. package/android/src/reactNativeVersionPatch/CustomMountingManager/81/app/getbullet/marcus/CustomFabricUIManager.java +22 -3
  30. package/android/src/reactNativeVersionPatch/CustomMountingManager/latest/app/getbullet/marcus/CustomFabricUIManager.java +22 -3
  31. package/android/src/test/java/app/getbullet/marcus/MarkdownCopyTest.kt +69 -0
  32. package/android/src/test/java/app/getbullet/marcus/MarkdownFormatterTest.kt +170 -25
  33. package/android/src/test/java/app/getbullet/marcus/RenderModel.kt +22 -5
  34. package/android/src/test/java/app/getbullet/marcus/spans/MarkdownCodeBlockSpanTest.kt +91 -0
  35. package/android/src/test/resources/render-model.txt +221 -16
  36. package/apple/MarcusSwizzle.h +25 -0
  37. package/apple/MarcusTextDecoratorComponentView.h +22 -0
  38. package/apple/MarcusTextDecoratorComponentView.mm +72 -0
  39. package/apple/MarcusTextDecoratorShadowNode.h +62 -0
  40. package/apple/MarcusTextDecoratorShadowNode.mm +192 -0
  41. package/apple/MarcusTextDecoratorViewManager.h +4 -0
  42. package/apple/MarcusTextDecoratorViewManager.mm +11 -0
  43. package/apple/MarcusTextInputDecoratorComponentView.mm +1 -1
  44. package/apple/MarcusTextInputDecoratorShadowNode.mm +1 -1
  45. package/apple/MarcusTextLayoutManager.h +40 -0
  46. package/apple/MarcusTextLayoutManager.mm +173 -0
  47. package/apple/MarkdownAttributes.h +23 -2
  48. package/apple/MarkdownAttributes.m +6 -0
  49. package/apple/MarkdownCopy.swift +63 -0
  50. package/apple/MarkdownDecorator.swift +2 -2
  51. package/apple/MarkdownFormatter+React.swift +23 -0
  52. package/apple/MarkdownFormatter.swift +869 -148
  53. package/apple/MarkdownParagraphLayoutManager.swift +455 -0
  54. package/apple/MarkdownSwiftInterop.h +0 -1
  55. package/apple/MarkdownTextBackground.swift +59 -1
  56. package/apple/MarkdownTextFieldObserver.swift +2 -2
  57. package/apple/MarkdownTextLayoutFragment.swift +159 -33
  58. package/apple/MarkdownTextLayoutManagerDelegate.swift +67 -3
  59. package/apple/MarkdownTextStorageDelegate.swift +2 -2
  60. package/apple/MarkdownTextViewObserver.swift +2 -2
  61. package/apple/MarkdownUtils.swift +38 -0
  62. package/apple/MarkdownWritingDirection.swift +132 -0
  63. package/apple/RCTMarcusStyle+Codegen.h +115 -9
  64. package/apple/RCTMarcusStyle.h +128 -31
  65. package/apple/RCTMarcusStyle.m +48 -0
  66. package/apple/RCTParagraphComponentView+Marcus.h +17 -0
  67. package/apple/RCTParagraphComponentView+Marcus.mm +38 -0
  68. package/apple/RCTTextInputComponentView+Marcus.mm +1 -25
  69. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorState.h +37 -0
  70. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorViewComponentDescriptor.h +17 -0
  71. package/lib/commonjs/MarcusTextDecoratorViewNativeComponent.ts +199 -0
  72. package/lib/commonjs/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
  73. package/lib/commonjs/MarkdownText.js +330 -0
  74. package/lib/commonjs/MarkdownText.js.map +1 -0
  75. package/lib/commonjs/MarkdownText.web.js +529 -0
  76. package/lib/commonjs/MarkdownText.web.js.map +1 -0
  77. package/lib/commonjs/MarkdownTextInput.js +22 -27
  78. package/lib/commonjs/MarkdownTextInput.js.map +1 -1
  79. package/lib/commonjs/MarkdownTextInput.web.js +12 -7
  80. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
  81. package/lib/commonjs/atomUtils.js +112 -0
  82. package/lib/commonjs/atomUtils.js.map +1 -0
  83. package/lib/commonjs/childrenUtils.js +36 -0
  84. package/lib/commonjs/childrenUtils.js.map +1 -0
  85. package/lib/commonjs/index.js +8 -7
  86. package/lib/commonjs/index.js.map +1 -1
  87. package/lib/commonjs/math/environment.js +69 -0
  88. package/lib/commonjs/math/environment.js.map +1 -0
  89. package/lib/commonjs/math/index.js +61 -0
  90. package/lib/commonjs/math/index.js.map +1 -0
  91. package/lib/commonjs/math/mathjax.js +548 -0
  92. package/lib/commonjs/math/mathjax.js.map +1 -0
  93. package/lib/commonjs/metro.js +172 -0
  94. package/lib/commonjs/metro.js.map +1 -0
  95. package/lib/commonjs/parser/index.d.ts +3 -0
  96. package/lib/commonjs/parser/index.js +450 -42
  97. package/lib/commonjs/parser/index.js.map +4 -4
  98. package/lib/commonjs/stripUtils.js +743 -0
  99. package/lib/commonjs/stripUtils.js.map +1 -0
  100. package/lib/commonjs/styleUtils.js +194 -23
  101. package/lib/commonjs/styleUtils.js.map +1 -1
  102. package/lib/commonjs/web/MarkdownElements.css +67 -0
  103. package/lib/commonjs/web/MarkdownTextInput.css +9 -7
  104. package/lib/commonjs/web/utils/blockLayout.js +95 -7
  105. package/lib/commonjs/web/utils/blockLayout.js.map +1 -1
  106. package/lib/commonjs/web/utils/blockUtils.js +327 -54
  107. package/lib/commonjs/web/utils/blockUtils.js.map +1 -1
  108. package/lib/commonjs/web/utils/elementUtils.js +80 -0
  109. package/lib/commonjs/web/utils/elementUtils.js.map +1 -0
  110. package/lib/commonjs/web/utils/parserUtils.js +84 -28
  111. package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
  112. package/lib/commonjs/web/utils/treeUtils.js +6 -0
  113. package/lib/commonjs/web/utils/treeUtils.js.map +1 -1
  114. package/lib/commonjs/web/utils/webStyleUtils.js +25 -5
  115. package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -1
  116. package/lib/module/MarcusTextDecoratorViewNativeComponent.ts +199 -0
  117. package/lib/module/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
  118. package/lib/module/MarkdownText.js +325 -0
  119. package/lib/module/MarkdownText.js.map +1 -0
  120. package/lib/module/MarkdownText.web.js +524 -0
  121. package/lib/module/MarkdownText.web.js.map +1 -0
  122. package/lib/module/MarkdownTextInput.js +24 -29
  123. package/lib/module/MarkdownTextInput.js.map +1 -1
  124. package/lib/module/MarkdownTextInput.web.js +12 -7
  125. package/lib/module/MarkdownTextInput.web.js.map +1 -1
  126. package/lib/module/atomUtils.js +106 -0
  127. package/lib/module/atomUtils.js.map +1 -0
  128. package/lib/module/childrenUtils.js +33 -0
  129. package/lib/module/childrenUtils.js.map +1 -0
  130. package/lib/module/index.js +1 -1
  131. package/lib/module/index.js.map +1 -1
  132. package/lib/module/math/environment.js +67 -0
  133. package/lib/module/math/environment.js.map +1 -0
  134. package/lib/module/math/index.js +56 -0
  135. package/lib/module/math/index.js.map +1 -0
  136. package/lib/module/math/mathjax.js +541 -0
  137. package/lib/module/math/mathjax.js.map +1 -0
  138. package/lib/module/metro.js +169 -0
  139. package/lib/module/metro.js.map +1 -0
  140. package/lib/module/parser/index.d.ts +3 -0
  141. package/lib/module/parser/index.js +450 -42
  142. package/lib/module/parser/index.js.map +4 -4
  143. package/lib/module/stripUtils.js +739 -0
  144. package/lib/module/stripUtils.js.map +1 -0
  145. package/lib/module/styleUtils.js +196 -25
  146. package/lib/module/styleUtils.js.map +1 -1
  147. package/lib/module/web/MarkdownElements.css +67 -0
  148. package/lib/module/web/MarkdownTextInput.css +9 -7
  149. package/lib/module/web/utils/blockLayout.js +94 -8
  150. package/lib/module/web/utils/blockLayout.js.map +1 -1
  151. package/lib/module/web/utils/blockUtils.js +328 -56
  152. package/lib/module/web/utils/blockUtils.js.map +1 -1
  153. package/lib/module/web/utils/elementUtils.js +75 -0
  154. package/lib/module/web/utils/elementUtils.js.map +1 -0
  155. package/lib/module/web/utils/parserUtils.js +86 -30
  156. package/lib/module/web/utils/parserUtils.js.map +1 -1
  157. package/lib/module/web/utils/treeUtils.js +6 -0
  158. package/lib/module/web/utils/treeUtils.js.map +1 -1
  159. package/lib/module/web/utils/webStyleUtils.js +25 -5
  160. package/lib/module/web/utils/webStyleUtils.js.map +1 -1
  161. package/lib/typescript/commonjs/e2e/links.test.d.ts +2 -0
  162. package/lib/typescript/commonjs/e2e/links.test.d.ts.map +1 -0
  163. package/lib/typescript/commonjs/src/MarcusTextDecoratorViewNativeComponent.d.ts +93 -0
  164. package/lib/typescript/commonjs/src/MarcusTextDecoratorViewNativeComponent.d.ts.map +1 -0
  165. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +64 -51
  166. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/src/MarkdownText.d.ts +127 -0
  168. package/lib/typescript/commonjs/src/MarkdownText.d.ts.map +1 -0
  169. package/lib/typescript/commonjs/src/MarkdownText.web.d.ts +126 -0
  170. package/lib/typescript/commonjs/src/MarkdownText.web.d.ts.map +1 -0
  171. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts +23 -1
  172. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts +26 -4
  174. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts.map +1 -1
  175. package/lib/typescript/commonjs/src/atomUtils.d.ts +58 -0
  176. package/lib/typescript/commonjs/src/atomUtils.d.ts.map +1 -0
  177. package/lib/typescript/commonjs/src/childrenUtils.d.ts +12 -0
  178. package/lib/typescript/commonjs/src/childrenUtils.d.ts.map +1 -0
  179. package/lib/typescript/commonjs/src/commonTypes.d.ts +39 -3
  180. package/lib/typescript/commonjs/src/commonTypes.d.ts.map +1 -1
  181. package/lib/typescript/commonjs/src/index.d.ts +2 -1
  182. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  183. package/lib/typescript/commonjs/src/math/environment.d.ts +7 -0
  184. package/lib/typescript/commonjs/src/math/environment.d.ts.map +1 -0
  185. package/lib/typescript/commonjs/src/math/index.d.ts +64 -0
  186. package/lib/typescript/commonjs/src/math/index.d.ts.map +1 -0
  187. package/lib/typescript/commonjs/src/math/mathjax.d.ts +63 -0
  188. package/lib/typescript/commonjs/src/math/mathjax.d.ts.map +1 -0
  189. package/lib/typescript/commonjs/src/metro.d.ts +59 -0
  190. package/lib/typescript/commonjs/src/metro.d.ts.map +1 -0
  191. package/lib/typescript/commonjs/src/parser/index.d.ts +3 -3
  192. package/lib/typescript/commonjs/src/parser/index.d.ts.map +1 -1
  193. package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts.map +1 -1
  194. package/lib/typescript/commonjs/src/rangeUtils.d.ts +1 -1
  195. package/lib/typescript/commonjs/src/stripUtils.d.ts +108 -0
  196. package/lib/typescript/commonjs/src/stripUtils.d.ts.map +1 -0
  197. package/lib/typescript/commonjs/src/styleUtils.d.ts +62 -4
  198. package/lib/typescript/commonjs/src/styleUtils.d.ts.map +1 -1
  199. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts +32 -4
  200. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts.map +1 -1
  201. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts +20 -8
  202. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts.map +1 -1
  203. package/lib/typescript/commonjs/src/web/utils/elementUtils.d.ts +37 -0
  204. package/lib/typescript/commonjs/src/web/utils/elementUtils.d.ts.map +1 -0
  205. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts +5 -5
  206. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts.map +1 -1
  207. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts +6 -1
  208. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts.map +1 -1
  209. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts +2 -2
  210. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts.map +1 -1
  211. package/lib/typescript/module/e2e/links.test.d.ts +2 -0
  212. package/lib/typescript/module/e2e/links.test.d.ts.map +1 -0
  213. package/lib/typescript/module/src/MarcusTextDecoratorViewNativeComponent.d.ts +93 -0
  214. package/lib/typescript/module/src/MarcusTextDecoratorViewNativeComponent.d.ts.map +1 -0
  215. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +64 -51
  216. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
  217. package/lib/typescript/module/src/MarkdownText.d.ts +127 -0
  218. package/lib/typescript/module/src/MarkdownText.d.ts.map +1 -0
  219. package/lib/typescript/module/src/MarkdownText.web.d.ts +126 -0
  220. package/lib/typescript/module/src/MarkdownText.web.d.ts.map +1 -0
  221. package/lib/typescript/module/src/MarkdownTextInput.d.ts +23 -1
  222. package/lib/typescript/module/src/MarkdownTextInput.d.ts.map +1 -1
  223. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts +26 -4
  224. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts.map +1 -1
  225. package/lib/typescript/module/src/atomUtils.d.ts +58 -0
  226. package/lib/typescript/module/src/atomUtils.d.ts.map +1 -0
  227. package/lib/typescript/module/src/childrenUtils.d.ts +12 -0
  228. package/lib/typescript/module/src/childrenUtils.d.ts.map +1 -0
  229. package/lib/typescript/module/src/commonTypes.d.ts +39 -3
  230. package/lib/typescript/module/src/commonTypes.d.ts.map +1 -1
  231. package/lib/typescript/module/src/index.d.ts +2 -1
  232. package/lib/typescript/module/src/index.d.ts.map +1 -1
  233. package/lib/typescript/module/src/math/environment.d.ts +7 -0
  234. package/lib/typescript/module/src/math/environment.d.ts.map +1 -0
  235. package/lib/typescript/module/src/math/index.d.ts +64 -0
  236. package/lib/typescript/module/src/math/index.d.ts.map +1 -0
  237. package/lib/typescript/module/src/math/mathjax.d.ts +63 -0
  238. package/lib/typescript/module/src/math/mathjax.d.ts.map +1 -0
  239. package/lib/typescript/module/src/metro.d.ts +59 -0
  240. package/lib/typescript/module/src/metro.d.ts.map +1 -0
  241. package/lib/typescript/module/src/parser/index.d.ts +3 -3
  242. package/lib/typescript/module/src/parser/index.d.ts.map +1 -1
  243. package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts.map +1 -1
  244. package/lib/typescript/module/src/rangeUtils.d.ts +1 -1
  245. package/lib/typescript/module/src/stripUtils.d.ts +108 -0
  246. package/lib/typescript/module/src/stripUtils.d.ts.map +1 -0
  247. package/lib/typescript/module/src/styleUtils.d.ts +62 -4
  248. package/lib/typescript/module/src/styleUtils.d.ts.map +1 -1
  249. package/lib/typescript/module/src/web/utils/blockLayout.d.ts +32 -4
  250. package/lib/typescript/module/src/web/utils/blockLayout.d.ts.map +1 -1
  251. package/lib/typescript/module/src/web/utils/blockUtils.d.ts +20 -8
  252. package/lib/typescript/module/src/web/utils/blockUtils.d.ts.map +1 -1
  253. package/lib/typescript/module/src/web/utils/elementUtils.d.ts +37 -0
  254. package/lib/typescript/module/src/web/utils/elementUtils.d.ts.map +1 -0
  255. package/lib/typescript/module/src/web/utils/parserUtils.d.ts +5 -5
  256. package/lib/typescript/module/src/web/utils/parserUtils.d.ts.map +1 -1
  257. package/lib/typescript/module/src/web/utils/treeUtils.d.ts +6 -1
  258. package/lib/typescript/module/src/web/utils/treeUtils.d.ts.map +1 -1
  259. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts +2 -2
  260. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts.map +1 -1
  261. package/package.json +29 -2
  262. package/react-native.config.js +8 -1
  263. package/src/MarcusTextDecoratorViewNativeComponent.ts +199 -0
  264. package/src/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
  265. package/src/MarkdownText.tsx +472 -0
  266. package/src/MarkdownText.web.tsx +668 -0
  267. package/src/MarkdownTextInput.tsx +42 -38
  268. package/src/MarkdownTextInput.web.tsx +36 -13
  269. package/src/atomUtils.ts +112 -0
  270. package/src/childrenUtils.ts +39 -0
  271. package/src/commonTypes.ts +69 -9
  272. package/src/index.tsx +2 -1
  273. package/src/math/environment.ts +68 -0
  274. package/src/math/index.ts +78 -0
  275. package/src/math/mathjax.ts +604 -0
  276. package/src/metro.ts +238 -0
  277. package/src/parser/index.ts +175 -10
  278. package/src/parser/micromark-extension-mentions.ts +104 -11
  279. package/src/stripUtils.ts +919 -0
  280. package/src/styleUtils.ts +290 -26
  281. package/src/web/MarkdownElements.css +67 -0
  282. package/src/web/MarkdownTextInput.css +9 -7
  283. package/src/web/utils/blockLayout.ts +124 -14
  284. package/src/web/utils/blockUtils.ts +365 -59
  285. package/src/web/utils/elementUtils.ts +87 -0
  286. package/src/web/utils/parserUtils.ts +119 -31
  287. package/src/web/utils/treeUtils.ts +6 -1
  288. package/src/web/utils/webStyleUtils.ts +32 -8
  289. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundColorSpan.kt +0 -6
  290. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBoldSpan.kt +0 -6
  291. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownItalicSpan.kt +0 -6
  292. package/apple/RCTMarcusStyle+Codegen.mm +0 -64
  293. package/apple/RCTMarcusUtils.h +0 -26
  294. package/apple/RCTMarcusUtils.mm +0 -26
@@ -1,27 +1,49 @@
1
1
  package app.getbullet.marcus
2
2
 
3
3
  import android.content.res.AssetManager
4
+ import android.graphics.Color
4
5
  import android.os.Trace
5
6
  import android.text.SpannableStringBuilder
6
7
  import android.text.Spanned
8
+ import android.text.TextDirectionHeuristics
7
9
  import android.text.TextPaint
10
+ import android.text.style.AbsoluteSizeSpan
8
11
  import android.text.style.MetricAffectingSpan
9
- import app.getbullet.marcus.spans.MarkdownBackgroundColorSpan
12
+ import androidx.annotation.ColorInt
13
+ import app.getbullet.marcus.spans.MarkdownAbsoluteLineHeightSpan
10
14
  import app.getbullet.marcus.spans.MarkdownBackgroundSpan
11
15
  import app.getbullet.marcus.spans.MarkdownBlockIndentSpan
12
- import app.getbullet.marcus.spans.MarkdownBoldSpan
16
+ import app.getbullet.marcus.spans.MarkdownBulletSpan
17
+ import app.getbullet.marcus.spans.MarkdownCodeBlockSpan
13
18
  import app.getbullet.marcus.spans.MarkdownFontFamilySpan
14
19
  import app.getbullet.marcus.spans.MarkdownFontSizeSpan
20
+ import app.getbullet.marcus.spans.MarkdownFontStyleSpan
21
+ import app.getbullet.marcus.spans.MarkdownFontWeightSpan
15
22
  import app.getbullet.marcus.spans.MarkdownForegroundColorSpan
16
23
  import app.getbullet.marcus.spans.MarkdownGapSpan
17
- import app.getbullet.marcus.spans.MarkdownItalicSpan
24
+ import app.getbullet.marcus.spans.MarkdownLetterSpacingSpan
18
25
  import app.getbullet.marcus.spans.MarkdownLineHeightSpan
19
26
  import app.getbullet.marcus.spans.MarkdownSpan
20
27
  import app.getbullet.marcus.spans.MarkdownStrikethroughSpan
28
+ import app.getbullet.marcus.spans.MarkdownTextShadowSpan
21
29
  import app.getbullet.marcus.spans.MarkdownUnderlineSpan
22
30
  import com.facebook.react.uimanager.PixelUtil
23
-
24
- class MarkdownFormatter(private val assetManager: AssetManager) {
31
+ import kotlin.math.ceil
32
+
33
+ /**
34
+ * @param backgroundPass set for an input, where the view paints the markdown
35
+ * backgrounds itself so that the caret stays visible, and handed on to the
36
+ * spans that would otherwise paint themselves. Null for a `Text`.
37
+ * @param display whether the list markers are rendered or shown. A `Text` draws
38
+ * them -- a bullet for an unordered item, the number at its own scale for an
39
+ * ordered one -- where an input shows the marker you typed, in the base font,
40
+ * because there it is text being edited rather than a rendering of it.
41
+ */
42
+ class MarkdownFormatter(
43
+ private val assetManager: AssetManager,
44
+ private val backgroundPass: MarkdownBackgroundPass? = null,
45
+ private val display: Boolean = false
46
+ ) {
25
47
 
26
48
  fun format(
27
49
  ssb: SpannableStringBuilder,
@@ -60,8 +82,11 @@ class MarkdownFormatter(private val assetManager: AssetManager) {
60
82
  Trace.beginSection("applyRanges")
61
83
  // Containers arrive per line, outermost first, each preceded by the run of
62
84
  // text its own marker takes up on that line.
63
- val line = BlockLayout(markdownStyle, textPaint)
85
+ val line = BlockLayout(markdownStyle, textPaint, display)
64
86
  var prefix: MarkdownRange? = null
87
+ // Boxed once the walk is over: a block's own indent is whatever its lines
88
+ // ended up with, and the containers around it are still being placed here.
89
+ val codeBlocks = mutableListOf<MarkdownRange>()
65
90
 
66
91
  for (markdownRange in markdownRanges) {
67
92
  if (markdownRange.type == "block-prefix") {
@@ -69,34 +94,248 @@ class MarkdownFormatter(private val assetManager: AssetManager) {
69
94
  continue
70
95
  }
71
96
 
97
+ if (markdownRange.type == "codeblock") {
98
+ codeBlocks.add(markdownRange)
99
+ continue
100
+ }
101
+
72
102
  val gutter = gutterOf(markdownRange, markdownStyle)
73
103
 
74
104
  if (gutter != null) {
75
105
  line.add(ssb, markdownRange, gutter, prefix)
76
106
  prefix = null
107
+
108
+ // The text of a quoted block, as opposed to the bar beside it.
109
+ // Applied here rather than in `applyRange` because a quote is a
110
+ // container and so never reaches it. It covers the line's markers as
111
+ // well as its prose, which is why it goes on before the `syntax`
112
+ // ranges that arrive later and paint over it.
113
+ if (markdownRange.type == "blockquote") {
114
+ applyTextStyle(ssb, markdownStyle.blockquote, markdownRange.start, markdownRange.end)
115
+ }
116
+
77
117
  continue
78
118
  }
79
119
 
80
120
  applyRange(ssb, markdownRange, markdownStyle)
81
121
  }
122
+
123
+ for (codeBlock in codeBlocks) {
124
+ box(ssb, codeBlock, markdownStyle)
125
+ }
126
+
127
+ // Last, so that a marker's own style wins over the `syntax` range that
128
+ // also covers it -- a display keeps that one range of markup and draws it,
129
+ // where everything else the colour is for has been stripped out. Only the
130
+ // scale reaches the width the text was indented past, so a marker style
131
+ // that changes the face or the size changes how the marker looks and not
132
+ // how much room it was given.
133
+ for ((start, end) in line.markerRuns) {
134
+ applyTextStyle(ssb, markdownStyle.orderedList.marker, start, end)
135
+ }
82
136
  } finally {
83
137
  Trace.endSection()
84
138
  }
85
139
  }
86
140
 
141
+ /**
142
+ * Boxes one fenced or indented block: the room its background takes up, and
143
+ * the background itself.
144
+ *
145
+ * Run after the container walk rather than during it, because a block is not a
146
+ * container -- its range covers every line at once, where a container arrives
147
+ * one line at a time -- and because what it indents by is whatever the
148
+ * containers around it left its lines at.
149
+ *
150
+ * The indent is added to the spans those containers already left on the block's
151
+ * lines, rather than laid on top as a second one: a line has one authority for
152
+ * how far in it starts, and a quote's ribbons are drawn from that same span.
153
+ */
154
+ private fun box(
155
+ ssb: SpannableStringBuilder,
156
+ markdownRange: MarkdownRange,
157
+ markdownStyle: MarkdownStyle
158
+ ) {
159
+ val (start, end) = body(ssb, markdownRange) ?: return
160
+
161
+ // Horizontally the room is an indent, vertically it is space held open
162
+ // above and below the block, so the two axes are worked out apart.
163
+ val insetX = PixelUtil.toPixelFromDIP(
164
+ markdownStyle.pre.paddingHorizontal + markdownStyle.pre.marginHorizontal
165
+ )
166
+ val marginHorizontal = PixelUtil.toPixelFromDIP(markdownStyle.pre.marginHorizontal)
167
+
168
+ // Where the box's near edge goes: the least far in any of the block's own
169
+ // lines start, taken before they are moved over to make room for the padding.
170
+ // A line that opens with a marker starts at its marker, so the box covers
171
+ // that -- the `>` of a quoted block is inside the block being quoted.
172
+ //
173
+ // An indent is measured from the edge the line begins at, so this is too, and
174
+ // `rightToLeft` below is what says which edge that is.
175
+ val indents = ssb.getSpans(start, end, MarkdownBlockIndentSpan::class.java)
176
+ var left = 0f
177
+
178
+ if (indents.isEmpty()) {
179
+ setSpan(
180
+ ssb,
181
+ MarkdownBlockIndentSpan().also {
182
+ it.firstLineIndent = insetX
183
+ it.indent = insetX
184
+ },
185
+ start,
186
+ end
187
+ )
188
+ } else {
189
+ left = indents.minOf { it.firstLineIndent }
190
+ for (indent in indents) {
191
+ indent.firstLineIndent += insetX
192
+ indent.indent += insetX
193
+ }
194
+ }
195
+
196
+ setSpan(
197
+ ssb,
198
+ MarkdownCodeBlockSpan(
199
+ markdownStyle.pre.backgroundColor ?: Color.TRANSPARENT,
200
+ PixelUtil.toPixelFromDIP(markdownStyle.pre.borderRadius),
201
+ PixelUtil.toPixelFromDIP(markdownStyle.pre.paddingVertical),
202
+ marginHorizontal,
203
+ PixelUtil.toPixelFromDIP(markdownStyle.pre.marginVertical),
204
+ left + marginHorizontal,
205
+ rightToLeft(ssb, start, end),
206
+ start,
207
+ end,
208
+ // A border that names no colour takes the run's own, which is the one
209
+ // colour the style is certain to have an opinion about.
210
+ markdownStyle.pre.borderColor ?: markdownStyle.pre.color,
211
+ PixelUtil.toPixelFromDIP(markdownStyle.pre.borderWidth),
212
+ backgroundPass
213
+ ),
214
+ start,
215
+ end
216
+ )
217
+ }
218
+
219
+ /**
220
+ * Whether a run of the text reads right to left, which is what says which edge
221
+ * the shapes drawn beside it hang off.
222
+ *
223
+ * The framework's own first-strong heuristic, and deliberately so: it is the
224
+ * same rule a `TextView` resolves a paragraph with by default, and so the same
225
+ * answer `MarkdownBlockIndentSpan` is handed as `dir` when it draws a quote's
226
+ * ribbons on the very same line. Reimplementing it here would be one more
227
+ * place for the bars and the box to disagree. iOS has to work the rule out
228
+ * itself, in `MarkdownWritingDirection`, only because UIKit keeps its answer.
229
+ *
230
+ * `FIRSTSTRONG_LTR` rather than the view's resolved heuristic, which the
231
+ * formatter has no view to ask for: the two differ only over text with no
232
+ * strong character at all -- code that is punctuation and digits throughout --
233
+ * and only in an app whose own layout direction is right to left.
234
+ */
235
+ private fun rightToLeft(ssb: SpannableStringBuilder, start: Int, end: Int): Boolean =
236
+ TextDirectionHeuristics.FIRSTSTRONG_LTR.isRtl(ssb, start, end - start)
237
+
238
+ /**
239
+ * Boxes one inline run -- a mention, or a run of code: the room its background
240
+ * takes up, and the background itself.
241
+ *
242
+ * The room is held open by widening the characters the run starts and ends
243
+ * with, which is the one way a line can be made to leave space around a run
244
+ * of itself: a paragraph span would move the whole line, and a background
245
+ * colour would simply sit under whatever is next to it. Only the horizontal
246
+ * lengths are held open that way; vertically the box grows into the line's own
247
+ * spacing instead -- one run cannot push the lines around it apart.
248
+ *
249
+ * The lengths arrive in dp, as they are written in a style.
250
+ */
251
+ private fun chip(
252
+ ssb: SpannableStringBuilder,
253
+ start: Int,
254
+ end: Int,
255
+ style: MarkdownBoxStyle
256
+ ) {
257
+ val padding = PixelUtil.toPixelFromDIP(style.paddingHorizontal)
258
+ val margin = PixelUtil.toPixelFromDIP(style.marginHorizontal)
259
+
260
+ setSpan(
261
+ ssb,
262
+ MarkdownBackgroundSpan(
263
+ style.backgroundColor ?: Color.TRANSPARENT,
264
+ PixelUtil.toPixelFromDIP(style.borderRadius),
265
+ start,
266
+ end,
267
+ PixelUtil.toPixelFromDIP(style.paddingVertical),
268
+ margin,
269
+ // A border that names no colour takes the run's own, which is the one
270
+ // colour the style is certain to have an opinion about.
271
+ style.borderColor ?: style.color,
272
+ PixelUtil.toPixelFromDIP(style.borderWidth),
273
+ backgroundPass
274
+ ),
275
+ start,
276
+ end
277
+ )
278
+
279
+ val gap = padding + margin
280
+ if (gap <= 0f) {
281
+ return
282
+ }
283
+
284
+ // The gap spans stand in for the characters they cover, so they draw them
285
+ // too and need the colour the run is drawn in.
286
+ val color = style.color ?: Color.BLACK
287
+
288
+ // Whole code points, so a gap never lands on half a surrogate pair.
289
+ val afterFirst = Character.offsetByCodePoints(ssb, start, 1)
290
+ val beforeLast = Character.offsetByCodePoints(ssb, end, -1)
291
+
292
+ if (afterFirst >= end) {
293
+ // One character long: it opens and closes the run at once.
294
+ setSpan(ssb, MarkdownGapSpan(gap, gap, color), start, end)
295
+ return
296
+ }
297
+
298
+ setSpan(ssb, MarkdownGapSpan(leading = gap, color = color), start, afterFirst)
299
+ setSpan(ssb, MarkdownGapSpan(trailing = gap, color = color), beforeLast, end)
300
+ }
301
+
302
+ /**
303
+ * The lines a block actually occupies: its range with the line breaks it opens
304
+ * and closes with taken off.
305
+ *
306
+ * A display renderer removes the opening fence and leaves the break that ended
307
+ * it, so a block's range there begins on the line above its first line of code.
308
+ * Boxing that break would paint a box around the line above.
309
+ */
310
+ private fun body(ssb: SpannableStringBuilder, markdownRange: MarkdownRange): Pair<Int, Int>? {
311
+ var start = markdownRange.start
312
+ var end = markdownRange.end
313
+
314
+ while (start < end && isLineBreak(ssb[start])) {
315
+ start++
316
+ }
317
+ while (end > start && isLineBreak(ssb[end - 1])) {
318
+ end--
319
+ }
320
+
321
+ return if (end > start) start to end else null
322
+ }
323
+
324
+ private fun isLineBreak(character: Char): Boolean = character == '\n' || character == '\r'
325
+
87
326
  /** The gutter a container reserves for itself, or null if it is not one. */
88
327
  private fun gutterOf(markdownRange: MarkdownRange, markdownStyle: MarkdownStyle): Float? {
89
328
  val step = when (markdownRange.type) {
90
329
  "blockquote" ->
91
- markdownStyle.blockquoteMarginLeft +
92
- markdownStyle.blockquoteBorderWidth +
93
- markdownStyle.blockquotePaddingLeft
330
+ markdownStyle.blockquote.marginStart +
331
+ markdownStyle.blockquote.borderWidth +
332
+ markdownStyle.blockquote.paddingStart
94
333
 
95
334
  "list-ordered" ->
96
- markdownStyle.orderedListMarginLeft + markdownStyle.orderedListPaddingLeft
335
+ markdownStyle.orderedList.marginStart + markdownStyle.orderedList.paddingStart
97
336
 
98
337
  "list-unordered" ->
99
- markdownStyle.unorderedListMarginLeft + markdownStyle.unorderedListPaddingLeft
338
+ markdownStyle.unorderedList.marginStart + markdownStyle.unorderedList.paddingStart
100
339
 
101
340
  else -> return null
102
341
  }
@@ -119,13 +358,28 @@ class MarkdownFormatter(private val assetManager: AssetManager) {
119
358
  */
120
359
  private class BlockLayout(
121
360
  private val markdownStyle: MarkdownStyle,
122
- private val textPaint: TextPaint?
361
+ private val textPaint: TextPaint?,
362
+ private val display: Boolean
123
363
  ) {
124
364
  /** Scratch paint, reset from [textPaint] before each measurement. */
125
365
  private val measurePaint = TextPaint()
126
366
 
367
+ /** The size the wrapped `Text` draws at, in pixels; -1 until it is asked for. */
368
+ private var baseSize = -1f
369
+
127
370
  /** Last marker seen for each container type, for the lines that continue it. */
128
371
  private val markers = HashMap<String, MarkdownRange>()
372
+
373
+ /**
374
+ * The ordered markers this walk drew, styled once it is over. See
375
+ * [applyRanges].
376
+ *
377
+ * Unordered ones are left out: an unordered marker has no glyph left to
378
+ * style -- its character is drawn clear and a circle put in its advance, in
379
+ * the same colour this would have applied -- and painting a foreground
380
+ * colour back over it would bring the character out of hiding.
381
+ */
382
+ val markerRuns = mutableListOf<Pair<Int, Int>>()
129
383
  private var lineStart = -1
130
384
  /** Offset reached so far, from the line's own left edge. */
131
385
  private var offset = 0f
@@ -150,13 +404,14 @@ class MarkdownFormatter(private val assetManager: AssetManager) {
150
404
  textStart = -1f
151
405
  padded = null
152
406
  span = MarkdownBlockIndentSpan(
153
- markdownStyle.blockquoteBorderColor,
154
- PixelUtil.toPixelFromDIP(markdownStyle.blockquoteBorderWidth),
407
+ markdownStyle.blockquote.borderColor ?: Color.TRANSPARENT,
408
+ PixelUtil.toPixelFromDIP(markdownStyle.blockquote.borderWidth),
155
409
  PixelUtil.toPixelFromDIP(
156
- markdownStyle.blockquoteMarginLeft +
157
- markdownStyle.blockquoteBorderWidth +
158
- markdownStyle.blockquotePaddingLeft
159
- )
410
+ markdownStyle.blockquote.marginStart +
411
+ markdownStyle.blockquote.borderWidth +
412
+ markdownStyle.blockquote.paddingStart
413
+ ),
414
+ PixelUtil.toPixelFromDIP(markdownStyle.blockquote.borderRadius)
160
415
  ).also {
161
416
  ssb.setSpan(it, markdownRange.start, markdownRange.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
162
417
  }
@@ -164,22 +419,24 @@ class MarkdownFormatter(private val assetManager: AssetManager) {
164
419
 
165
420
  val indent = span ?: return
166
421
 
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
- }
422
+ padded?.let { hold(ssb, it, gutter) }
175
423
 
176
424
  if (markdownRange.type == "blockquote") {
177
- indent.ribbonOffset = offset + PixelUtil.toPixelFromDIP(markdownStyle.blockquoteMarginLeft)
425
+ indent.ribbonOffset =
426
+ offset + PixelUtil.toPixelFromDIP(markdownStyle.blockquote.marginStart)
178
427
  indent.depth = markdownRange.depth
179
428
  }
180
429
 
181
430
  offset += gutter
182
431
 
432
+ // The room either side of a list marker is indent on the way in -- the
433
+ // marker moves right with the text -- and held open with a gap on the way
434
+ // out, which is the only thing that opens space after a character.
435
+ val marker = prefix ?: markers[markdownRange.type]
436
+ val padding = if (marker == null) 0f else markerPadding(markdownRange.type)
437
+
438
+ offset += padding
439
+
183
440
  if (prefix != null) {
184
441
  markers[markdownRange.type] = prefix
185
442
  if (textStart < 0f) {
@@ -187,8 +444,15 @@ class MarkdownFormatter(private val assetManager: AssetManager) {
187
444
  }
188
445
  }
189
446
 
190
- (prefix ?: markers[markdownRange.type])?.let {
191
- offset += measure(ssb, it.start, it.end)
447
+ if (marker != null) {
448
+ // Only decorated on the line the marker is written on: a line continuing
449
+ // the block reuses the one above, which has been drawn already.
450
+ offset += placeMarker(ssb, marker, markdownRange.type, prefix != null)
451
+ offset += padding
452
+
453
+ if (prefix != null) {
454
+ hold(ssb, marker, padding)
455
+ }
192
456
  }
193
457
 
194
458
  padded = prefix
@@ -196,6 +460,195 @@ class MarkdownFormatter(private val assetManager: AssetManager) {
196
460
  indent.indent = offset
197
461
  }
198
462
 
463
+ /**
464
+ * Holds `amount` of space open after a marker, adding to whatever is already
465
+ * holding some.
466
+ *
467
+ * The padding around a marker and the gutter of the container nested inside
468
+ * it land on the same character, and a second replacement span there would
469
+ * replace the first rather than add to it -- only one of them is ever asked
470
+ * for a width.
471
+ */
472
+ private fun hold(ssb: SpannableStringBuilder, marker: MarkdownRange, amount: Float) {
473
+ if (amount <= 0f) {
474
+ return
475
+ }
476
+
477
+ val slot = marker.end - 1
478
+
479
+ ssb.getSpans(slot, marker.end, MarkdownBulletSpan::class.java)
480
+ .firstOrNull { ssb.getSpanStart(it) == slot }
481
+ ?.let {
482
+ it.trailing += amount
483
+ return
484
+ }
485
+
486
+ ssb.getSpans(slot, marker.end, MarkdownGapSpan::class.java)
487
+ .firstOrNull { ssb.getSpanStart(it) == slot }
488
+ ?.let {
489
+ it.trailing += amount
490
+ return
491
+ }
492
+
493
+ ssb.setSpan(
494
+ MarkdownGapSpan(trailing = amount, color = markdownStyle.syntax.color ?: Color.BLACK),
495
+ slot,
496
+ marker.end,
497
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
498
+ )
499
+ }
500
+
501
+ /**
502
+ * Room held open either side of a list's marker, or nothing at all for a
503
+ * container whose marker is shown rather than rendered.
504
+ */
505
+ private fun markerPadding(type: String): Float {
506
+ if (!display) {
507
+ return 0f
508
+ }
509
+
510
+ return PixelUtil.toPixelFromDIP(
511
+ when (type) {
512
+ "list-ordered" -> markdownStyle.orderedList.markerPadding
513
+ "list-unordered" -> markdownStyle.unorderedList.markerPadding
514
+ else -> return 0f
515
+ }
516
+ )
517
+ }
518
+
519
+ /**
520
+ * The width a prefix's glyphs take up, drawing whatever a display draws its
521
+ * marker as on the way past.
522
+ *
523
+ * A display renders the marker rather than showing it: an unordered item's
524
+ * `-` becomes a circle, an ordered item's `1.` stays the number it means but
525
+ * at its own scale. Both change how wide the prefix comes out, and the
526
+ * containers nested inside it start after that width, so the two are worked
527
+ * out in one place.
528
+ */
529
+ private fun placeMarker(
530
+ ssb: SpannableStringBuilder,
531
+ marker: MarkdownRange,
532
+ type: String,
533
+ decorate: Boolean
534
+ ): Float {
535
+ val width = measure(ssb, marker.start, marker.end)
536
+
537
+ if (!display) {
538
+ return width
539
+ }
540
+
541
+ val run = markerRun(ssb, marker) ?: return width
542
+ val base = baseTextSize(ssb)
543
+
544
+ if (base <= 0f) {
545
+ return width
546
+ }
547
+
548
+ return when (type) {
549
+ "list-unordered" -> {
550
+ val diameter = base * markdownStyle.unorderedList.markerScale
551
+ // A whole pixel, because that is what the span can report as an
552
+ // advance, and the indent has to agree with it exactly.
553
+ val box = ceil(diameter).toInt()
554
+
555
+ if (decorate) {
556
+ ssb.setSpan(
557
+ MarkdownBulletSpan(
558
+ markdownStyle.unorderedList.marker.color ?: Color.BLACK,
559
+ diameter,
560
+ box
561
+ ),
562
+ run.first,
563
+ run.second,
564
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
565
+ )
566
+ }
567
+
568
+ // The marker's own glyph is not drawn, so the circle's width stands in
569
+ // for it and the rest of the prefix -- an indent in front, the space
570
+ // behind -- measures as it always did.
571
+ width - measure(ssb, run.first, run.second) + box
572
+ }
573
+
574
+ "list-ordered" -> {
575
+ if (!decorate) {
576
+ // The scaling is already on the string, so the width above is the
577
+ // one the number is drawn at.
578
+ return width
579
+ }
580
+
581
+ ssb.setSpan(
582
+ MarkdownFontSizeSpan(
583
+ PixelUtil.toDIPFromPixel(base * markdownStyle.orderedList.markerScale)
584
+ ),
585
+ run.first,
586
+ run.second,
587
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
588
+ )
589
+ markerRuns.add(run)
590
+
591
+ measure(ssb, marker.start, marker.end)
592
+ }
593
+
594
+ else -> width
595
+ }
596
+ }
597
+
598
+ /**
599
+ * The marker at the end of a prefix: the `-` of a bullet or the `1.` of a
600
+ * numbered item, without the indent in front of it or the space after it.
601
+ *
602
+ * Read off the text rather than taken from the `syntax` range covering it,
603
+ * because a prefix runs from wherever the previous container's marker ended
604
+ * and so can carry another container's marker with it -- the ordered list in
605
+ * `- > 1. ` is handed `> 1. `. The last run of non-blanks is this
606
+ * container's own.
607
+ */
608
+ private fun markerRun(ssb: SpannableStringBuilder, prefix: MarkdownRange): Pair<Int, Int>? {
609
+ var end = prefix.end
610
+ while (end > prefix.start && isBlank(ssb[end - 1])) {
611
+ end--
612
+ }
613
+
614
+ var start = end
615
+ while (start > prefix.start && !isBlank(ssb[start - 1])) {
616
+ start--
617
+ }
618
+
619
+ return if (end > start) start to end else null
620
+ }
621
+
622
+ private fun isBlank(character: Char): Boolean = character == ' ' || character == '\t'
623
+
624
+ /**
625
+ * The size the wrapped `Text` draws at, which is what a marker is drawn in
626
+ * proportion to.
627
+ *
628
+ * The view's own paint knows it; the measure path has no view, and takes it
629
+ * from the size React Native left over the whole string. Only a span
630
+ * covering all of it will do -- one covering part of it is a nested `Text`
631
+ * or a heading, neither of which is the base.
632
+ */
633
+ private fun baseTextSize(ssb: SpannableStringBuilder): Float {
634
+ if (baseSize >= 0f) {
635
+ return baseSize
636
+ }
637
+
638
+ baseSize = textPaint?.textSize
639
+ ?: ssb.getSpans(0, ssb.length, AbsoluteSizeSpan::class.java)
640
+ .firstOrNull {
641
+ it !is MarkdownSpan &&
642
+ ssb.getSpanStart(it) == 0 &&
643
+ ssb.getSpanEnd(it) == ssb.length
644
+ }
645
+ ?.size
646
+ ?.toFloat()
647
+ ?: 0f
648
+
649
+ return baseSize
650
+ }
651
+
199
652
  /**
200
653
  * Width a marker renders at.
201
654
  *
@@ -219,6 +672,28 @@ class MarkdownFormatter(private val assetManager: AssetManager) {
219
672
  }
220
673
  }
221
674
 
675
+ /**
676
+ * The entry a type is styled from, or null for a type that carries no text
677
+ * attributes of its own.
678
+ *
679
+ * A fenced block's language is its own type so that something can read it, but
680
+ * in an input it is part of the fence you are typing and reads as markup, so it
681
+ * is styled like the rest of it.
682
+ */
683
+ private fun textStyleOf(type: String, markdownStyle: MarkdownStyle): MarkdownTextStyle? =
684
+ when (type) {
685
+ "syntax", "language" -> markdownStyle.syntax
686
+ "link" -> markdownStyle.link
687
+ "strong" -> markdownStyle.strong
688
+ "emphasis" -> markdownStyle.emphasis
689
+ "strikethrough" -> markdownStyle.strikethrough
690
+ "heading" -> markdownStyle.heading
691
+ "code" -> markdownStyle.code
692
+ "pre" -> markdownStyle.pre
693
+ "mention" -> markdownStyle.mention
694
+ else -> null
695
+ }
696
+
222
697
  private fun applyRange(
223
698
  ssb: SpannableStringBuilder,
224
699
  markdownRange: MarkdownRange,
@@ -226,104 +701,168 @@ class MarkdownFormatter(private val assetManager: AssetManager) {
226
701
  ) {
227
702
  val start = markdownRange.start
228
703
  val end = markdownRange.end
229
- when (markdownRange.type) {
230
- "bold" -> setSpan(ssb, MarkdownBoldSpan(), start, end)
231
704
 
232
- "italic" -> setSpan(ssb, MarkdownItalicSpan(), start, end)
705
+ // Before the text style, because the size span it ends with has to be set
706
+ // after any line-height span or the line jumps as it is measured again.
707
+ if (markdownRange.type == "heading") {
708
+ applyHeadingLineHeight(ssb, markdownRange, markdownStyle, start, end)
709
+ }
233
710
 
234
- "strikethrough" -> setSpan(ssb, MarkdownStrikethroughSpan(), start, end)
711
+ textStyleOf(markdownRange.type, markdownStyle)?.let { style ->
712
+ applyTextStyle(
713
+ ssb,
714
+ style,
715
+ start,
716
+ end,
717
+ // A heading's size is the one thing a style cannot state outright: level
718
+ // N is the base size scaled N-1 times, and the level only arrives here.
719
+ fontSize = if (markdownRange.type == "heading") {
720
+ headingFontSize(markdownRange.depth, markdownStyle.heading)
721
+ } else {
722
+ null
723
+ }
724
+ )
725
+ }
235
726
 
727
+ when (markdownRange.type) {
236
728
  "emoji" -> {
237
- setSpan(
238
- ssb,
239
- MarkdownFontFamilySpan(markdownStyle.emojiFontFamily, assetManager),
240
- start,
241
- end
242
- )
243
- setSpan(ssb, MarkdownFontSizeSpan(markdownStyle.emojiFontSize), start, end)
729
+ markdownStyle.emoji.fontFamily?.let {
730
+ setSpan(ssb, MarkdownFontFamilySpan(it, assetManager), start, end)
731
+ }
732
+ if (markdownStyle.emoji.fontSize > 0f) {
733
+ setSpan(ssb, MarkdownFontSizeSpan(markdownStyle.emoji.fontSize), start, end)
734
+ }
244
735
  }
245
736
 
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
- }
737
+ "mention" -> chip(ssb, start, end, markdownStyle.mention)
260
738
 
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
- }
739
+ // `pre` gets only its text attributes: what a block is drawn on is a box
740
+ // behind the whole of it, applied from the `codeblock` range in box().
741
+ "code" -> chip(ssb, start, end, markdownStyle.code)
742
+ }
743
+ }
276
744
 
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
- }
745
+ /**
746
+ * Level N is the base size scaled N-1 times, so a single pair of style values
747
+ * covers all six. Null when the style names no size, which reads as unset.
748
+ */
749
+ private fun headingFontSize(depth: Int, style: MarkdownHeadingStyle): Float? {
750
+ if (style.fontSize <= 0f) {
751
+ return null
752
+ }
291
753
 
292
- "syntax" -> setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.syntaxColor), start, end)
754
+ var fontSize = style.fontSize
755
+ repeat(maxOf(depth, 1) - 1) { fontSize *= style.scale }
756
+ return fontSize
757
+ }
293
758
 
294
- "link" -> {
295
- setSpan(ssb, MarkdownUnderlineSpan(), start, end)
296
- setSpan(ssb, MarkdownForegroundColorSpan(markdownStyle.linkColor), start, end)
297
- }
759
+ /**
760
+ * Grows a heading's line so that the larger type has somewhere to sit.
761
+ *
762
+ * Only where the style has not asked for a line height of its own: an explicit
763
+ * `heading.lineHeight` is the author saying what the line should be, and two
764
+ * line-height spans on one line would fight.
765
+ */
766
+ private fun applyHeadingLineHeight(
767
+ ssb: SpannableStringBuilder,
768
+ markdownRange: MarkdownRange,
769
+ markdownStyle: MarkdownStyle,
770
+ start: Int,
771
+ end: Int
772
+ ) {
773
+ val style = markdownStyle.heading
774
+ val fontSize = headingFontSize(markdownRange.depth, style) ?: return
298
775
 
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
- }
776
+ if (style.lineHeight > 0f) {
777
+ return
778
+ }
305
779
 
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
- }
780
+ val lineHeight = ReactLineHeight.find(ssb)
781
+ if (lineHeight >= 0) {
782
+ setSpan(
783
+ ssb,
784
+ MarkdownLineHeightSpan(lineHeight * (fontSize / style.fontSize) * 1.5f),
785
+ start,
786
+ end
787
+ )
788
+ }
789
+ }
312
790
 
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
- }
791
+ /**
792
+ * Puts one text style on one run.
793
+ *
794
+ * One function rather than a case per type, because every styled type now
795
+ * carries the same set of attributes: what tells a mention from a heading is
796
+ * the box around it and the size it is given, not which properties it
797
+ * understands. `MarkdownFormatter.swift` applies the same set as attributed
798
+ * string attributes, and `blockUtils` writes it as CSS.
799
+ *
800
+ * Everything unset is left alone rather than written as a zero, so a run
801
+ * inherits whatever the prose around it is drawn in.
802
+ */
803
+ private fun applyTextStyle(
804
+ ssb: SpannableStringBuilder,
805
+ style: MarkdownTextStyle,
806
+ start: Int,
807
+ end: Int,
808
+ fontSize: Float? = null
809
+ ) {
810
+ if (end <= start) {
811
+ return
812
+ }
813
+
814
+ style.fontFamily?.let { setSpan(ssb, MarkdownFontFamilySpan(it, assetManager), start, end) }
815
+ style.fontWeight?.let { setSpan(ssb, MarkdownFontWeightSpan(it), start, end) }
816
+ style.fontStyle?.let { setSpan(ssb, MarkdownFontStyleSpan(it), start, end) }
817
+ style.color?.let { setSpan(ssb, MarkdownForegroundColorSpan(it), start, end) }
818
+
819
+ if (style.letterSpacing != 0f) {
820
+ setSpan(ssb, MarkdownLetterSpacingSpan(style.letterSpacing), start, end)
821
+ }
822
+
823
+ applyDecoration(ssb, style, start, end)
326
824
 
825
+ if (style.textShadowColor != null ||
826
+ style.textShadowRadius > 0f ||
827
+ style.textShadowOffsetX != 0f ||
828
+ style.textShadowOffsetY != 0f
829
+ ) {
830
+ setSpan(
831
+ ssb,
832
+ MarkdownTextShadowSpan(
833
+ style.textShadowColor ?: Color.BLACK,
834
+ style.textShadowOffsetX,
835
+ style.textShadowOffsetY,
836
+ style.textShadowRadius
837
+ ),
838
+ start,
839
+ end
840
+ )
841
+ }
842
+
843
+ // The line height goes on before the size, so that a line already told how
844
+ // tall to be is not measured again from the larger type and made to jump.
845
+ if (style.lineHeight > 0f) {
846
+ setSpan(ssb, MarkdownAbsoluteLineHeightSpan(style.lineHeight), start, end)
847
+ }
848
+
849
+ val size = fontSize ?: style.fontSize.takeIf { it > 0f }
850
+ size?.let { setSpan(ssb, MarkdownFontSizeSpan(it), start, end) }
851
+ }
852
+
853
+ private fun applyDecoration(
854
+ ssb: SpannableStringBuilder,
855
+ style: MarkdownTextStyle,
856
+ start: Int,
857
+ end: Int
858
+ ) {
859
+ val line = style.textDecorationLine ?: return
860
+
861
+ if (line.contains("underline")) {
862
+ setSpan(ssb, MarkdownUnderlineSpan(), start, end)
863
+ }
864
+ if (line.contains("line-through")) {
865
+ setSpan(ssb, MarkdownStrikethroughSpan(), start, end)
327
866
  }
328
867
  }
329
868