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
@@ -11,27 +11,50 @@ import UIKit
11
11
  /// Font resolution is injected rather than reached for, so this file carries
12
12
  /// no React dependency and can be compiled on its own. The Objective-C entry
13
13
  /// point in `MarkdownFormatter+React.swift` supplies the production one.
14
+ /// `resetAttributes` distinguishes the two callers. A text input formats the
15
+ /// same live storage over and over, so it starts from a clean slate each
16
+ /// time. A paragraph is handed a string that was just built from the shadow
17
+ /// tree, already carrying the per-fragment attributes of every nested `Text`
18
+ /// -- and, where a `View` was passed as a child, an attachment that a reset
19
+ /// would throw away.
20
+ ///
21
+ /// `display` says which of the two is being formatted, which only the list
22
+ /// markers care about: a display draws them -- a bullet for an unordered item,
23
+ /// the number at its own scale for an ordered one -- where an input shows the
24
+ /// marker you typed, in the base font, because there it is text being edited
25
+ /// rather than a rendering of it.
14
26
  public static func format(
15
27
  _ attributedString: NSMutableAttributedString,
16
28
  defaultTextAttributes: [NSAttributedString.Key: Any],
17
29
  ranges: [MarcusRange],
18
30
  style: RCTMarcusStyle,
19
- fonts: MarkdownFontProviding
31
+ fonts: MarkdownFontProviding,
32
+ resetAttributes: Bool = true,
33
+ display: Bool = false
20
34
  ) {
21
35
  let fullRange = NSRange(location: 0, length: attributedString.length)
22
36
 
23
37
  attributedString.beginEditing()
24
38
 
25
- attributedString.setAttributes(defaultTextAttributes, range: fullRange)
39
+ if resetAttributes {
40
+ attributedString.setAttributes(defaultTextAttributes, range: fullRange)
26
41
 
27
- // Forces the swizzled `_textOf:equals:` into string-only comparison.
28
- attributedString.addAttribute(.marcusText, value: true, range: fullRange)
42
+ // Forces the swizzled `_textOf:equals:` into string-only comparison.
43
+ attributedString.addAttribute(.marcusText, value: true, range: fullRange)
44
+ }
29
45
 
30
46
  // Containers arrive per line, outermost first, each preceded by the run of
31
47
  // text its own marker takes up on that line. Laying a line out is a walk
32
48
  // left to right: reserve a container's gutter, step over its marker, repeat.
33
- var line = BlockLayout()
49
+ var line = BlockLayout(
50
+ style: style,
51
+ display: display,
52
+ baseFontSize: (defaultTextAttributes[.font] as? UIFont)?.pointSize ?? 0
53
+ )
34
54
  var pendingPrefix: NSRange?
55
+ // Boxed once the walk is over: a block's own indent is whatever its lines
56
+ // ended up with, and the containers around it are still being placed here.
57
+ var codeBlocks: [NSRange] = []
35
58
 
36
59
  for markdownRange in ranges {
37
60
  if markdownRange.type == "block-prefix" {
@@ -39,6 +62,11 @@ import UIKit
39
62
  continue
40
63
  }
41
64
 
65
+ if markdownRange.type == "codeblock" {
66
+ codeBlocks.append(markdownRange.range)
67
+ continue
68
+ }
69
+
42
70
  if let gutter = gutter(
43
71
  for: markdownRange.type,
44
72
  depth: Int(markdownRange.depth),
@@ -54,6 +82,22 @@ import UIKit
54
82
  defaultTextAttributes: defaultTextAttributes
55
83
  )
56
84
  pendingPrefix = nil
85
+
86
+ // The text of a quoted block, as opposed to the bar beside it. Applied
87
+ // here rather than in `apply` because a quote is a container and so
88
+ // never reaches the range walk below. It covers the line's markers as
89
+ // well as its prose, which is why it goes on before the `syntax` ranges
90
+ // that arrive later and paint over it.
91
+ if markdownRange.type == "blockquote" {
92
+ applyTextStyle(
93
+ style.blockquote,
94
+ to: attributedString,
95
+ range: markdownRange.range,
96
+ defaultTextAttributes: defaultTextAttributes,
97
+ fonts: fonts
98
+ )
99
+ }
100
+
57
101
  continue
58
102
  }
59
103
 
@@ -63,15 +107,51 @@ import UIKit
63
107
  range: markdownRange.range,
64
108
  depth: Int(markdownRange.depth),
65
109
  style: style,
110
+ defaultTextAttributes: defaultTextAttributes,
111
+ fonts: fonts
112
+ )
113
+ }
114
+
115
+ for codeBlock in codeBlocks {
116
+ box(
117
+ codeBlock,
118
+ in: attributedString,
119
+ style: style,
120
+ defaultTextAttributes: defaultTextAttributes
121
+ )
122
+ }
123
+
124
+ // Last, so that a marker's own style wins over the `syntax` range that also
125
+ // covers it -- a display keeps that one range of markup and draws it, where
126
+ // everything else the colour is for has been stripped out. Only the scale
127
+ // reaches the width the text was indented past, so a marker style that
128
+ // changes the face or the size changes how the marker looks and not how much
129
+ // room it was given.
130
+ for marker in line.markerRuns {
131
+ applyTextStyle(
132
+ marker.style,
133
+ to: attributedString,
134
+ range: marker.range,
135
+ defaultTextAttributes: defaultTextAttributes,
66
136
  fonts: fonts
67
137
  )
68
138
  }
69
139
 
70
- (attributedString.string as NSString).enumerateSubstrings(
140
+ let text = attributedString.string as NSString
141
+
142
+ text.enumerateSubstrings(
71
143
  in: fullRange,
72
144
  options: [.byLines, .substringNotRequired]
73
- ) { _, _, enclosingRange, _ in
145
+ ) { _, lineRange, enclosingRange, _ in
74
146
  applyBaselineOffset(to: attributedString, range: enclosingRange)
147
+
148
+ applyWritingDirection(
149
+ to: attributedString,
150
+ line: lineRange,
151
+ enclosing: enclosingRange,
152
+ in: text,
153
+ defaultTextAttributes: defaultTextAttributes
154
+ )
75
155
  }
76
156
 
77
157
  attributedString.fixAttributes(in: fullRange)
@@ -81,174 +161,695 @@ import UIKit
81
161
 
82
162
  // MARK: - Per-range attributes
83
163
 
164
+ /// The entry a type is styled from, or nil for a type that carries no text
165
+ /// attributes of its own.
166
+ ///
167
+ /// A fenced block's language is its own type so that something can read it,
168
+ /// but in an input it is part of the fence you are typing and reads as markup,
169
+ /// so it is styled like the rest of it.
170
+ private static func textStyle(
171
+ for type: String,
172
+ style: RCTMarcusStyle
173
+ ) -> RCTMarcusTextStyle? {
174
+ switch type {
175
+ case "syntax", "language":
176
+ return style.syntax
177
+ case "link":
178
+ return style.link
179
+ case "strong":
180
+ return style.strong
181
+ case "emphasis":
182
+ return style.emphasis
183
+ case "strikethrough":
184
+ return style.strikethrough
185
+ case "heading":
186
+ return style.heading
187
+ case "code":
188
+ return style.code
189
+ case "pre":
190
+ return style.pre
191
+ case "mention":
192
+ return style.mention
193
+ default:
194
+ return nil
195
+ }
196
+ }
197
+
84
198
  private static func apply(
85
199
  _ type: String,
86
200
  to attributedString: NSMutableAttributedString,
87
201
  range: NSRange,
88
202
  depth: Int,
89
203
  style: RCTMarcusStyle,
204
+ defaultTextAttributes: [NSAttributedString.Key: Any],
90
205
  fonts: MarkdownFontProviding
91
206
  ) {
207
+ if let textStyle = textStyle(for: type, style: style) {
208
+ applyTextStyle(
209
+ textStyle,
210
+ to: attributedString,
211
+ range: range,
212
+ // A heading's size is the one thing a style cannot state outright: level
213
+ // N is the base size scaled N-1 times, and the level only arrives here.
214
+ fontSize: type == "heading" ? headingFontSize(depth, style.heading) : nil,
215
+ defaultTextAttributes: defaultTextAttributes,
216
+ fonts: fonts
217
+ )
218
+ }
219
+
92
220
  switch type {
93
- case "bold", "italic", "code", "pre", "heading", "emoji":
94
- var font =
95
- attributedString.attribute(
96
- .font,
97
- at: range.location,
98
- effectiveRange: nil
99
- ) as? UIFont
100
- switch type {
101
- case "bold":
102
- font = fonts.update(font, weight: "bold")
103
- case "italic":
104
- font = fonts.update(font, style: "italic")
105
- case "code":
106
- font = fonts.update(
107
- font,
108
- family: style.codeFontFamily,
109
- size: style.codeFontSize,
110
- weight: nil
111
- )
112
- case "pre":
113
- font = fonts.update(
114
- font,
115
- family: style.preFontFamily,
116
- size: style.preFontSize,
117
- weight: nil
118
- )
119
- case "heading":
120
- // Level N is the base size scaled N-1 times, so a single pair of style
121
- // values covers all six.
122
- var size = style.headingFontSize
123
- for _ in 1..<max(depth, 1) {
124
- size *= style.headingScale
125
- }
126
- font = fonts.update(font, family: nil, size: size, weight: "bold")
127
- case "emoji":
128
- font = fonts.update(
129
- font,
130
- family: style.emojiFontFamily,
131
- size: style.emojiFontSize,
132
- weight: nil
133
- )
134
- default:
135
- break
136
- }
137
- attributedString.addAttribute(.font, value: font as Any, range: range)
221
+ case "emoji":
222
+ applyEmoji(style.emoji, to: attributedString, range: range, fonts: fonts)
223
+
224
+ case "code":
225
+ chip(range, in: attributedString, style: style.code)
226
+
227
+ // Only the text attributes: what a block is drawn on is a box behind the
228
+ // whole of it, applied from the `codeblock` range in box(_:in:style:).
229
+ case "mention":
230
+ chip(range, in: attributedString, style: style.mention)
231
+
138
232
  default:
139
233
  break
140
234
  }
235
+ }
141
236
 
142
- switch type {
143
- case "syntax":
144
- attributedString.addAttribute(
145
- .foregroundColor,
146
- value: style.syntaxColor,
147
- range: range
148
- )
237
+ /// Level N is the base size scaled N-1 times, so a single pair of style values
238
+ /// covers all six. Zero when the style names no size, which reads as unset.
239
+ private static func headingFontSize(
240
+ _ depth: Int,
241
+ _ style: RCTMarcusHeadingStyle
242
+ ) -> CGFloat? {
243
+ guard style.fontSize > 0 else { return nil }
149
244
 
150
- case "strikethrough":
151
- attributedString.addAttribute(
152
- .strikethroughStyle,
153
- value: NSUnderlineStyle.single.rawValue,
154
- range: range
245
+ var size = style.fontSize
246
+ for _ in 1..<max(depth, 1) {
247
+ size *= style.scale
248
+ }
249
+ return size
250
+ }
251
+
252
+ private static func applyEmoji(
253
+ _ style: RCTMarcusEmojiStyle,
254
+ to attributedString: NSMutableAttributedString,
255
+ range: NSRange,
256
+ fonts: MarkdownFontProviding
257
+ ) {
258
+ let current =
259
+ attributedString.attribute(.font, at: range.location, effectiveRange: nil)
260
+ as? UIFont
261
+
262
+ guard
263
+ let font = fonts.update(
264
+ current,
265
+ family: style.fontFamily,
266
+ size: style.fontSize > 0 ? style.fontSize : (current?.pointSize ?? 0),
267
+ weight: nil
155
268
  )
269
+ else { return }
156
270
 
157
- case "code":
158
- attributedString.addAttribute(
159
- .foregroundColor,
160
- value: style.codeColor,
161
- range: range
271
+ attributedString.addAttribute(.font, value: font, range: range)
272
+ }
273
+
274
+ // MARK: - Text style
275
+
276
+ /// Puts one text style on one run.
277
+ ///
278
+ /// One function rather than a case per type, because every styled type now
279
+ /// carries the same set of attributes: what tells a mention from a heading is
280
+ /// the box around it and the size it is given, not which properties it
281
+ /// understands. `blockUtils` writes the same set as CSS, and
282
+ /// `MarkdownFormatter.kt` as spans.
283
+ ///
284
+ /// Everything unset is left alone rather than written as a zero, so a run
285
+ /// inherits whatever the prose around it is drawn in.
286
+ private static func applyTextStyle(
287
+ _ style: RCTMarcusTextStyle,
288
+ to attributedString: NSMutableAttributedString,
289
+ range: NSRange,
290
+ fontSize: CGFloat? = nil,
291
+ defaultTextAttributes: [NSAttributedString.Key: Any],
292
+ fonts: MarkdownFontProviding
293
+ ) {
294
+ guard range.length > 0, NSMaxRange(range) <= attributedString.length else {
295
+ return
296
+ }
297
+
298
+ applyFont(
299
+ style,
300
+ to: attributedString,
301
+ range: range,
302
+ fontSize: fontSize,
303
+ fonts: fonts
304
+ )
305
+
306
+ if let color = style.color {
307
+ attributedString.addAttribute(.foregroundColor, value: color, range: range)
308
+ }
309
+
310
+ applyLetterSpacing(style, to: attributedString, range: range)
311
+ applyDecoration(style, to: attributedString, range: range)
312
+ applyShadow(style, to: attributedString, range: range)
313
+ applyLineHeight(
314
+ style,
315
+ to: attributedString,
316
+ range: range,
317
+ defaultTextAttributes: defaultTextAttributes
318
+ )
319
+ }
320
+
321
+ /// The face, the size, the weight and the slant, in that order.
322
+ ///
323
+ /// The family and the size go together in one call because React Native's font
324
+ /// resolution takes them together, and the two traits follow so that a weight
325
+ /// asked for on top of a family survives the family being resolved.
326
+ private static func applyFont(
327
+ _ style: RCTMarcusTextStyle,
328
+ to attributedString: NSMutableAttributedString,
329
+ range: NSRange,
330
+ fontSize: CGFloat?,
331
+ fonts: MarkdownFontProviding
332
+ ) {
333
+ let size = fontSize ?? (style.fontSize > 0 ? style.fontSize : nil)
334
+
335
+ guard
336
+ size != nil || style.fontFamily != nil || style.fontWeight != nil
337
+ || style.fontStyle != nil
338
+ else { return }
339
+
340
+ var font =
341
+ attributedString.attribute(.font, at: range.location, effectiveRange: nil)
342
+ as? UIFont
343
+
344
+ if size != nil || style.fontFamily != nil {
345
+ font = fonts.update(
346
+ font,
347
+ family: style.fontFamily,
348
+ size: size ?? font?.pointSize ?? 0,
349
+ weight: nil
162
350
  )
351
+ }
352
+ if let weight = style.fontWeight {
353
+ font = fonts.update(font, weight: weight)
354
+ }
355
+ if let fontStyle = style.fontStyle {
356
+ font = fonts.update(font, style: fontStyle)
357
+ }
358
+
359
+ guard let font else { return }
360
+
361
+ attributedString.addAttribute(.font, value: font, range: range)
362
+ }
363
+
364
+ /// Added to whatever each character was already kerned by rather than written
365
+ /// over it, because kerning is also how a marker holds a gutter open and how
366
+ /// an inline box holds its own room: a run given letter spacing must not
367
+ /// swallow the space a container reserved after the marker it starts with.
368
+ private static func applyLetterSpacing(
369
+ _ style: RCTMarcusTextStyle,
370
+ to attributedString: NSMutableAttributedString,
371
+ range: NSRange
372
+ ) {
373
+ guard style.letterSpacing != 0 else { return }
374
+
375
+ var existing: [(NSRange, CGFloat)] = []
376
+ attributedString.enumerateAttribute(.kern, in: range, options: []) {
377
+ value, subrange, _ in
378
+ existing.append((subrange, CGFloat((value as? NSNumber)?.doubleValue ?? 0)))
379
+ }
380
+
381
+ for (subrange, kern) in existing {
163
382
  attributedString.addAttribute(
164
- .backgroundColor,
165
- value: style.codeBackgroundColor,
166
- range: range
383
+ .kern,
384
+ value: kern + style.letterSpacing,
385
+ range: subrange
167
386
  )
387
+ }
388
+ }
168
389
 
169
- case "mention-here":
170
- applyMention(
171
- to: attributedString,
172
- range: range,
173
- color: style.mentionHereColor,
174
- backgroundColor: style.mentionHereBackgroundColor,
175
- borderRadius: style.mentionHereBorderRadius
176
- )
390
+ private static func applyDecoration(
391
+ _ style: RCTMarcusTextStyle,
392
+ to attributedString: NSMutableAttributedString,
393
+ range: NSRange
394
+ ) {
395
+ guard let line = style.textDecorationLine, line != "none" else { return }
177
396
 
178
- case "mention-user":
179
- // TODO: change mention color when it mentions current user
180
- applyMention(
181
- to: attributedString,
182
- range: range,
183
- color: style.mentionUserColor,
184
- backgroundColor: style.mentionUserBackgroundColor,
185
- borderRadius: style.mentionUserBorderRadius
186
- )
397
+ let pattern = underlineStyle(style.textDecorationStyle).rawValue
187
398
 
188
- case "mention-report":
189
- applyMention(
190
- to: attributedString,
191
- range: range,
192
- color: style.mentionReportColor,
193
- backgroundColor: style.mentionReportBackgroundColor,
194
- borderRadius: style.mentionReportBorderRadius
195
- )
399
+ if line.contains("underline") {
400
+ attributedString.addAttribute(.underlineStyle, value: pattern, range: range)
401
+ if let color = style.textDecorationColor {
402
+ attributedString.addAttribute(.underlineColor, value: color, range: range)
403
+ }
404
+ }
196
405
 
197
- case "link":
406
+ if line.contains("line-through") {
198
407
  attributedString.addAttribute(
199
- .underlineStyle,
200
- value: NSUnderlineStyle.single.rawValue,
408
+ .strikethroughStyle,
409
+ value: pattern,
201
410
  range: range
202
411
  )
203
- attributedString.addAttribute(
204
- .foregroundColor,
205
- value: style.linkColor,
206
- range: range
412
+ if let color = style.textDecorationColor {
413
+ attributedString.addAttribute(
414
+ .strikethroughColor,
415
+ value: color,
416
+ range: range
417
+ )
418
+ }
419
+ }
420
+ }
421
+
422
+ /// The CSS names for a decoration's line, as the patterns TextKit draws.
423
+ private static func underlineStyle(_ name: String?) -> NSUnderlineStyle {
424
+ switch name {
425
+ case "double":
426
+ return [.double]
427
+ case "dotted":
428
+ return [.single, .patternDot]
429
+ case "dashed":
430
+ return [.single, .patternDash]
431
+ default:
432
+ return .single
433
+ }
434
+ }
435
+
436
+ private static func applyShadow(
437
+ _ style: RCTMarcusTextStyle,
438
+ to attributedString: NSMutableAttributedString,
439
+ range: NSRange
440
+ ) {
441
+ guard
442
+ style.textShadowColor != nil || style.textShadowRadius > 0
443
+ || style.textShadowOffset != .zero
444
+ else { return }
445
+
446
+ let shadow = NSShadow()
447
+ shadow.shadowColor = style.textShadowColor
448
+ shadow.shadowOffset = style.textShadowOffset
449
+ shadow.shadowBlurRadius = style.textShadowRadius
450
+
451
+ attributedString.addAttribute(.shadow, value: shadow, range: range)
452
+ }
453
+
454
+ /// Line height is a paragraph attribute, and TextKit reads the paragraph style
455
+ /// of a paragraph's first character, so a run that asks for one raises the
456
+ /// whole line it sits on rather than only itself. Android's `LineHeightSpan`
457
+ /// behaves the same way, and the type declares as much.
458
+ private static func applyLineHeight(
459
+ _ style: RCTMarcusTextStyle,
460
+ to attributedString: NSMutableAttributedString,
461
+ range: NSRange,
462
+ defaultTextAttributes: [NSAttributedString.Key: Any]
463
+ ) {
464
+ guard style.lineHeight > 0 else { return }
465
+
466
+ let paragraph = (attributedString.string as NSString).paragraphRange(for: range)
467
+ let indents = paragraphStyle(
468
+ of: attributedString,
469
+ at: paragraph.location,
470
+ defaultTextAttributes: defaultTextAttributes
471
+ )
472
+
473
+ indents.minimumLineHeight = style.lineHeight
474
+ indents.maximumLineHeight = style.lineHeight
475
+
476
+ attributedString.addAttribute(
477
+ .paragraphStyle,
478
+ value: indents,
479
+ range: paragraph
480
+ )
481
+ }
482
+
483
+ // MARK: - Inline boxes
484
+
485
+ /// Boxes one run of text -- a mention, or an inline run of code: the room its
486
+ /// background takes up, and the background itself.
487
+ ///
488
+ /// The room is kerning, which is the one way a line can be made to leave space
489
+ /// around a run of itself: a paragraph's indent would move the whole line, and
490
+ /// a background colour would simply sit under whatever is next to it. The
491
+ /// character before the run holds the space in front of it and the run's own
492
+ /// last character the space behind, so both sides come out of an advance
493
+ /// rather than out of a glyph. Only the horizontal lengths are held open that
494
+ /// way; vertically the box grows into the line's own spacing, because one run
495
+ /// cannot push the lines around it apart.
496
+ ///
497
+ /// Nothing is kerned in front of a run that opens a line -- there is no
498
+ /// character there to widen -- and its box grows into the margin instead.
499
+ private static func chip(
500
+ _ range: NSRange,
501
+ in attributedString: NSMutableAttributedString,
502
+ style: RCTMarcusBoxStyle
503
+ ) {
504
+ let background = MarkdownTextBackground()
505
+ background.color = style.backgroundColor ?? .clear
506
+ background.borderWidth = style.borderWidth
507
+ // A border that names no colour takes the run's own, which is the one colour
508
+ // the style is certain to have an opinion about.
509
+ background.borderColor =
510
+ style.borderWidth > 0 ? (style.borderColor ?? style.color) : nil
511
+ background.borderRadius = style.borderRadius
512
+ background.paddingHorizontal = style.paddingHorizontal
513
+ background.paddingVertical = style.paddingVertical
514
+ background.marginHorizontal = style.marginHorizontal
515
+ attributedString.addAttribute(
516
+ .marcusTextBackground,
517
+ value: background,
518
+ range: range
519
+ )
520
+
521
+ let gap = style.paddingHorizontal + style.marginHorizontal
522
+ guard gap > 0, range.length > 0 else { return }
523
+
524
+ let text = attributedString.string as NSString
525
+ widen(NSMaxRange(range) - 1, by: gap, in: attributedString)
526
+
527
+ let before = range.location - 1
528
+ if before >= 0, !isLineBreak(text.character(at: before)) {
529
+ widen(before, by: gap, in: attributedString)
530
+ }
531
+ }
532
+
533
+ /// Adds to whatever the character at `index` was already kerned by, so a run
534
+ /// that opens right after a container's marker keeps the gutter that marker
535
+ /// was holding open.
536
+ private static func widen(
537
+ _ index: Int,
538
+ by gap: CGFloat,
539
+ in attributedString: NSMutableAttributedString
540
+ ) {
541
+ guard index >= 0, index < attributedString.length else { return }
542
+
543
+ let existing =
544
+ (attributedString.attribute(.kern, at: index, effectiveRange: nil)
545
+ as? NSNumber)?.doubleValue ?? 0
546
+
547
+ attributedString.addAttribute(
548
+ .kern,
549
+ value: CGFloat(existing) + gap,
550
+ range: NSRange(location: index, length: 1)
551
+ )
552
+ }
553
+
554
+ // MARK: - Code blocks
555
+
556
+ /// Boxes one fenced or indented block: the room its background takes up, and
557
+ /// the background itself.
558
+ ///
559
+ /// Run after the container walk rather than during it, because a block is not
560
+ /// a container -- its range covers every line at once, where a container
561
+ /// arrives one line at a time -- and because what it indents by is whatever
562
+ /// the containers around it left its lines at.
563
+ ///
564
+ /// The padding and the margin are held open as indent on the leading edge and
565
+ /// as paragraph spacing above the first line and below the last, each axis
566
+ /// taking its own pair of lengths. Nothing is reserved on the trailing edge;
567
+ /// the box runs to the edge
568
+ /// of the text there, so a line long enough to wrap reaches the padding. A
569
+ /// paragraph cannot be given a trailing indent on Android at all, and a box
570
+ /// that stopped short on one platform only would be worse than one that
571
+ /// stops short on neither.
572
+ private static func box(
573
+ _ range: NSRange,
574
+ in attributedString: NSMutableAttributedString,
575
+ style: RCTMarcusStyle,
576
+ defaultTextAttributes: [NSAttributedString.Key: Any]
577
+ ) {
578
+ let text = attributedString.string as NSString
579
+ guard let body = body(of: range, in: text) else { return }
580
+
581
+ // Horizontally the room is an indent, vertically it is paragraph spacing, so
582
+ // the two axes are held open by different things and are worked out apart.
583
+ let insetX = style.pre.paddingHorizontal + style.pre.marginHorizontal
584
+ let insetY = style.pre.paddingVertical + style.pre.marginVertical
585
+ var paragraphs: [NSRange] = []
586
+ var location = body.location
587
+
588
+ while location < NSMaxRange(body) {
589
+ let paragraph = text.paragraphRange(
590
+ for: NSRange(location: location, length: 0)
207
591
  )
592
+ // A paragraph range always contains the location it was asked about, so
593
+ // this cannot loop; the guard is for a string that changed underneath.
594
+ guard paragraph.length > 0 else { break }
595
+ paragraphs.append(paragraph)
596
+ location = NSMaxRange(paragraph)
597
+ }
208
598
 
209
- case "pre":
210
- attributedString.addAttribute(
211
- .foregroundColor,
212
- value: style.preColor,
213
- range: range
599
+ // Where the box's near edge goes: the least far in any of the block's own
600
+ // lines start, taken before they are moved over to make room for the
601
+ // padding. A line that opens with a marker starts at its marker, so the box
602
+ // covers that -- the `>` of a quoted block is inside the block being
603
+ // quoted. A head indent is measured from the edge the line begins at, so
604
+ // this is too, and `block.rightToLeft` is what says which edge that is.
605
+ var left = CGFloat.greatestFiniteMagnitude
606
+
607
+ for (index, paragraph) in paragraphs.enumerated() {
608
+ let indents = paragraphStyle(
609
+ of: attributedString,
610
+ at: paragraph.location,
611
+ defaultTextAttributes: defaultTextAttributes
214
612
  )
215
- // A fenced block's range starts with the newline after the opening fence;
216
- // the background should not cover it.
217
- let text = attributedString.string as NSString
218
- let rangeForBackground =
219
- text.character(at: range.location) == unichar(10)
220
- ? NSRange(location: range.location + 1, length: range.length - 1)
221
- : range
613
+
614
+ left = min(left, indents.firstLineHeadIndent)
615
+ indents.firstLineHeadIndent += insetX
616
+ indents.headIndent += insetX
617
+
618
+ if index == 0 {
619
+ indents.paragraphSpacingBefore += insetY
620
+ }
621
+ if index == paragraphs.count - 1 {
622
+ indents.paragraphSpacing += insetY
623
+ }
624
+
222
625
  attributedString.addAttribute(
223
- .backgroundColor,
224
- value: style.preBackgroundColor,
225
- range: rangeForBackground
626
+ .paragraphStyle,
627
+ value: indents,
628
+ range: paragraph
226
629
  )
630
+ }
631
+
632
+ guard left < .greatestFiniteMagnitude else { return }
633
+
634
+ let block = MarkdownCodeBlock()
635
+ block.color = style.pre.backgroundColor ?? .clear
636
+ block.borderWidth = style.pre.borderWidth
637
+ block.borderColor =
638
+ style.pre.borderWidth > 0 ? (style.pre.borderColor ?? style.pre.color) : nil
639
+ block.borderRadius = style.pre.borderRadius
640
+ block.marginHorizontal = style.pre.marginHorizontal
641
+ block.marginVertical = style.pre.marginVertical
642
+ block.leadingInset = left + style.pre.marginHorizontal
643
+ // The block's own text, and all of it rather than its first line: a fence
644
+ // is punctuation and decides nothing, and it is present in an input and
645
+ // stripped from a display, so a rule that stopped at the first line would
646
+ // answer differently in the two. The code inside is the same either way.
647
+ block.rightToLeft = MarkdownWritingDirection.isRightToLeft(text, in: body)
648
+
649
+ attributedString.addAttribute(.marcusCodeBlock, value: block, range: body)
650
+ }
651
+
652
+ /// The lines a block actually occupies: its range with the line breaks it
653
+ /// opens and closes with taken off.
654
+ ///
655
+ /// A display renderer removes the opening fence and leaves the break that
656
+ /// ended it, so a block's range there begins on the line above its first line
657
+ /// of code. Boxing that break would paint a box around the line above.
658
+ private static func body(of range: NSRange, in text: NSString) -> NSRange? {
659
+ var start = range.location
660
+ var end = NSMaxRange(range)
661
+
662
+ while start < end, isLineBreak(text.character(at: start)) {
663
+ start += 1
664
+ }
665
+ while end > start, isLineBreak(text.character(at: end - 1)) {
666
+ end -= 1
667
+ }
668
+
669
+ return end > start
670
+ ? NSRange(location: start, length: end - start)
671
+ : nil
672
+ }
673
+
674
+ private static func isLineBreak(_ character: unichar) -> Bool {
675
+ character == unichar(10) || character == unichar(13)
676
+ }
677
+
678
+ // MARK: - List markers
679
+
680
+ /// A list marker that a display drew, and the style it is finished with once
681
+ /// the walk is over.
682
+ ///
683
+ /// Only ordered markers are recorded. An unordered one has no glyph left to
684
+ /// style -- its character is drawn clear and a circle put in its advance, in
685
+ /// the same colour this would have applied -- and painting a foreground colour
686
+ /// back over it would bring the character out of hiding.
687
+ private struct MarkerRun {
688
+ let range: NSRange
689
+ let style: RCTMarcusTextStyle
690
+ }
227
691
 
692
+ /// Room held open either side of a list's marker, or nothing at all for a
693
+ /// container whose marker is shown rather than rendered.
694
+ private static func markerPadding(
695
+ for type: String,
696
+ style: RCTMarcusStyle,
697
+ display: Bool
698
+ ) -> CGFloat {
699
+ guard display else { return 0 }
700
+
701
+ switch type {
702
+ case "list-ordered":
703
+ return style.orderedList.markerPadding
704
+ case "list-unordered":
705
+ return style.unorderedList.markerPadding
228
706
  default:
229
- break
707
+ return 0
230
708
  }
231
709
  }
232
710
 
233
- private static func applyMention(
234
- to attributedString: NSMutableAttributedString,
235
- range: NSRange,
711
+ /// The width a prefix's glyphs take up, drawing whatever a display draws its
712
+ /// marker as on the way past.
713
+ ///
714
+ /// A display renders the marker rather than showing it: an unordered item's
715
+ /// `-` becomes a circle, an ordered item's `1.` stays the number it means but
716
+ /// at its own scale. Both change how wide the prefix comes out, and the
717
+ /// containers nested inside it start after that width, so the two are worked
718
+ /// out in one place.
719
+ private static func placeMarker(
720
+ _ prefix: NSRange,
721
+ type: String,
722
+ decorate: Bool,
723
+ in attributedString: NSMutableAttributedString,
724
+ style: RCTMarcusStyle,
725
+ display: Bool,
726
+ baseFontSize: CGFloat,
727
+ runs: inout [MarkerRun]
728
+ ) -> CGFloat {
729
+ let text = attributedString.string as NSString
730
+
731
+ guard display, baseFontSize > 0, let marker = markerRun(prefix, in: text) else {
732
+ return measure(prefix, in: attributedString)
733
+ }
734
+
735
+ switch type {
736
+ case "list-unordered":
737
+ let diameter = baseFontSize * style.unorderedList.markerScale
738
+
739
+ if decorate {
740
+ bullet(
741
+ marker,
742
+ diameter: diameter,
743
+ color: style.unorderedList.marker.color ?? .clear,
744
+ in: attributedString
745
+ )
746
+ }
747
+
748
+ // The marker's own glyph is not drawn, so the circle's width stands in
749
+ // for it and the rest of the prefix -- an indent in front, the space
750
+ // behind -- measures as it always did.
751
+ return
752
+ (width(prefix, in: attributedString) - width(marker, in: attributedString)
753
+ + diameter).rounded(.up)
754
+
755
+ case "list-ordered":
756
+ if decorate {
757
+ scale(
758
+ marker,
759
+ to: baseFontSize * style.orderedList.markerScale,
760
+ in: attributedString
761
+ )
762
+ runs.append(MarkerRun(range: marker, style: style.orderedList.marker))
763
+ }
764
+
765
+ // Measured after the scaling, which is on the string by now, so this is
766
+ // the width the number will actually be drawn at.
767
+ return measure(prefix, in: attributedString)
768
+
769
+ default:
770
+ return measure(prefix, in: attributedString)
771
+ }
772
+ }
773
+
774
+ /// The marker at the end of a prefix: the `-` of a bullet or the `1.` of a
775
+ /// numbered item, without the indent in front of it or the space after it.
776
+ ///
777
+ /// Read off the text rather than taken from the `syntax` range covering it,
778
+ /// because a prefix runs from wherever the previous container's marker ended
779
+ /// and so can carry another container's marker with it -- the ordered list in
780
+ /// `- > 1. ` is handed `> 1. `. The last run of non-blanks is this
781
+ /// container's own.
782
+ private static func markerRun(_ prefix: NSRange, in text: NSString) -> NSRange? {
783
+ var end = NSMaxRange(prefix)
784
+
785
+ while end > prefix.location, isBlank(text.character(at: end - 1)) {
786
+ end -= 1
787
+ }
788
+
789
+ var start = end
790
+
791
+ while start > prefix.location, !isBlank(text.character(at: start - 1)) {
792
+ start -= 1
793
+ }
794
+
795
+ return end > start ? NSRange(location: start, length: end - start) : nil
796
+ }
797
+
798
+ /// Leaves the marker's glyph undrawn and puts a circle in the advance it had.
799
+ ///
800
+ /// The character stays in the string -- it is what a reader copies and what
801
+ /// VoiceOver announces -- so hiding the glyph is a matter of colour, and the
802
+ /// kerning is what makes its advance the circle's width whatever glyph the
803
+ /// item happened to be written with.
804
+ private static func bullet(
805
+ _ marker: NSRange,
806
+ diameter: CGFloat,
236
807
  color: UIColor,
237
- backgroundColor: UIColor,
238
- borderRadius: CGFloat
808
+ in attributedString: NSMutableAttributedString
239
809
  ) {
240
- attributedString.addAttribute(.foregroundColor, value: color, range: range)
810
+ let shape = MarkdownListBullet()
811
+ shape.color = color
812
+ shape.diameter = diameter
241
813
 
242
- let textBackground = MarkdownTextBackground()
243
- textBackground.color = backgroundColor
244
- textBackground.borderRadius = borderRadius
814
+ attributedString.addAttribute(.marcusListBullet, value: shape, range: marker)
245
815
  attributedString.addAttribute(
246
- .marcusTextBackground,
247
- value: textBackground,
248
- range: range
816
+ .foregroundColor,
817
+ value: UIColor.clear,
818
+ range: marker
819
+ )
820
+
821
+ widen(
822
+ NSMaxRange(marker) - 1,
823
+ by: diameter - width(marker, in: attributedString),
824
+ in: attributedString
249
825
  )
250
826
  }
251
827
 
828
+ /// Draws the marker at `size`, keeping the face it inherited.
829
+ private static func scale(
830
+ _ marker: NSRange,
831
+ to size: CGFloat,
832
+ in attributedString: NSMutableAttributedString
833
+ ) {
834
+ guard
835
+ let font = attributedString.attribute(
836
+ .font,
837
+ at: marker.location,
838
+ effectiveRange: nil
839
+ ) as? UIFont
840
+ else { return }
841
+
842
+ attributedString.addAttribute(
843
+ .font,
844
+ value: font.withSize(size),
845
+ range: marker
846
+ )
847
+ }
848
+
849
+ private static func isBlank(_ character: unichar) -> Bool {
850
+ character == unichar(32) || character == unichar(9)
851
+ }
852
+
252
853
  // MARK: - Block layout
253
854
 
254
855
  /// The gutter a container reserves for itself, or nil if the type is not one.
@@ -259,13 +860,13 @@ import UIKit
259
860
  ) -> CGFloat? {
260
861
  switch type {
261
862
  case "blockquote":
262
- return (style.blockquoteMarginLeft + style.blockquoteBorderWidth
263
- + style.blockquotePaddingLeft) * CGFloat(depth)
863
+ return (style.blockquote.marginStart + style.blockquote.borderWidth
864
+ + style.blockquote.paddingStart) * CGFloat(depth)
264
865
  case "list-ordered":
265
- return (style.orderedListMarginLeft + style.orderedListPaddingLeft)
866
+ return (style.orderedList.marginStart + style.orderedList.paddingStart)
266
867
  * CGFloat(depth)
267
868
  case "list-unordered":
268
- return (style.unorderedListMarginLeft + style.unorderedListPaddingLeft)
869
+ return (style.unorderedList.marginStart + style.unorderedList.paddingStart)
269
870
  * CGFloat(depth)
270
871
  default:
271
872
  return nil
@@ -284,8 +885,27 @@ import UIKit
284
885
  /// that opened it -- otherwise its text, and any ribbon beside it, would sit
285
886
  /// at a different offset than the line above and break the block in two.
286
887
  private struct BlockLayout {
888
+ let style: RCTMarcusStyle
889
+ /// Whether the markers are being rendered or shown; see `format`.
890
+ let display: Bool
891
+ /// The size the wrapped `Text` draws at, which is what a marker is drawn in
892
+ /// proportion to. Zero when the string carries no font at all, which leaves
893
+ /// the markers as they were typed.
894
+ let baseFontSize: CGFloat
895
+
287
896
  /// Last marker seen for each container type, for the lines that continue it.
288
897
  private var markers: [String: NSRange] = [:]
898
+ /// The markers this walk drew, styled once it is over. See `format`.
899
+ private(set) var markerRuns: [MarkerRun] = []
900
+
901
+ // Spelled out because a private type's memberwise initialiser is private to
902
+ // the type itself.
903
+ init(style: RCTMarcusStyle, display: Bool, baseFontSize: CGFloat) {
904
+ self.style = style
905
+ self.display = display
906
+ self.baseFontSize = baseFontSize
907
+ }
908
+
289
909
  private var lineStart = NSNotFound
290
910
  /// Offset reached so far, from the paragraph's own left edge.
291
911
  private var offset: CGFloat = 0
@@ -335,6 +955,16 @@ import UIKit
335
955
 
336
956
  offset += gutter
337
957
 
958
+ // The room either side of a list marker is indent on the way in -- the
959
+ // marker moves right with the text -- and held open with kerning on the
960
+ // way out, which is the only thing that opens space after a character.
961
+ let marker = prefix ?? markers[type]
962
+ let padding =
963
+ marker == nil
964
+ ? 0 : MarkdownFormatter.markerPadding(for: type, style: style, display: display)
965
+
966
+ offset += padding
967
+
338
968
  if let prefix {
339
969
  markers[type] = prefix
340
970
  if textStart == nil {
@@ -342,8 +972,24 @@ import UIKit
342
972
  }
343
973
  }
344
974
 
345
- if let marker = prefix ?? markers[type] {
346
- offset += MarkdownFormatter.measure(marker, in: attributedString)
975
+ if let marker {
976
+ offset += MarkdownFormatter.placeMarker(
977
+ marker,
978
+ type: type,
979
+ // Only on the line the marker is written on: a line continuing the
980
+ // block reuses the one above, which has been drawn already.
981
+ decorate: prefix != nil,
982
+ in: attributedString,
983
+ style: style,
984
+ display: display,
985
+ baseFontSize: baseFontSize,
986
+ runs: &markerRuns
987
+ )
988
+ offset += padding
989
+
990
+ if prefix != nil {
991
+ MarkdownFormatter.pad(marker, by: padding, in: attributedString)
992
+ }
347
993
  }
348
994
 
349
995
  padded = prefix
@@ -389,6 +1035,71 @@ import UIKit
389
1035
  attributedString.addAttribute(.paragraphStyle, value: style, range: range)
390
1036
  }
391
1037
 
1038
+ /// Settles which way one line reads and writes the answer down twice: into
1039
+ /// the paragraph style, which is what moves the text and its gutter, and
1040
+ /// under `.marcusRightToLeft`, which is what the ribbons and boxes drawn by
1041
+ /// hand are placed from. Two readers, one answer, and no way for the bar to
1042
+ /// end up on the far side of the text from the quote it marks.
1043
+ ///
1044
+ /// Writing it into the paragraph style at all is the part worth explaining.
1045
+ /// `NSWritingDirectionNatural` looks like it would do this by itself, and for
1046
+ /// the glyphs it does -- an Arabic line is laid out right to left and aligned
1047
+ /// to the right edge whatever the paragraph style says. What it does not do is
1048
+ /// turn the indents round: a head indent under a natural direction is applied
1049
+ /// from the left, so the quote's gutter opens on the side the text has just
1050
+ /// left and the text itself ends up flush against the container's right edge.
1051
+ /// Naming the direction is what makes `firstLineHeadIndent` mean the leading
1052
+ /// edge, which is what it means on the other two platforms already.
1053
+ ///
1054
+ /// A direction that was named before this ran is left alone and believed: it
1055
+ /// came from the `writingDirection` style prop, and an app that says which way
1056
+ /// its text reads is not overruled by a guess about the characters in it.
1057
+ private static func applyWritingDirection(
1058
+ to attributedString: NSMutableAttributedString,
1059
+ line: NSRange,
1060
+ enclosing: NSRange,
1061
+ in text: NSString,
1062
+ defaultTextAttributes: [NSAttributedString.Key: Any]
1063
+ ) {
1064
+ let declared = paragraphStyle(
1065
+ of: attributedString,
1066
+ at: enclosing.location,
1067
+ defaultTextAttributes: defaultTextAttributes
1068
+ ).baseWritingDirection
1069
+
1070
+ let rightToLeft =
1071
+ declared == .natural
1072
+ ? MarkdownWritingDirection.isRightToLeft(text, in: line)
1073
+ : declared == .rightToLeft
1074
+
1075
+ if declared == .natural {
1076
+ // Run by run rather than over the line in one go: a list nested inside a
1077
+ // quote indents its own share of the line, so one line can carry more
1078
+ // than one paragraph style and flattening them would lose an indent.
1079
+ attributedString.enumerateAttribute(
1080
+ .paragraphStyle,
1081
+ in: enclosing,
1082
+ options: []
1083
+ ) { _, range, _ in
1084
+ let style = paragraphStyle(
1085
+ of: attributedString,
1086
+ at: range.location,
1087
+ defaultTextAttributes: defaultTextAttributes
1088
+ )
1089
+ style.baseWritingDirection = rightToLeft ? .rightToLeft : .leftToRight
1090
+ attributedString.addAttribute(.paragraphStyle, value: style, range: range)
1091
+ }
1092
+ }
1093
+
1094
+ if rightToLeft {
1095
+ attributedString.addAttribute(
1096
+ .marcusRightToLeft,
1097
+ value: true,
1098
+ range: enclosing
1099
+ )
1100
+ }
1101
+ }
1102
+
392
1103
  private static func paragraphStyle(
393
1104
  of attributedString: NSAttributedString,
394
1105
  at location: Int,
@@ -410,6 +1121,10 @@ import UIKit
410
1121
 
411
1122
  /// Opens `width` of space after `range` without touching its glyphs, so a
412
1123
  /// nested container's gutter can sit between one marker and the next.
1124
+ ///
1125
+ /// Added to whatever that character already holds: a marker padded in a
1126
+ /// display is the same character a nested container then pads again, and the
1127
+ /// second must not replace the first.
413
1128
  private static func pad(
414
1129
  _ range: NSRange,
415
1130
  by width: CGFloat,
@@ -417,19 +1132,11 @@ import UIKit
417
1132
  ) {
418
1133
  guard width > 0, range.length > 0 else { return }
419
1134
 
420
- attributedString.addAttribute(
421
- .kern,
422
- value: width,
423
- range: NSRange(location: NSMaxRange(range) - 1, length: 1)
424
- )
1135
+ widen(NSMaxRange(range) - 1, by: width, in: attributedString)
425
1136
  }
426
1137
 
427
1138
  /// Width `range` takes up mid-line, rounded up to a whole point.
428
1139
  ///
429
- /// A sentinel is appended and its width taken back off again because `size()`
430
- /// measures the range as a line of its own and drops trailing whitespace --
431
- /// which a list-item prefix always ends in.
432
- ///
433
1140
  /// Rounding is what keeps a quote's ribbon in one piece. TextKit snaps every
434
1141
  /// line's origin to the device pixel grid, so a line indented past a marker
435
1142
  /// and a line indented past nothing land on different sides of a fractional
@@ -440,6 +1147,20 @@ import UIKit
440
1147
  private static func measure(
441
1148
  _ range: NSRange,
442
1149
  in attributedString: NSAttributedString
1150
+ ) -> CGFloat {
1151
+ width(range, in: attributedString).rounded(.up)
1152
+ }
1153
+
1154
+ /// The same width before it is rounded, for a caller adding to it: a marker
1155
+ /// that is part glyphs and part drawn shape rounds once, at the end, rather
1156
+ /// than once per part.
1157
+ ///
1158
+ /// A sentinel is appended and its width taken back off again because `size()`
1159
+ /// measures the range as a line of its own and drops trailing whitespace --
1160
+ /// which a list-item prefix always ends in.
1161
+ private static func width(
1162
+ _ range: NSRange,
1163
+ in attributedString: NSAttributedString
443
1164
  ) -> CGFloat {
444
1165
  let text = NSMutableAttributedString(
445
1166
  attributedString: attributedString.attributedSubstring(from: range)
@@ -460,7 +1181,7 @@ import UIKit
460
1181
  )
461
1182
  text.append(sentinel)
462
1183
 
463
- return (text.size().width - sentinel.size().width).rounded(.up)
1184
+ return text.size().width - sentinel.size().width
464
1185
  }
465
1186
 
466
1187
  // MARK: - Baseline