@mathers101/react-native-render-rich-text 0.1.10 → 0.1.14

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 (327) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +140 -140
  3. package/ReactNativeRenderRichText.podspec +23 -23
  4. package/android/build.gradle +86 -86
  5. package/android/consumer-rules.pro +3 -3
  6. package/android/src/main/AndroidManifest.xml +2 -2
  7. package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextLayoutManager.kt +24 -24
  8. package/android/src/main/java/com/renderrichtext/EnrichedTiptapTextPackage.kt +13 -13
  9. package/android/src/main/java/com/renderrichtext/MeasurementStore.kt +644 -644
  10. package/android/src/main/java/com/renderrichtext/accessibility/AccessibilityLabels.kt +17 -17
  11. package/android/src/main/java/com/renderrichtext/accessibility/AccessibleTiptapTextView.kt +112 -112
  12. package/android/src/main/java/com/renderrichtext/accessibility/TiptapAccessibilityHelper.kt +576 -576
  13. package/android/src/main/java/com/renderrichtext/events/CodeBlockPressEvent.kt +24 -24
  14. package/android/src/main/java/com/renderrichtext/events/ContextMenuItemPressEvent.kt +28 -28
  15. package/android/src/main/java/com/renderrichtext/events/CopyPressEvent.kt +24 -24
  16. package/android/src/main/java/com/renderrichtext/events/ImagePressEvent.kt +25 -25
  17. package/android/src/main/java/com/renderrichtext/events/LatexErrorEvent.kt +29 -29
  18. package/android/src/main/java/com/renderrichtext/events/LinkLongPressEvent.kt +23 -23
  19. package/android/src/main/java/com/renderrichtext/events/LinkPressEvent.kt +23 -23
  20. package/android/src/main/java/com/renderrichtext/events/MentionLongPressEvent.kt +25 -25
  21. package/android/src/main/java/com/renderrichtext/events/MentionPressEvent.kt +25 -25
  22. package/android/src/main/java/com/renderrichtext/events/TaskListItemPressEvent.kt +26 -26
  23. package/android/src/main/java/com/renderrichtext/math/LatexErrorReporter.kt +9 -9
  24. package/android/src/main/java/com/renderrichtext/parser/Parser.kt +59 -59
  25. package/android/src/main/java/com/renderrichtext/parser/TextLinkMatching.kt +35 -35
  26. package/android/src/main/java/com/renderrichtext/renderer/BaselineShiftRenderer.kt +45 -45
  27. package/android/src/main/java/com/renderrichtext/renderer/BlankLineRenderer.kt +39 -39
  28. package/android/src/main/java/com/renderrichtext/renderer/BlockquoteRenderer.kt +243 -243
  29. package/android/src/main/java/com/renderrichtext/renderer/BlockquoteTextRenderer.kt +56 -56
  30. package/android/src/main/java/com/renderrichtext/renderer/CodeBlockRenderer.kt +135 -135
  31. package/android/src/main/java/com/renderrichtext/renderer/CodeRenderer.kt +37 -37
  32. package/android/src/main/java/com/renderrichtext/renderer/EmphasisRenderer.kt +28 -28
  33. package/android/src/main/java/com/renderrichtext/renderer/HeadingRenderer.kt +81 -81
  34. package/android/src/main/java/com/renderrichtext/renderer/HighlightRenderer.kt +26 -26
  35. package/android/src/main/java/com/renderrichtext/renderer/LineBreakRenderer.kt +17 -17
  36. package/android/src/main/java/com/renderrichtext/renderer/LinkRenderer.kt +90 -90
  37. package/android/src/main/java/com/renderrichtext/renderer/ListContextManager.kt +109 -109
  38. package/android/src/main/java/com/renderrichtext/renderer/ListItemRenderer.kt +211 -211
  39. package/android/src/main/java/com/renderrichtext/renderer/ListRenderer.kt +133 -133
  40. package/android/src/main/java/com/renderrichtext/renderer/MentionRenderer.kt +85 -85
  41. package/android/src/main/java/com/renderrichtext/renderer/ParagraphRenderer.kt +91 -91
  42. package/android/src/main/java/com/renderrichtext/renderer/Renderer.kt +148 -148
  43. package/android/src/main/java/com/renderrichtext/renderer/SoftBreakRenderer.kt +17 -17
  44. package/android/src/main/java/com/renderrichtext/renderer/SpanStyleCache.kt +143 -143
  45. package/android/src/main/java/com/renderrichtext/renderer/StrikethroughRenderer.kt +28 -28
  46. package/android/src/main/java/com/renderrichtext/renderer/StrongRenderer.kt +28 -28
  47. package/android/src/main/java/com/renderrichtext/renderer/TextRenderer.kt +31 -31
  48. package/android/src/main/java/com/renderrichtext/renderer/ThematicBreakRenderer.kt +46 -46
  49. package/android/src/main/java/com/renderrichtext/renderer/UnderlineRenderer.kt +28 -28
  50. package/android/src/main/java/com/renderrichtext/spans/BaseListSpan.kt +164 -164
  51. package/android/src/main/java/com/renderrichtext/spans/BaselineShiftSpan.kt +27 -27
  52. package/android/src/main/java/com/renderrichtext/spans/BlockquoteSpan.kt +302 -302
  53. package/android/src/main/java/com/renderrichtext/spans/CodeBackgroundSpan.kt +205 -205
  54. package/android/src/main/java/com/renderrichtext/spans/CodeBlockSpan.kt +216 -216
  55. package/android/src/main/java/com/renderrichtext/spans/CodeSpan.kt +32 -32
  56. package/android/src/main/java/com/renderrichtext/spans/EmphasisSpan.kt +42 -42
  57. package/android/src/main/java/com/renderrichtext/spans/FadeInSpan.kt +29 -29
  58. package/android/src/main/java/com/renderrichtext/spans/HeadingSpan.kt +38 -38
  59. package/android/src/main/java/com/renderrichtext/spans/HighlightSpan.kt +80 -80
  60. package/android/src/main/java/com/renderrichtext/spans/ImageSpan.kt +481 -481
  61. package/android/src/main/java/com/renderrichtext/spans/InlineBackgroundGeometry.kt +42 -42
  62. package/android/src/main/java/com/renderrichtext/spans/LineHeightSpan.kt +39 -39
  63. package/android/src/main/java/com/renderrichtext/spans/LinkPillIconCache.kt +154 -154
  64. package/android/src/main/java/com/renderrichtext/spans/LinkPillSpan.kt +381 -381
  65. package/android/src/main/java/com/renderrichtext/spans/LinkSpan.kt +68 -68
  66. package/android/src/main/java/com/renderrichtext/spans/ListItemSpacingSpan.kt +33 -33
  67. package/android/src/main/java/com/renderrichtext/spans/ListMarkerAnchorSpan.kt +38 -38
  68. package/android/src/main/java/com/renderrichtext/spans/MarginBottomSpan.kt +76 -76
  69. package/android/src/main/java/com/renderrichtext/spans/MentionChipSpan.kt +132 -132
  70. package/android/src/main/java/com/renderrichtext/spans/MentionSpan.kt +46 -46
  71. package/android/src/main/java/com/renderrichtext/spans/OrderedListSpan.kt +127 -127
  72. package/android/src/main/java/com/renderrichtext/spans/SpoilerSpan.kt +33 -33
  73. package/android/src/main/java/com/renderrichtext/spans/StrikethroughSpan.kt +12 -12
  74. package/android/src/main/java/com/renderrichtext/spans/StrongSpan.kt +39 -39
  75. package/android/src/main/java/com/renderrichtext/spans/TaskListSpan.kt +143 -143
  76. package/android/src/main/java/com/renderrichtext/spans/TextSpan.kt +26 -26
  77. package/android/src/main/java/com/renderrichtext/spans/ThematicBreakSpan.kt +69 -69
  78. package/android/src/main/java/com/renderrichtext/spans/UnorderedListSpan.kt +96 -96
  79. package/android/src/main/java/com/renderrichtext/spoiler/SpoilerStubs.kt +46 -46
  80. package/android/src/main/java/com/renderrichtext/styles/BaseBlockStyle.kt +11 -11
  81. package/android/src/main/java/com/renderrichtext/styles/BlockquoteStyle.kt +83 -83
  82. package/android/src/main/java/com/renderrichtext/styles/CodeBlockStyle.kt +80 -80
  83. package/android/src/main/java/com/renderrichtext/styles/CodeStyle.kt +26 -26
  84. package/android/src/main/java/com/renderrichtext/styles/EmphasisStyle.kt +21 -21
  85. package/android/src/main/java/com/renderrichtext/styles/FactoidStyle.kt +21 -21
  86. package/android/src/main/java/com/renderrichtext/styles/HeadingStyle.kt +33 -33
  87. package/android/src/main/java/com/renderrichtext/styles/HighlightStyle.kt +19 -19
  88. package/android/src/main/java/com/renderrichtext/styles/ImageStyle.kt +29 -29
  89. package/android/src/main/java/com/renderrichtext/styles/InlineImageStyle.kt +17 -17
  90. package/android/src/main/java/com/renderrichtext/styles/InlineMathStyle.kt +17 -17
  91. package/android/src/main/java/com/renderrichtext/styles/LinkPillContent.kt +36 -36
  92. package/android/src/main/java/com/renderrichtext/styles/LinkPillStyle.kt +44 -44
  93. package/android/src/main/java/com/renderrichtext/styles/LinkStyle.kt +23 -23
  94. package/android/src/main/java/com/renderrichtext/styles/LinkVariantEntry.kt +34 -34
  95. package/android/src/main/java/com/renderrichtext/styles/ListStyle.kt +65 -65
  96. package/android/src/main/java/com/renderrichtext/styles/MathStyle.kt +29 -29
  97. package/android/src/main/java/com/renderrichtext/styles/MentionStyle.kt +19 -19
  98. package/android/src/main/java/com/renderrichtext/styles/ParagraphStyle.kt +33 -33
  99. package/android/src/main/java/com/renderrichtext/styles/SpoilerStyle.kt +27 -27
  100. package/android/src/main/java/com/renderrichtext/styles/StrikethroughStyle.kt +17 -17
  101. package/android/src/main/java/com/renderrichtext/styles/StrongStyle.kt +21 -21
  102. package/android/src/main/java/com/renderrichtext/styles/StyleConfig.kt +411 -411
  103. package/android/src/main/java/com/renderrichtext/styles/StyleParser.kt +92 -92
  104. package/android/src/main/java/com/renderrichtext/styles/SubscriptStyle.kt +16 -16
  105. package/android/src/main/java/com/renderrichtext/styles/SuperscriptStyle.kt +16 -16
  106. package/android/src/main/java/com/renderrichtext/styles/TableStyle.kt +75 -75
  107. package/android/src/main/java/com/renderrichtext/styles/TaskListStyle.kt +29 -29
  108. package/android/src/main/java/com/renderrichtext/styles/TextAlignment.kt +32 -32
  109. package/android/src/main/java/com/renderrichtext/styles/ThematicBreakStyle.kt +23 -23
  110. package/android/src/main/java/com/renderrichtext/styles/UnderlineStyle.kt +17 -17
  111. package/android/src/main/java/com/renderrichtext/styles/VideoStyle.kt +31 -31
  112. package/android/src/main/java/com/renderrichtext/utils/common/BorderStyleUtils.kt +86 -86
  113. package/android/src/main/java/com/renderrichtext/utils/common/BreakStrategyUtils.kt +32 -32
  114. package/android/src/main/java/com/renderrichtext/utils/common/CodeBlockHighlighter.kt +36 -36
  115. package/android/src/main/java/com/renderrichtext/utils/common/CodeBlockNode.kt +42 -42
  116. package/android/src/main/java/com/renderrichtext/utils/common/EllipsizeUtils.kt +27 -27
  117. package/android/src/main/java/com/renderrichtext/utils/common/EnrichedTiptapAncestorUtils.kt +10 -10
  118. package/android/src/main/java/com/renderrichtext/utils/common/FeatureFlags.kt +9 -9
  119. package/android/src/main/java/com/renderrichtext/utils/common/LinkRecognitionProps.kt +31 -31
  120. package/android/src/main/java/com/renderrichtext/utils/common/LinkRegexConfig.kt +56 -56
  121. package/android/src/main/java/com/renderrichtext/utils/common/MotionPreferences.kt +18 -18
  122. package/android/src/main/java/com/renderrichtext/utils/common/ReadableMapExtensions.kt +28 -28
  123. package/android/src/main/java/com/renderrichtext/utils/common/layout/RTLUtils.kt +6 -6
  124. package/android/src/main/java/com/renderrichtext/utils/common/serialization/TiptapASTSerializer.kt +368 -368
  125. package/android/src/main/java/com/renderrichtext/utils/text/ImageCache.kt +88 -88
  126. package/android/src/main/java/com/renderrichtext/utils/text/ImageDownloader.kt +203 -203
  127. package/android/src/main/java/com/renderrichtext/utils/text/LocalImageLoader.kt +167 -167
  128. package/android/src/main/java/com/renderrichtext/utils/text/TailFadeInAnimator.kt +84 -84
  129. package/android/src/main/java/com/renderrichtext/utils/text/conversion/HTMLGenerator.kt +1105 -1105
  130. package/android/src/main/java/com/renderrichtext/utils/text/conversion/TiptapExtractor.kt +423 -423
  131. package/android/src/main/java/com/renderrichtext/utils/text/extensions/ParagraphUtils.kt +9 -9
  132. package/android/src/main/java/com/renderrichtext/utils/text/extensions/SpannableExtensions.kt +9 -9
  133. package/android/src/main/java/com/renderrichtext/utils/text/extensions/TextPaintExtensions.kt +76 -76
  134. package/android/src/main/java/com/renderrichtext/utils/text/interaction/CheckboxTouchHelper.kt +61 -61
  135. package/android/src/main/java/com/renderrichtext/utils/text/interaction/TaskListTapUtils.kt +286 -286
  136. package/android/src/main/java/com/renderrichtext/utils/text/span/ApplyLineHeight.kt +39 -39
  137. package/android/src/main/java/com/renderrichtext/utils/text/span/BlockSpacing.kt +142 -142
  138. package/android/src/main/java/com/renderrichtext/utils/text/span/SpanFlags.kt +43 -43
  139. package/android/src/main/java/com/renderrichtext/utils/text/span/WidthAwareSpans.kt +30 -30
  140. package/android/src/main/java/com/renderrichtext/utils/text/view/CodeBlockPressHost.kt +15 -15
  141. package/android/src/main/java/com/renderrichtext/utils/text/view/ImagePressHost.kt +19 -19
  142. package/android/src/main/java/com/renderrichtext/utils/text/view/SelectionActionMode.kt +235 -235
  143. package/android/src/main/java/com/renderrichtext/utils/text/view/TextSelectionColors.kt +48 -48
  144. package/android/src/main/java/com/renderrichtext/utils/text/view/TextViewSetup.kt +119 -119
  145. package/android/src/main/java/com/renderrichtext/views/ContextMenuPopup.kt +217 -217
  146. package/android/src/main/java/com/renderrichtext/views/CopyGlyph.kt +101 -101
  147. package/android/src/main/java/com/renderrichtext/views/WrapGlyph.kt +40 -40
  148. package/android/src/main/jni/CMakeLists.txt +52 -52
  149. package/android/src/main/jni/react/renderer/components/RichTextSpec/ComponentDescriptors.h +30 -30
  150. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextComponentDescriptor.h +29 -29
  151. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextMeasurementManager.cpp +45 -45
  152. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextMeasurementManager.h +21 -21
  153. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextShadowNode.cpp +38 -38
  154. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextShadowNode.h +53 -53
  155. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextState.cpp +9 -9
  156. package/android/src/main/jni/react/renderer/components/RichTextSpec/TiptapTextState.h +24 -24
  157. package/android/src/main/jni/react/renderer/components/RichTextSpec/conversions.h +40 -40
  158. package/cpp/parser/TiptapParser.hpp +13 -13
  159. package/cpp/vendor/nlohmann/json.hpp +24765 -24765
  160. package/ios/EnrichedTiptapText.h +27 -27
  161. package/ios/attachments/ENRMImageAttachment.h +51 -51
  162. package/ios/attachments/ENRMImageAttachment.m +398 -398
  163. package/ios/attachments/ENRMLinkPillAttachment.h +47 -47
  164. package/ios/attachments/ENRMLinkPillAttachment.m +324 -324
  165. package/ios/attachments/ENRMLinkPillIconCache.h +28 -28
  166. package/ios/attachments/ENRMLinkPillIconCache.m +181 -181
  167. package/ios/attachments/ENRMLinkPillText.h +27 -27
  168. package/ios/attachments/ENRMLinkPillText.m +118 -118
  169. package/ios/attachments/ENRMMathInlineAttachment.h +7 -7
  170. package/ios/attachments/ENRMMathInlineAttachment.m +4 -4
  171. package/ios/attachments/ThematicBreakAttachment.h +15 -15
  172. package/ios/attachments/ThematicBreakAttachment.m +33 -33
  173. package/ios/internals/ENRMViewFreeMeasurement.h +208 -208
  174. package/ios/internals/EnrichedTiptapTextComponentDescriptor.h +19 -19
  175. package/ios/internals/EnrichedTiptapTextShadowNode.h +43 -43
  176. package/ios/internals/EnrichedTiptapTextShadowNode.mm +69 -69
  177. package/ios/internals/EnrichedTiptapTextState.h +24 -24
  178. package/ios/internals/MeasurementCache.h +308 -308
  179. package/ios/internals/ShadowMeasurementUtils.h +145 -145
  180. package/ios/parser/ENRMTextLinkAttributes.h +7 -7
  181. package/ios/parser/ENRMTiptapParser.h +33 -33
  182. package/ios/parser/ENRMTiptapParser.mm +73 -73
  183. package/ios/parser/TiptapASTNode.m +52 -52
  184. package/ios/renderer/AttributedRenderer.h +23 -23
  185. package/ios/renderer/AttributedRenderer.m +229 -229
  186. package/ios/renderer/BaseRenderer.h +33 -33
  187. package/ios/renderer/BaseRenderer.m +46 -46
  188. package/ios/renderer/BaselineShiftTextAttributes.h +10 -10
  189. package/ios/renderer/BaselineShiftTextAttributes.m +26 -26
  190. package/ios/renderer/BlankLineRenderer.h +4 -4
  191. package/ios/renderer/BlankLineRenderer.m +51 -51
  192. package/ios/renderer/BlockquoteRenderer.h +6 -6
  193. package/ios/renderer/BlockquoteRenderer.m +258 -258
  194. package/ios/renderer/CodeBlockRenderer.h +9 -9
  195. package/ios/renderer/CodeBlockRenderer.m +99 -99
  196. package/ios/renderer/CodeRenderer.h +9 -9
  197. package/ios/renderer/CodeRenderer.m +69 -69
  198. package/ios/renderer/ENRMBlockquoteTextRenderer.h +25 -25
  199. package/ios/renderer/ENRMBlockquoteTextRenderer.m +48 -48
  200. package/ios/renderer/EmphasisRenderer.h +5 -5
  201. package/ios/renderer/EmphasisRenderer.m +99 -99
  202. package/ios/renderer/HeadingRenderer.h +6 -6
  203. package/ios/renderer/HeadingRenderer.m +102 -102
  204. package/ios/renderer/HighlightRenderer.h +7 -7
  205. package/ios/renderer/HighlightRenderer.m +33 -33
  206. package/ios/renderer/LinkRenderer.h +6 -6
  207. package/ios/renderer/LinkRenderer.m +157 -157
  208. package/ios/renderer/ListItemRenderer.h +24 -24
  209. package/ios/renderer/ListItemRenderer.m +283 -283
  210. package/ios/renderer/ListRenderer.h +13 -13
  211. package/ios/renderer/ListRenderer.m +126 -126
  212. package/ios/renderer/MentionRenderer.h +9 -9
  213. package/ios/renderer/MentionRenderer.m +96 -96
  214. package/ios/renderer/NodeRenderer.h +8 -8
  215. package/ios/renderer/ParagraphRenderer.h +6 -6
  216. package/ios/renderer/ParagraphRenderer.m +85 -85
  217. package/ios/renderer/RenderContext.h +145 -145
  218. package/ios/renderer/RenderContext.m +402 -402
  219. package/ios/renderer/RendererFactory.h +12 -12
  220. package/ios/renderer/StrikethroughRenderer.h +5 -5
  221. package/ios/renderer/StrikethroughRenderer.m +30 -30
  222. package/ios/renderer/StrongRenderer.h +5 -5
  223. package/ios/renderer/StrongRenderer.m +87 -87
  224. package/ios/renderer/TextRenderer.h +6 -6
  225. package/ios/renderer/TextRenderer.m +18 -18
  226. package/ios/renderer/ThematicBreakRenderer.h +4 -4
  227. package/ios/renderer/ThematicBreakRenderer.m +41 -41
  228. package/ios/renderer/UnderlineRenderer.h +4 -4
  229. package/ios/renderer/UnderlineRenderer.m +41 -41
  230. package/ios/styles/ENRMFontSlot.h +12 -12
  231. package/ios/styles/ENRMFontSlot.m +24 -24
  232. package/ios/styles/StyleConfig.h +488 -488
  233. package/ios/styles/StyleConfig.mm +2820 -2820
  234. package/ios/utils/AccessibilityInfo.h +35 -35
  235. package/ios/utils/AccessibilityInfo.m +24 -24
  236. package/ios/utils/BlockquoteBorder.h +26 -26
  237. package/ios/utils/BlockquoteBorder.m +294 -294
  238. package/ios/utils/CodeBackground.h +19 -19
  239. package/ios/utils/CodeBackground.m +196 -196
  240. package/ios/utils/CodeBlockBackground.h +17 -17
  241. package/ios/utils/CodeBlockBackground.m +102 -102
  242. package/ios/utils/ContextMenuUtils.h +76 -76
  243. package/ios/utils/ContextMenuUtils.mm +75 -75
  244. package/ios/utils/ENRMAccessibilityLabels.h +40 -40
  245. package/ios/utils/ENRMAccessibilityLabels.m +5 -5
  246. package/ios/utils/ENRMAsyncRenderCoordinator.h +24 -24
  247. package/ios/utils/ENRMAsyncRenderCoordinator.m +37 -37
  248. package/ios/utils/ENRMAtomicSize.h +65 -65
  249. package/ios/utils/ENRMCodeBlockContent.h +37 -37
  250. package/ios/utils/ENRMCodeBlockContent.mm +89 -89
  251. package/ios/utils/ENRMFeatureFlags.h +25 -25
  252. package/ios/utils/ENRMImageDownloader.h +30 -30
  253. package/ios/utils/ENRMImageDownloader.m +168 -168
  254. package/ios/utils/ENRMLatexErrorCoordinator.h +27 -27
  255. package/ios/utils/ENRMLatexErrorCoordinator.mm +75 -75
  256. package/ios/utils/ENRMLatexErrorReporting.h +13 -13
  257. package/ios/utils/ENRMLinkContextMenus.h +93 -93
  258. package/ios/utils/ENRMLinkContextMenus.m +178 -178
  259. package/ios/utils/ENRMLinkRegexConfig.h +30 -30
  260. package/ios/utils/ENRMLinkRegexConfig.mm +104 -104
  261. package/ios/utils/ENRMLinkRegexProps.h +23 -23
  262. package/ios/utils/ENRMLocalImageLoader.h +35 -35
  263. package/ios/utils/ENRMLocalImageLoader.m +111 -111
  264. package/ios/utils/ENRMMenuAction.h +27 -27
  265. package/ios/utils/ENRMMenuAction.m +33 -33
  266. package/ios/utils/ENRMSpoilerOverlayManager.h +15 -15
  267. package/ios/utils/ENRMSpoilerOverlayManager.m +19 -19
  268. package/ios/utils/ENRMSpoilerTapUtils.h +6 -6
  269. package/ios/utils/ENRMSpoilerTapUtils.m +11 -11
  270. package/ios/utils/ENRMTailFadeInAnimator.h +16 -16
  271. package/ios/utils/ENRMTailFadeInAnimator.m +161 -161
  272. package/ios/utils/ENRMTextHitTest.h +23 -23
  273. package/ios/utils/ENRMTextHitTest.m +20 -20
  274. package/ios/utils/ENRMTextInteractionUtils.h +20 -20
  275. package/ios/utils/ENRMTextInteractionUtils.m +15 -15
  276. package/ios/utils/ENRMTextRenderer.h +38 -38
  277. package/ios/utils/ENRMTextRenderer.m +54 -54
  278. package/ios/utils/ENRMTextViewSetup.h +132 -132
  279. package/ios/utils/ENRMUIKit.h +429 -429
  280. package/ios/utils/EditMenuUtils.h +105 -105
  281. package/ios/utils/EditMenuUtils.m +143 -143
  282. package/ios/utils/FontScaleObserver.h +10 -10
  283. package/ios/utils/FontScaleObserver.m +55 -55
  284. package/ios/utils/FontUtils.h +31 -31
  285. package/ios/utils/FontUtils.m +63 -63
  286. package/ios/utils/HTMLGenerator.h +25 -25
  287. package/ios/utils/HTMLGenerator.m +1041 -1041
  288. package/ios/utils/HeightUpdateUtils.h +25 -25
  289. package/ios/utils/ImageRequestHeaderUtils.h +35 -35
  290. package/ios/utils/LastElementUtils.h +97 -97
  291. package/ios/utils/LinkContextMenuUtils.h +55 -55
  292. package/ios/utils/LinkPillContentUtils.h +46 -46
  293. package/ios/utils/ListMarkerDrawer.h +15 -15
  294. package/ios/utils/ListMarkerDrawer.m +239 -239
  295. package/ios/utils/MentionBackground.h +26 -26
  296. package/ios/utils/MentionBackground.m +86 -86
  297. package/ios/utils/ParagraphStyleUtils.h +87 -87
  298. package/ios/utils/ParagraphStyleUtils.m +484 -484
  299. package/ios/utils/PasteboardUtils.h +48 -48
  300. package/ios/utils/PasteboardUtils.m +166 -166
  301. package/ios/utils/RTFExportUtils.h +24 -24
  302. package/ios/utils/RTFExportUtils.m +299 -299
  303. package/ios/utils/SelectionColorUtils.h +14 -14
  304. package/ios/utils/StylePropsUtils.h +1340 -1340
  305. package/ios/utils/TaskListTapUtils.h +6 -6
  306. package/ios/utils/TaskListTapUtils.m +14 -14
  307. package/ios/utils/TextViewLayoutManager.h +14 -14
  308. package/ios/utils/TiptapAccessibilityElementBuilder.h +43 -43
  309. package/ios/utils/TiptapAccessibilityElementBuilder.m +432 -432
  310. package/ios/utils/TiptapExtractor.h +17 -17
  311. package/ios/utils/TiptapExtractor.m +367 -367
  312. package/ios/views/ENRMTiptapTextView.h +25 -25
  313. package/ios/views/ENRMTiptapTextView.m +247 -247
  314. package/package.json +65 -70
  315. package/react-native.config.js +10 -10
  316. package/src/admonitionDefaults.ts +58 -58
  317. package/src/darkModeTextStyle.ts +99 -99
  318. package/src/headingDefaults.ts +21 -21
  319. package/src/index.tsx +4 -4
  320. package/src/linkVariantUtils.ts +120 -120
  321. package/src/normalizeTiptapStyle.ts +397 -397
  322. package/src/rn-globals.d.ts +1 -1
  323. package/src/styleUtils.ts +107 -107
  324. package/src/types/TiptapStyle.ts +532 -532
  325. package/src/types/TiptapStyleInternal.ts +299 -299
  326. package/src/types/linkPill.ts +9 -9
  327. package/src/utils/isDev.ts +4 -4
@@ -1,1041 +1,1041 @@
1
- #import "HTMLGenerator.h"
2
- #import "BlockquoteBorder.h"
3
- #import "CodeBackground.h"
4
- #import "ENRMFeatureFlags.h"
5
- #import "ENRMImageAttachment.h"
6
- #import "ENRMLinkPillText.h"
7
- #import "HighlightRenderer.h"
8
- #if ENRICHED_TIPTAP_MATH
9
- #import "ENRMMathInlineAttachment.h"
10
- #endif
11
- #import "LastElementUtils.h"
12
- #import "ListItemRenderer.h"
13
- #import "ParagraphStyleUtils.h"
14
- #import "RenderContext.h"
15
- #import "RuntimeKeys.h"
16
- #import "StyleConfig.h"
17
- #import "ThematicBreakAttachment.h"
18
-
19
- static NSString *const kObjectReplacementChar = @"\uFFFC";
20
- static const CGFloat kBlockquoteVerticalPadding = 8.0;
21
- static const CGFloat kBlockquoteParagraphSpacing = 4.0;
22
- static const CGFloat kNestedBlockquoteTopMargin = 8.0;
23
- static const CGFloat kDefaultListIndent = 24.0;
24
- static const CGFloat kCodePadding = 2.0;
25
- static const CGFloat kCodeBorderRadius = 4.0;
26
-
27
- #pragma mark - Paragraph Types
28
-
29
- typedef NS_ENUM(NSInteger, ParagraphType) {
30
- ParagraphTypeNormal,
31
- ParagraphTypeHeading1,
32
- ParagraphTypeHeading2,
33
- ParagraphTypeHeading3,
34
- ParagraphTypeHeading4,
35
- ParagraphTypeHeading5,
36
- ParagraphTypeHeading6,
37
- ParagraphTypeCodeBlock,
38
- ParagraphTypeBlockquote,
39
- ParagraphTypeListItemUnordered,
40
- ParagraphTypeListItemOrdered,
41
- ParagraphTypeTaskList
42
- };
43
-
44
- typedef struct {
45
- NSRange range;
46
- ParagraphType type;
47
- NSInteger depth;
48
- NSInteger listNumber;
49
- BOOL isTaskChecked;
50
- } ParagraphData;
51
-
52
- #pragma mark - Cached Style Config
53
-
54
- /// Pre-fetched style values to avoid repeated StyleConfig method calls
55
- @interface CachedStyles : NSObject
56
- @property (nonatomic, copy) NSString *paragraphColor;
57
- @property (nonatomic, copy) NSString *strongColor;
58
- @property (nonatomic, copy) NSString *emphasisColor;
59
- @property (nonatomic, copy) NSString *codeColor;
60
- @property (nonatomic, copy) NSString *codeBackgroundColor;
61
- @property (nonatomic, copy) NSString *codeBlockColor;
62
- @property (nonatomic, copy) NSString *codeBlockBackgroundColor;
63
- @property (nonatomic, copy) NSString *blockquoteColor;
64
- @property (nonatomic, copy) NSString *blockquoteBackgroundColor;
65
- @property (nonatomic, copy) NSString *blockquoteBorderColor;
66
- @property (nonatomic, copy) NSString *listStyleColor;
67
- @property (nonatomic, copy) NSString *taskCheckedColor;
68
- @property (nonatomic, copy) NSString *taskBorderColor;
69
- @property (nonatomic, copy) NSString *taskCheckmarkColor;
70
- @property (nonatomic) CGFloat taskCheckboxSize;
71
- @property (nonatomic) CGFloat taskCheckboxBorderRadius;
72
- @property (nonatomic, copy) NSString *h1Color;
73
- @property (nonatomic, copy) NSString *h2Color;
74
- @property (nonatomic, copy) NSString *h3Color;
75
- @property (nonatomic, copy) NSString *h4Color;
76
- @property (nonatomic, copy) NSString *h5Color;
77
- @property (nonatomic, copy) NSString *h6Color;
78
- @property (nonatomic) CGFloat paragraphFontSize;
79
- @property (nonatomic) CGFloat paragraphMarginBottom;
80
- @property (nonatomic) CGFloat codeBlockFontSize;
81
- @property (nonatomic) CGFloat codeBlockPadding;
82
- @property (nonatomic) CGFloat codeBlockBorderRadius;
83
- @property (nonatomic) CGFloat codeBlockMarginBottom;
84
- @property (nonatomic) CGFloat blockquoteFontSize;
85
- @property (nonatomic) CGFloat blockquoteBorderWidth;
86
- @property (nonatomic) CGFloat blockquoteMarginBottom;
87
- @property (nonatomic) CGFloat blockquoteGapWidth;
88
- @property (nonatomic) CGFloat listStyleFontSize;
89
- @property (nonatomic) CGFloat listStyleMarginBottom;
90
- @property (nonatomic) CGFloat listStyleMarginLeft;
91
- @property (nonatomic) CGFloat imageMarginBottom;
92
- @property (nonatomic) CGFloat imageBorderRadius;
93
- @property (nonatomic) CGFloat h1FontSize;
94
- @property (nonatomic) CGFloat h2FontSize;
95
- @property (nonatomic) CGFloat h3FontSize;
96
- @property (nonatomic) CGFloat h4FontSize;
97
- @property (nonatomic) CGFloat h5FontSize;
98
- @property (nonatomic) CGFloat h6FontSize;
99
- @property (nonatomic) CGFloat h1MarginBottom;
100
- @property (nonatomic) CGFloat h2MarginBottom;
101
- @property (nonatomic) CGFloat h3MarginBottom;
102
- @property (nonatomic) CGFloat h4MarginBottom;
103
- @property (nonatomic) CGFloat h5MarginBottom;
104
- @property (nonatomic) CGFloat h6MarginBottom;
105
- @property (nonatomic, copy) NSString *h1FontWeight;
106
- @property (nonatomic, copy) NSString *h2FontWeight;
107
- @property (nonatomic, copy) NSString *h3FontWeight;
108
- @property (nonatomic, copy) NSString *h4FontWeight;
109
- @property (nonatomic, copy) NSString *h5FontWeight;
110
- @property (nonatomic, copy) NSString *h6FontWeight;
111
- @property (nonatomic, copy) NSString *linkFontFamily;
112
- @property (nonatomic, copy) NSString *linkColor;
113
- @property (nonatomic) BOOL linkUnderline;
114
- @property (nonatomic, copy) NSString *thematicBreakColor;
115
- @property (nonatomic) CGFloat thematicBreakHeight;
116
- @property (nonatomic) CGFloat thematicBreakMarginTop;
117
- @property (nonatomic) CGFloat thematicBreakMarginBottom;
118
- @property (nonatomic, copy) NSString *strikethroughColor;
119
- @property (nonatomic, copy) NSString *underlineColor;
120
- @property (nonatomic, copy) NSString *highlightColor;
121
- @property (nonatomic, copy) NSString *highlightBackgroundColor;
122
- @end
123
-
124
- @implementation CachedStyles
125
- @end
126
-
127
- #pragma mark - Generator State
128
-
129
- @interface GeneratorState : NSObject
130
- @property (nonatomic) NSInteger currentListDepth;
131
- @property (nonatomic) NSInteger currentBlockquoteDepth;
132
- @property (nonatomic) BOOL inBlockquote;
133
- @property (nonatomic) BOOL inCodeBlock;
134
- @property (nonatomic) BOOL previousWasCodeBlock;
135
- @property (nonatomic) BOOL previousWasBlockquote;
136
- @property (nonatomic, strong) NSMutableArray<NSNumber *> *openListTypes;
137
- @end
138
-
139
- @implementation GeneratorState
140
- - (instancetype)init
141
- {
142
- self = [super init];
143
- if (self) {
144
- _currentListDepth = -1;
145
- _currentBlockquoteDepth = -1;
146
- _openListTypes = [NSMutableArray array];
147
- }
148
- return self;
149
- }
150
- @end
151
-
152
- #pragma mark - Color Conversion
153
-
154
- static NSString *colorToCSS(RCTUIColor *color)
155
- {
156
- if (!color)
157
- return @"inherit";
158
-
159
- CGFloat r = 0, g = 0, b = 0, a = 1;
160
- [color getRed:&r green:&g blue:&b alpha:&a];
161
-
162
- if (a < 1.0) {
163
- return [NSString stringWithFormat:@"rgba(%.0f, %.0f, %.0f, %.2f)", r * 255, g * 255, b * 255, a];
164
- }
165
- return [NSString stringWithFormat:@"#%02X%02X%02X", (int)(r * 255), (int)(g * 255), (int)(b * 255)];
166
- }
167
-
168
- #pragma mark - HTML Escaping
169
-
170
- static void appendEscapedHTML(NSMutableString *output, NSString *text, BOOL preserveLineBreaks)
171
- {
172
- NSUInteger length = text.length;
173
- if (length == 0)
174
- return;
175
-
176
- for (NSUInteger i = 0; i < length; i++) {
177
- unichar c = [text characterAtIndex:i];
178
- switch (c) {
179
- case '&':
180
- [output appendString:@"&amp;"];
181
- break;
182
- case '<':
183
- [output appendString:@"&lt;"];
184
- break;
185
- case '>':
186
- [output appendString:@"&gt;"];
187
- break;
188
- case '"':
189
- [output appendString:@"&quot;"];
190
- break;
191
- case '\'':
192
- [output appendString:@"&#39;"];
193
- break;
194
- case '\r':
195
- case '\n':
196
- case 0x2028:
197
- case 0x2029:
198
- if (preserveLineBreaks) {
199
- if (c == '\r' && i + 1 < length && [text characterAtIndex:i + 1] == '\n')
200
- i++;
201
- [output appendString:@"<br>"];
202
- } else {
203
- [output appendFormat:@"%C", c];
204
- }
205
- break;
206
- default:
207
- [output appendFormat:@"%C", c];
208
- break;
209
- }
210
- }
211
- }
212
-
213
- static NSString *escapeHTML(NSString *text, BOOL preserveLineBreaks)
214
- {
215
- // Fast path: skip if no special chars
216
- static NSCharacterSet *escapeChars = nil;
217
- static NSCharacterSet *inlineEscapeChars = nil;
218
- static dispatch_once_t onceToken;
219
- dispatch_once(&onceToken, ^{
220
- escapeChars = [NSCharacterSet characterSetWithCharactersInString:@"&<>\"'"];
221
- inlineEscapeChars = [NSCharacterSet characterSetWithCharactersInString:@"&<>\"'\n\r\u2028\u2029"];
222
- });
223
-
224
- if ([text rangeOfCharacterFromSet:preserveLineBreaks ? inlineEscapeChars : escapeChars].location == NSNotFound) {
225
- return text;
226
- }
227
-
228
- NSMutableString *escaped = [NSMutableString stringWithCapacity:text.length + 16];
229
- appendEscapedHTML(escaped, text, preserveLineBreaks);
230
- return escaped;
231
- }
232
-
233
- #pragma mark - Font Weight Conversion
234
-
235
- static NSString *fontWeightToCSS(NSString *fontWeight)
236
- {
237
- if (!fontWeight || fontWeight.length == 0)
238
- return @"normal";
239
-
240
- if ([fontWeight integerValue] > 0)
241
- return fontWeight;
242
-
243
- static NSDictionary *weightMap = nil;
244
- static dispatch_once_t onceToken;
245
- dispatch_once(&onceToken, ^{ weightMap = @{@"bold" : @"700", @"semibold" : @"600", @"medium" : @"500"}; });
246
-
247
- NSString *mapped = weightMap[fontWeight.lowercaseString];
248
- return mapped ?: @"normal";
249
- }
250
-
251
- #pragma mark - Style Caching
252
-
253
- static CachedStyles *cacheStyles(StyleConfig *styleConfig)
254
- {
255
- CachedStyles *cache = [[CachedStyles alloc] init];
256
-
257
- cache.paragraphColor = colorToCSS([styleConfig paragraphColor]);
258
- cache.strongColor = colorToCSS([styleConfig strongColor]);
259
- cache.emphasisColor = colorToCSS([styleConfig emphasisColor]);
260
- cache.codeColor = colorToCSS([styleConfig codeColor]);
261
- cache.codeBackgroundColor = colorToCSS([styleConfig codeBackgroundColor]);
262
- cache.codeBlockColor = colorToCSS([styleConfig codeBlockColor]);
263
- cache.codeBlockBackgroundColor = colorToCSS([styleConfig codeBlockBackgroundColor]);
264
- cache.blockquoteColor = colorToCSS([styleConfig blockquoteColor]);
265
- cache.blockquoteBackgroundColor = colorToCSS([styleConfig blockquoteBackgroundColor]);
266
- cache.blockquoteBorderColor = colorToCSS([styleConfig blockquoteBorderColor]);
267
- cache.listStyleColor = colorToCSS([styleConfig listStyleColor]);
268
- cache.taskCheckedColor = colorToCSS([styleConfig taskListCheckedColor]);
269
- cache.taskBorderColor = colorToCSS([styleConfig taskListBorderColor]);
270
- cache.taskCheckmarkColor = colorToCSS([styleConfig taskListCheckmarkColor]);
271
- cache.taskCheckboxSize = [styleConfig taskListCheckboxSize];
272
- cache.taskCheckboxBorderRadius = [styleConfig taskListCheckboxBorderRadius];
273
- cache.h1Color = colorToCSS([styleConfig h1Color]);
274
- cache.h2Color = colorToCSS([styleConfig h2Color]);
275
- cache.h3Color = colorToCSS([styleConfig h3Color]);
276
- cache.h4Color = colorToCSS([styleConfig h4Color]);
277
- cache.h5Color = colorToCSS([styleConfig h5Color]);
278
- cache.h6Color = colorToCSS([styleConfig h6Color]);
279
-
280
- cache.paragraphFontSize = [styleConfig paragraphFontSize];
281
- cache.paragraphMarginBottom = [styleConfig paragraphMarginBottom];
282
- cache.codeBlockFontSize = [styleConfig codeBlockFontSize];
283
- cache.codeBlockPadding = [styleConfig codeBlockPadding];
284
- cache.codeBlockBorderRadius = [styleConfig codeBlockBorderRadius];
285
- cache.codeBlockMarginBottom = [styleConfig codeBlockMarginBottom];
286
- cache.blockquoteFontSize = [styleConfig blockquoteFontSize];
287
- cache.blockquoteBorderWidth = [styleConfig blockquoteBorderWidth];
288
- cache.blockquoteMarginBottom = [styleConfig blockquoteMarginBottom];
289
- cache.blockquoteGapWidth = [styleConfig blockquoteGapWidth];
290
- cache.listStyleFontSize = [styleConfig listStyleFontSize];
291
- cache.listStyleMarginBottom = [styleConfig listStyleMarginBottom];
292
- cache.listStyleMarginLeft = [styleConfig listStyleMarginLeft];
293
- cache.imageMarginBottom = [styleConfig imageMarginBottom];
294
- cache.imageBorderRadius = [styleConfig imageBorderRadius];
295
- cache.h1FontSize = [styleConfig h1FontSize];
296
- cache.h2FontSize = [styleConfig h2FontSize];
297
- cache.h3FontSize = [styleConfig h3FontSize];
298
- cache.h4FontSize = [styleConfig h4FontSize];
299
- cache.h5FontSize = [styleConfig h5FontSize];
300
- cache.h6FontSize = [styleConfig h6FontSize];
301
- cache.h1MarginBottom = [styleConfig h1MarginBottom];
302
- cache.h2MarginBottom = [styleConfig h2MarginBottom];
303
- cache.h3MarginBottom = [styleConfig h3MarginBottom];
304
- cache.h4MarginBottom = [styleConfig h4MarginBottom];
305
- cache.h5MarginBottom = [styleConfig h5MarginBottom];
306
- cache.h6MarginBottom = [styleConfig h6MarginBottom];
307
- cache.h1FontWeight = fontWeightToCSS([styleConfig h1FontWeight]);
308
- cache.h2FontWeight = fontWeightToCSS([styleConfig h2FontWeight]);
309
- cache.h3FontWeight = fontWeightToCSS([styleConfig h3FontWeight]);
310
- cache.h4FontWeight = fontWeightToCSS([styleConfig h4FontWeight]);
311
- cache.h5FontWeight = fontWeightToCSS([styleConfig h5FontWeight]);
312
- cache.h6FontWeight = fontWeightToCSS([styleConfig h6FontWeight]);
313
- cache.linkFontFamily = [styleConfig linkFontFamily];
314
- cache.linkColor = colorToCSS([styleConfig linkColor]);
315
- cache.linkUnderline = [styleConfig linkUnderline];
316
- cache.thematicBreakColor = colorToCSS([styleConfig thematicBreakColor]);
317
- cache.thematicBreakHeight = [styleConfig thematicBreakHeight];
318
- cache.thematicBreakMarginTop = [styleConfig thematicBreakMarginTop];
319
- cache.thematicBreakMarginBottom = [styleConfig thematicBreakMarginBottom];
320
- cache.strikethroughColor = colorToCSS([styleConfig strikethroughColor]);
321
- cache.underlineColor = colorToCSS([styleConfig underlineColor]);
322
- cache.highlightColor = colorToCSS([styleConfig highlightColor]);
323
- cache.highlightBackgroundColor = colorToCSS([styleConfig highlightBackgroundColor]);
324
-
325
- return cache;
326
- }
327
-
328
- #pragma mark - Font Helpers
329
-
330
- static BOOL isMonospaceFont(UIFont *font)
331
- {
332
- if (!font)
333
- return NO;
334
- NSString *fontName = font.fontName.lowercaseString;
335
- return [fontName containsString:@"menlo"] || [fontName containsString:@"courier"] ||
336
- [fontName containsString:@"monaco"] || [fontName containsString:@"consolas"];
337
- }
338
-
339
- static BOOL isCodeSpan(NSDictionary *attrs, BOOL isCodeBlock)
340
- {
341
- if (isCodeBlock)
342
- return NO;
343
-
344
- NSNumber *codeAttr = attrs[CodeAttributeName];
345
- if ([codeAttr boolValue])
346
- return YES;
347
-
348
- return isMonospaceFont(attrs[NSFontAttributeName]);
349
- }
350
-
351
- #pragma mark - Paragraph Type Detection
352
-
353
- static ParagraphType getParagraphType(NSDictionary *attrs)
354
- {
355
- NSNumber *isCodeBlock = attrs[CodeBlockAttributeName];
356
- if ([isCodeBlock boolValue])
357
- return ParagraphTypeCodeBlock;
358
-
359
- NSString *tiptapType = attrs[TiptapTypeAttributeName];
360
- if (tiptapType) {
361
- if ([tiptapType hasPrefix:@"heading-"]) {
362
- NSInteger level = [[tiptapType substringFromIndex:8] integerValue];
363
- if (level >= 1 && level <= 6) {
364
- return ParagraphTypeHeading1 + (level - 1);
365
- }
366
- }
367
- if ([tiptapType isEqualToString:@"code-block"])
368
- return ParagraphTypeCodeBlock;
369
- }
370
-
371
- NSNumber *blockquoteDepth = attrs[BlockquoteDepthAttributeName];
372
- if (blockquoteDepth && [blockquoteDepth integerValue] >= 0)
373
- return ParagraphTypeBlockquote;
374
-
375
- NSNumber *listDepth = attrs[ListDepthAttribute];
376
- if (listDepth && [listDepth integerValue] >= 0) {
377
- if ([attrs[TaskItemAttribute] boolValue])
378
- return ParagraphTypeTaskList;
379
- NSNumber *listType = attrs[ListTypeAttribute];
380
- if (listType && [listType integerValue] == ListTypeOrdered)
381
- return ParagraphTypeListItemOrdered;
382
- return ParagraphTypeListItemUnordered;
383
- }
384
-
385
- return ParagraphTypeNormal;
386
- }
387
-
388
- #pragma mark - Paragraph Collection
389
-
390
- static NSData *collectParagraphsData(NSAttributedString *attributedString, NSUInteger *outCount)
391
- {
392
- NSString *string = attributedString.string;
393
- NSMutableData *data = [NSMutableData dataWithCapacity:string.length / 20 * sizeof(ParagraphData)];
394
- NSUInteger currentIndex = 0;
395
- NSUInteger count = 0;
396
-
397
- while (currentIndex < string.length) {
398
- // Hard breaks render as U+2028 and must stay inside their block, so split on paragraphs, not lines.
399
- NSRange lineRange = [string paragraphRangeForRange:NSMakeRange(currentIndex, 0)];
400
-
401
- ParagraphData para = {
402
- .range = lineRange, .type = ParagraphTypeNormal, .depth = 0, .listNumber = 1, .isTaskChecked = NO};
403
-
404
- if (lineRange.location < attributedString.length) {
405
- NSDictionary *attrs = [attributedString attributesAtIndex:lineRange.location effectiveRange:NULL];
406
- para.type = getParagraphType(attrs);
407
-
408
- NSNumber *listDepth = attrs[ListDepthAttribute];
409
- NSNumber *blockquoteDepth = attrs[BlockquoteDepthAttributeName];
410
- NSNumber *listNumber = attrs[ListItemNumberAttribute];
411
-
412
- para.depth = listDepth ? [listDepth integerValue] : (blockquoteDepth ? [blockquoteDepth integerValue] : 0);
413
- para.listNumber = listNumber ? [listNumber integerValue] : 1;
414
-
415
- if (para.type == ParagraphTypeTaskList) {
416
- para.isTaskChecked = [attrs[TaskCheckedAttribute] boolValue];
417
- }
418
- }
419
-
420
- [data appendBytes:&para length:sizeof(ParagraphData)];
421
- count++;
422
- currentIndex = NSMaxRange(lineRange);
423
- }
424
-
425
- *outCount = count;
426
- return data;
427
- }
428
-
429
- #pragma mark - Heading Helpers
430
-
431
- static NSInteger headingLevel(ParagraphType type)
432
- {
433
- if (type >= ParagraphTypeHeading1 && type <= ParagraphTypeHeading6) {
434
- return type - ParagraphTypeHeading1 + 1;
435
- }
436
- return 0;
437
- }
438
-
439
- #pragma mark - Container Closing Helpers
440
-
441
- static void closeBlockquotes(NSMutableString *html, GeneratorState *state)
442
- {
443
- while (state.currentBlockquoteDepth >= 0) {
444
- [html appendString:@"</blockquote>"];
445
- state.currentBlockquoteDepth--;
446
- }
447
- state.inBlockquote = NO;
448
- }
449
-
450
- static void closeLists(NSMutableString *html, GeneratorState *state)
451
- {
452
- while (state.openListTypes.count > 0) {
453
- NSString *closeTag = ([state.openListTypes.lastObject integerValue] == 1) ? @"</ol>" : @"</ul>";
454
- [html appendString:closeTag];
455
- [state.openListTypes removeLastObject];
456
- }
457
- state.currentListDepth = -1;
458
- }
459
-
460
- static void closeCodeBlock(NSMutableString *html, GeneratorState *state)
461
- {
462
- if (state.inCodeBlock) {
463
- [html appendString:@"</code></pre>"];
464
- state.inCodeBlock = NO;
465
- }
466
- }
467
-
468
- #pragma mark - Inline Content Generation
469
-
470
- static void generateInlineHTML(NSMutableString *html, NSAttributedString *attributedString, NSRange range,
471
- CachedStyles *styles, BOOL isCodeBlock)
472
- {
473
- NSString *string = attributedString.string;
474
-
475
- [attributedString
476
- enumerateAttributesInRange:range
477
- options:0
478
- usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange attrRange, BOOL *stop) {
479
- NSString *text = [string substringWithRange:attrRange];
480
-
481
- if ([text isEqualToString:@"\n"])
482
- return;
483
-
484
- if ([text containsString:kObjectReplacementChar]) {
485
- id attachment = attrs[NSAttachmentAttributeName];
486
- if ([attachment isKindOfClass:[ENRMImageAttachment class]]) {
487
- ENRMImageAttachment *img = (ENRMImageAttachment *)attachment;
488
- if (img.imageURL) {
489
- if (img.isInline) {
490
- [html appendFormat:
491
- @"<img src=\"%@\" style=\"height: 1.2em; width: auto; "
492
- @"vertical-align: -0.2em;\">",
493
- img.imageURL];
494
- } else {
495
- [html appendFormat:
496
- @"</p><div style=\"margin-bottom: %.0fpx;\"><img src=\"%@\" "
497
- @"style=\"max-width: 100%%; border-radius: %.0fpx;\"></div><p>",
498
- styles.imageMarginBottom, img.imageURL, styles.imageBorderRadius];
499
- }
500
- }
501
- } else if ([attachment isKindOfClass:[ThematicBreakAttachment class]]) {
502
- [html appendFormat:
503
- @"</p><hr style=\"border: none; border-top: %.0fpx solid %@; "
504
- @"margin: %.0fpx 0 %.0fpx 0;\"><p>",
505
- styles.thematicBreakHeight, styles.thematicBreakColor,
506
- styles.thematicBreakMarginTop, styles.thematicBreakMarginBottom];
507
- #if ENRICHED_TIPTAP_MATH
508
- } else if ([attachment isKindOfClass:[ENRMMathInlineAttachment class]]) {
509
- ENRMMathInlineAttachment *math = (ENRMMathInlineAttachment *)attachment;
510
- if (math.latex.length > 0) {
511
- [html appendFormat:
512
- @"<code style=\"background-color: %@; color: %@; "
513
- @"padding: %.0fpx %.0fpx; border-radius: %.0fpx; "
514
- @"font-size: 1em; font-family: Menlo, Monaco, Consolas, monospace;\">%@</code>",
515
- styles.codeBackgroundColor, styles.codeColor, kCodePadding, kCodePadding * 2,
516
- kCodeBorderRadius, escapeHTML(math.latex, NO)];
517
- }
518
- #endif
519
- }
520
- return;
521
- }
522
-
523
- UIFont *font = attrs[NSFontAttributeName];
524
- NSNumber *underline = attrs[NSUnderlineStyleAttributeName];
525
- NSNumber *strikethrough = attrs[NSStrikethroughStyleAttributeName];
526
- id linkAttr = attrs[NSLinkAttributeName];
527
- BOOL isCode = isCodeSpan(attrs, isCodeBlock);
528
-
529
- BOOL isBold = NO, isItalic = NO;
530
- if (font) {
531
- UIFontDescriptorSymbolicTraits traits = font.fontDescriptor.symbolicTraits;
532
- isBold = (traits & UIFontDescriptorTraitBold) != 0;
533
- isItalic = (traits & UIFontDescriptorTraitItalic) != 0;
534
- }
535
-
536
- BOOL isHighlight = [attrs[HighlightAttributeName] boolValue];
537
-
538
- if (isHighlight) {
539
- NSMutableString *markStyle = [NSMutableString
540
- stringWithFormat:@"background-color: %@", styles.highlightBackgroundColor];
541
- if (styles.highlightColor && ![styles.highlightColor isEqualToString:styles.paragraphColor]) {
542
- [markStyle appendFormat:@"; color: %@", styles.highlightColor];
543
- } else {
544
- [markStyle appendString:@"; color: inherit"];
545
- }
546
- [html appendFormat:@"<mark style=\"%@\">", markStyle];
547
- }
548
-
549
- if (linkAttr) {
550
- NSString *href =
551
- [linkAttr isKindOfClass:[NSURL class]] ? [(NSURL *)linkAttr absoluteString] : linkAttr;
552
- if (styles.linkFontFamily.length > 0) {
553
- [html appendFormat:
554
- @"<a href=\"%@\" style=\"color: %@; text-decoration: %@; font-family: '%@';\">",
555
- escapeHTML(href, NO), styles.linkColor,
556
- styles.linkUnderline ? @"underline" : @"none", styles.linkFontFamily];
557
- } else {
558
- [html appendFormat:@"<a href=\"%@\" style=\"color: %@; text-decoration: %@;\">",
559
- escapeHTML(href, NO), styles.linkColor,
560
- styles.linkUnderline ? @"underline" : @"none"];
561
- }
562
- }
563
-
564
- if (isCode) {
565
- [html appendFormat:
566
- @"<code style=\"background-color: %@; color: %@; "
567
- @"padding: %.0fpx %.0fpx; border-radius: %.0fpx; "
568
- @"font-size: 1em; font-family: Menlo, Monaco, Consolas, monospace;\">",
569
- styles.codeBackgroundColor, styles.codeColor, kCodePadding, kCodePadding * 2,
570
- kCodeBorderRadius];
571
- }
572
-
573
- if (isBold) {
574
- if (styles.strongColor && ![styles.strongColor isEqualToString:@"inherit"]) {
575
- [html appendFormat:@"<strong style=\"color: %@;\">", styles.strongColor];
576
- } else {
577
- [html appendString:@"<strong>"];
578
- }
579
- }
580
-
581
- if (isItalic) {
582
- if (styles.emphasisColor && ![styles.emphasisColor isEqualToString:@"inherit"]) {
583
- [html appendFormat:@"<em style=\"color: %@;\">", styles.emphasisColor];
584
- } else {
585
- [html appendString:@"<em>"];
586
- }
587
- }
588
-
589
- if ([strikethrough integerValue] > 0) {
590
- if (styles.strikethroughColor && ![styles.strikethroughColor isEqualToString:@"inherit"]) {
591
- [html appendFormat:@"<s style=\"text-decoration-color: %@;\">", styles.strikethroughColor];
592
- } else {
593
- [html appendString:@"<s>"];
594
- }
595
- }
596
- if ([underline integerValue] > 0 && !linkAttr) {
597
- if (styles.underlineColor && ![styles.underlineColor isEqualToString:@"inherit"]) {
598
- [html appendFormat:@"<u style=\"text-decoration-color: %@;\">", styles.underlineColor];
599
- } else {
600
- [html appendString:@"<u>"];
601
- }
602
- }
603
-
604
- [html appendString:escapeHTML(text, !isCodeBlock)];
605
-
606
- // Reverse order
607
- if ([underline integerValue] > 0 && !linkAttr)
608
- [html appendString:@"</u>"];
609
- if ([strikethrough integerValue] > 0)
610
- [html appendString:@"</s>"];
611
- if (isItalic)
612
- [html appendString:@"</em>"];
613
- if (isBold)
614
- [html appendString:@"</strong>"];
615
- if (isCode)
616
- [html appendString:@"</code>"];
617
- if (linkAttr)
618
- [html appendString:@"</a>"];
619
- if (isHighlight)
620
- [html appendString:@"</mark>"];
621
- }];
622
- }
623
-
624
- #pragma mark - Block Handlers
625
-
626
- static void handleCodeBlock(NSMutableString *html, NSMutableString *inlineContent, CachedStyles *styles,
627
- GeneratorState *state)
628
- {
629
- if (!state.inCodeBlock) {
630
- state.inCodeBlock = YES;
631
- [html appendFormat:
632
- @"<pre dir=\"ltr\" style=\"background-color: %@; padding: %.0fpx; border-radius: %.0fpx; "
633
- @"margin: 0 0 %.0fpx 0; overflow-x: auto; text-align: left;\"><code style=\"font-family: Menlo, Monaco, "
634
- @"Consolas, monospace; font-size: %.0fpx; color: %@;\">",
635
- styles.codeBlockBackgroundColor, styles.codeBlockPadding, styles.codeBlockBorderRadius,
636
- styles.codeBlockMarginBottom, styles.codeBlockFontSize, styles.codeBlockColor];
637
- } else if (state.previousWasCodeBlock) {
638
- [html appendString:@"\n"];
639
- }
640
-
641
- [html appendString:inlineContent];
642
- state.previousWasCodeBlock = YES;
643
- }
644
-
645
- static void handleBlockquote(NSMutableString *html, ParagraphData *para, NSMutableString *inlineContent,
646
- CachedStyles *styles, GeneratorState *state)
647
- {
648
- NSInteger depth = para->depth;
649
-
650
- // Reset if starting a new blockquote block
651
- if (!state.previousWasBlockquote && state.inBlockquote) {
652
- closeBlockquotes(html, state);
653
- }
654
-
655
- while (state.currentBlockquoteDepth > depth) {
656
- [html appendString:@"</blockquote>"];
657
- state.currentBlockquoteDepth--;
658
- }
659
-
660
- while (state.currentBlockquoteDepth < depth) {
661
- state.currentBlockquoteDepth++;
662
- state.inBlockquote = YES;
663
-
664
- if (state.currentBlockquoteDepth == 0) {
665
- [html appendFormat:
666
- @"<blockquote style=\"background-color: %@; border-inline-start: %.0fpx solid %@; "
667
- @"padding: %.0fpx %.0fpx; margin: 0 0 %.0fpx 0; border-start-end-radius: 8px; border-end-end-radius: "
668
- @"8px;\">",
669
- styles.blockquoteBackgroundColor, styles.blockquoteBorderWidth, styles.blockquoteBorderColor,
670
- kBlockquoteVerticalPadding, styles.blockquoteGapWidth, styles.blockquoteMarginBottom];
671
- } else {
672
- [html appendFormat:
673
- @"<blockquote style=\"border-inline-start: %.0fpx solid %@; padding-inline-start: %.0fpx; "
674
- @"margin: %.0fpx 0 0 0;\">",
675
- styles.blockquoteBorderWidth, styles.blockquoteBorderColor, styles.blockquoteGapWidth,
676
- kNestedBlockquoteTopMargin];
677
- }
678
- }
679
-
680
- [html appendFormat:@"<p style=\"margin: 0 0 %.0fpx 0; color: %@; font-size: %.0fpx;\">%@</p>",
681
- kBlockquoteParagraphSpacing, styles.blockquoteColor, styles.blockquoteFontSize, inlineContent];
682
-
683
- state.previousWasBlockquote = YES;
684
- }
685
-
686
- static void handleListItem(NSMutableString *html, ParagraphData *para, NSMutableString *inlineContent,
687
- CachedStyles *styles, GeneratorState *state)
688
- {
689
- NSInteger depth = para->depth;
690
- BOOL isOrdered = (para->type == ParagraphTypeListItemOrdered);
691
- BOOL isTask = (para->type == ParagraphTypeTaskList);
692
- NSInteger listTypeValue = isOrdered ? 1 : 0;
693
-
694
- while (state.currentListDepth > depth) {
695
- NSString *closeTag = ([state.openListTypes.lastObject integerValue] == 1) ? @"</ol>" : @"</ul>";
696
- [html appendString:closeTag];
697
- [state.openListTypes removeLastObject];
698
- state.currentListDepth--;
699
- }
700
-
701
- // List type change at same depth (ul <-> ol)
702
- if (state.currentListDepth == depth && state.openListTypes.count > 0) {
703
- NSInteger currentType = [state.openListTypes.lastObject integerValue];
704
- if (currentType != listTypeValue) {
705
- NSString *closeTag = (currentType == 1) ? @"</ol>" : @"</ul>";
706
- [html appendString:closeTag];
707
- [state.openListTypes removeLastObject];
708
- state.currentListDepth--;
709
- }
710
- }
711
-
712
- CGFloat indent = styles.listStyleMarginLeft > 0 ? styles.listStyleMarginLeft : kDefaultListIndent;
713
-
714
- while (state.currentListDepth < depth) {
715
- state.currentListDepth++;
716
- if (isOrdered) {
717
- [html appendFormat:@"<ol style=\"margin: 0; padding-inline-start: %.0fpx;\">", indent];
718
- } else {
719
- NSString *listStyleType = isTask ? @"none" : @"disc";
720
- [html appendFormat:@"<ul style=\"margin: 0; padding-inline-start: %.0fpx; list-style-type: %@;\">", indent,
721
- listStyleType];
722
- }
723
- [state.openListTypes addObject:@(listTypeValue)];
724
- }
725
-
726
- [html appendFormat:@"<li style=\"margin-bottom: %.0fpx; color: %@; font-size: %.0fpx;\">",
727
- styles.listStyleMarginBottom, styles.listStyleColor, styles.listStyleFontSize];
728
-
729
- if (isTask) {
730
- CGFloat size = styles.taskCheckboxSize;
731
- CGFloat radius = styles.taskCheckboxBorderRadius;
732
- if (para->isTaskChecked) {
733
- [html appendFormat:
734
- @"<span style=\"display: inline-block; width: %.0fpx; height: %.0fpx; "
735
- @"border-radius: %.0fpx; background-color: %@; color: %@; "
736
- @"font-size: %.0fpx; line-height: %.0fpx; text-align: center; "
737
- @"vertical-align: middle; margin-inline-end: 4px;\">&#10003;</span> ",
738
- size, size, radius, styles.taskCheckedColor, styles.taskCheckmarkColor, size - 2, size];
739
- } else {
740
- [html appendFormat:
741
- @"<span style=\"display: inline-block; width: %.0fpx; height: %.0fpx; "
742
- @"border-radius: %.0fpx; border: 1.5px solid %@; "
743
- @"vertical-align: middle; margin-inline-end: 4px;\"></span> ",
744
- size, size, radius, styles.taskBorderColor];
745
- }
746
- }
747
-
748
- [html appendFormat:@"%@</li>", inlineContent];
749
- }
750
-
751
- static void handleHeading(NSMutableString *html, ParagraphData *para, NSMutableString *inlineContent,
752
- CachedStyles *styles)
753
- {
754
- NSInteger level = headingLevel(para->type);
755
- CGFloat fontSize, marginBottom;
756
- NSString *fontWeight, *color;
757
-
758
- switch (level) {
759
- case 1:
760
- fontSize = styles.h1FontSize;
761
- fontWeight = styles.h1FontWeight;
762
- color = styles.h1Color;
763
- marginBottom = styles.h1MarginBottom;
764
- break;
765
- case 2:
766
- fontSize = styles.h2FontSize;
767
- fontWeight = styles.h2FontWeight;
768
- color = styles.h2Color;
769
- marginBottom = styles.h2MarginBottom;
770
- break;
771
- case 3:
772
- fontSize = styles.h3FontSize;
773
- fontWeight = styles.h3FontWeight;
774
- color = styles.h3Color;
775
- marginBottom = styles.h3MarginBottom;
776
- break;
777
- case 4:
778
- fontSize = styles.h4FontSize;
779
- fontWeight = styles.h4FontWeight;
780
- color = styles.h4Color;
781
- marginBottom = styles.h4MarginBottom;
782
- break;
783
- case 5:
784
- fontSize = styles.h5FontSize;
785
- fontWeight = styles.h5FontWeight;
786
- color = styles.h5Color;
787
- marginBottom = styles.h5MarginBottom;
788
- break;
789
- case 6:
790
- fontSize = styles.h6FontSize;
791
- fontWeight = styles.h6FontWeight;
792
- color = styles.h6Color;
793
- marginBottom = styles.h6MarginBottom;
794
- break;
795
- default:
796
- fontSize = 16;
797
- fontWeight = @"normal";
798
- color = @"inherit";
799
- marginBottom = 0;
800
- break;
801
- }
802
-
803
- [html appendFormat:@"<h%ld style=\"font-size: %.0fpx; font-weight: %@; color: %@; margin: 0 0 %.0fpx 0;\">%@</h%ld>",
804
- (long)level, fontSize, fontWeight, color, marginBottom, inlineContent, (long)level];
805
- }
806
-
807
- static void handleParagraph(NSMutableString *html, NSMutableString *inlineContent, CachedStyles *styles)
808
- {
809
- [html appendFormat:@"<p style=\"margin: 0 0 %.0fpx 0; color: %@; font-size: %.0fpx;\">%@</p>",
810
- styles.paragraphMarginBottom, styles.paragraphColor, styles.paragraphFontSize, inlineContent];
811
- }
812
-
813
- #pragma mark - Main Generator
814
-
815
- static NSString *dirAttrFromParagraphStyle(NSParagraphStyle *_Nullable style)
816
- {
817
- if (!style || style.baseWritingDirection == NSWritingDirectionNatural) {
818
- return @"auto";
819
- }
820
- return style.baseWritingDirection == NSWritingDirectionRightToLeft ? @"rtl" : @"ltr";
821
- }
822
-
823
- NSString *_Nullable generateHTML(NSAttributedString *attributedString, StyleConfig *styleConfig)
824
- {
825
- if (!attributedString || attributedString.length == 0)
826
- return nil;
827
-
828
- attributedString = ENRMAttributedStringByExpandingLinkPills(attributedString, NULL);
829
-
830
- CachedStyles *styles = cacheStyles(styleConfig);
831
-
832
- NSParagraphStyle *firstParagraphStyle = [attributedString attribute:NSParagraphStyleAttributeName
833
- atIndex:0
834
- effectiveRange:NULL];
835
- NSString *dirAttr = dirAttrFromParagraphStyle(firstParagraphStyle);
836
-
837
- NSMutableString *html = [NSMutableString stringWithCapacity:attributedString.length * 2];
838
- [html appendFormat:
839
- @"<!DOCTYPE html><html dir=\"%@\"><head><meta charset=\"UTF-8\"></head><body "
840
- @"style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\">",
841
- dirAttr];
842
-
843
- NSUInteger paragraphCount = 0;
844
- NSData *paragraphsData = collectParagraphsData(attributedString, &paragraphCount);
845
- ParagraphData *paragraphs = (ParagraphData *)paragraphsData.bytes;
846
-
847
- GeneratorState *state = [[GeneratorState alloc] init];
848
- NSMutableString *inlineBuffer = [NSMutableString stringWithCapacity:256];
849
-
850
- for (NSUInteger i = 0; i < paragraphCount; i++) {
851
- ParagraphData *para = &paragraphs[i];
852
-
853
- NSString *content = [[attributedString.string substringWithRange:para->range]
854
- stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
855
-
856
- if (content.length == 0) {
857
- state.previousWasBlockquote = NO;
858
- continue;
859
- }
860
-
861
- [inlineBuffer setString:@""];
862
- NSRange contentRange = para->range;
863
- if (contentRange.length > 0 &&
864
- [[attributedString.string substringWithRange:NSMakeRange(NSMaxRange(contentRange) - 1, 1)]
865
- isEqualToString:@"\n"]) {
866
- contentRange.length--;
867
- }
868
-
869
- BOOL isCodeBlockPara = (para->type == ParagraphTypeCodeBlock);
870
- generateInlineHTML(inlineBuffer, attributedString, contentRange, styles, isCodeBlockPara);
871
-
872
- if (isCodeBlockPara) {
873
- handleCodeBlock(html, inlineBuffer, styles, state);
874
- continue;
875
- }
876
-
877
- if (state.inCodeBlock) {
878
- closeCodeBlock(html, state);
879
- }
880
- state.previousWasCodeBlock = NO;
881
-
882
- if (para->type == ParagraphTypeBlockquote) {
883
- handleBlockquote(html, para, inlineBuffer, styles, state);
884
- continue;
885
- }
886
-
887
- if (state.inBlockquote) {
888
- closeBlockquotes(html, state);
889
- }
890
- state.previousWasBlockquote = NO;
891
-
892
- if (para->type == ParagraphTypeListItemUnordered || para->type == ParagraphTypeListItemOrdered ||
893
- para->type == ParagraphTypeTaskList) {
894
- handleListItem(html, para, inlineBuffer, styles, state);
895
- continue;
896
- }
897
-
898
- if (state.currentListDepth >= 0) {
899
- closeLists(html, state);
900
- }
901
-
902
- NSInteger hLevel = headingLevel(para->type);
903
- if (hLevel > 0) {
904
- handleHeading(html, para, inlineBuffer, styles);
905
- continue;
906
- }
907
-
908
- handleParagraph(html, inlineBuffer, styles);
909
- }
910
-
911
- closeCodeBlock(html, state);
912
- closeBlockquotes(html, state);
913
- closeLists(html, state);
914
-
915
- [html appendString:@"</body></html>"];
916
-
917
- return html;
918
- }
919
-
920
- #pragma mark - Table HTML Generator
921
-
922
- typedef NSString *HTMLString;
923
-
924
- static NSString *alignmentToCSS(NSTextAlignment alignment)
925
- {
926
- switch (alignment) {
927
- case NSTextAlignmentCenter:
928
- return @"center";
929
- case NSTextAlignmentRight:
930
- return @"right";
931
- case NSTextAlignmentJustified:
932
- return @"justify";
933
- default:
934
- return @"left";
935
- }
936
- }
937
-
938
- static NSString *styleForCell(BOOL isHeader, NSUInteger rowIndex, StyleConfig *styleConfig, NSTextAlignment alignment)
939
- {
940
- NSString *backgroundColor = isHeader ? colorToCSS(styleConfig.tableHeaderBackgroundColor)
941
- : (rowIndex % 2 == 0 ? colorToCSS(styleConfig.tableRowEvenBackgroundColor)
942
- : colorToCSS(styleConfig.tableRowOddBackgroundColor));
943
-
944
- NSString *textColor = isHeader ? colorToCSS(styleConfig.tableHeaderTextColor) : colorToCSS(styleConfig.tableColor);
945
-
946
- return [NSString stringWithFormat:
947
- @"padding: %.0fpx %.0fpx; "
948
- "text-align: %@; "
949
- "background-color: %@; "
950
- "color: %@; "
951
- "border: %.0fpx solid %@; "
952
- "font-weight: %@;",
953
- styleConfig.tableCellPaddingVertical, styleConfig.tableCellPaddingHorizontal,
954
- alignmentToCSS(alignment), backgroundColor, textColor, styleConfig.tableBorderWidth,
955
- colorToCSS(styleConfig.tableBorderColor), isHeader ? @"bold" : @"normal"];
956
- }
957
-
958
- static NSString *tableDirAttrFromRows(NSArray<NSArray<NSDictionary *> *> *rows)
959
- {
960
- for (NSArray<NSDictionary *> *row in rows) {
961
- for (NSDictionary *cell in row) {
962
- NSAttributedString *cellText = cell[@"attributedText"];
963
- if (cellText.length == 0) {
964
- continue;
965
- }
966
- NSParagraphStyle *style = [cellText attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:NULL];
967
- return dirAttrFromParagraphStyle(style);
968
- }
969
- }
970
- return @"auto";
971
- }
972
-
973
- HTMLString _Nullable generateTableHTML(NSArray<NSArray<NSDictionary *> *> *rows, StyleConfig *styleConfig)
974
- {
975
- if (rows.count == 0)
976
- return nil;
977
-
978
- CachedStyles *cachedStyles = cacheStyles(styleConfig);
979
- NSMutableString *htmlOutput = [NSMutableString stringWithCapacity:rows.count * 250];
980
-
981
- NSString *dirAttr = tableDirAttrFromRows(rows);
982
-
983
- [htmlOutput appendFormat:
984
- @"<table dir=\"%@\" style=\"border-collapse: separate; border-spacing: 0; "
985
- "border: %.0fpx solid %@; border-radius: %.0fpx; overflow: hidden; font-size: %.0fpx;\">",
986
- dirAttr, styleConfig.tableBorderWidth, colorToCSS(styleConfig.tableBorderColor),
987
- styleConfig.tableBorderRadius, styleConfig.tableFontSize];
988
-
989
- BOOL hasOpenedBody = NO;
990
- NSUInteger bodyRowIndex = 0;
991
-
992
- for (NSArray<NSDictionary *> *row in rows) {
993
- if (row.count == 0)
994
- continue;
995
-
996
- BOOL isHeaderRow = [row.firstObject[@"isHeader"] boolValue];
997
-
998
- if (isHeaderRow) {
999
- [htmlOutput appendString:@"<thead>"];
1000
- } else if (!hasOpenedBody) {
1001
- [htmlOutput appendString:@"<tbody>"];
1002
- hasOpenedBody = YES;
1003
- }
1004
-
1005
- [htmlOutput appendString:@"<tr>"];
1006
-
1007
- for (NSDictionary *cellInfo in row) {
1008
- BOOL isHeaderCell = [cellInfo[@"isHeader"] boolValue];
1009
- NSTextAlignment alignment = (NSTextAlignment)[cellInfo[@"alignment"] integerValue];
1010
- NSAttributedString *attributedText = cellInfo[@"attributedText"];
1011
- if (attributedText)
1012
- attributedText = ENRMAttributedStringByExpandingLinkPills(attributedText, NULL);
1013
-
1014
- NSString *htmlTag = isHeaderCell ? @"th" : @"td";
1015
- NSString *cellStyle = styleForCell(isHeaderCell, bodyRowIndex, styleConfig, alignment);
1016
-
1017
- [htmlOutput appendFormat:@"<%@ style=\"%@\">", htmlTag, cellStyle];
1018
-
1019
- if (attributedText.length > 0) {
1020
- generateInlineHTML(htmlOutput, attributedText, NSMakeRange(0, attributedText.length), cachedStyles, NO);
1021
- }
1022
-
1023
- [htmlOutput appendFormat:@"</%@>", htmlTag];
1024
- }
1025
-
1026
- [htmlOutput appendString:@"</tr>"];
1027
-
1028
- if (isHeaderRow) {
1029
- [htmlOutput appendString:@"</thead>"];
1030
- } else {
1031
- bodyRowIndex++;
1032
- }
1033
- }
1034
-
1035
- if (hasOpenedBody) {
1036
- [htmlOutput appendString:@"</tbody>"];
1037
- }
1038
-
1039
- [htmlOutput appendString:@"</table>"];
1040
- return [htmlOutput copy];
1041
- }
1
+ #import "HTMLGenerator.h"
2
+ #import "BlockquoteBorder.h"
3
+ #import "CodeBackground.h"
4
+ #import "ENRMFeatureFlags.h"
5
+ #import "ENRMImageAttachment.h"
6
+ #import "ENRMLinkPillText.h"
7
+ #import "HighlightRenderer.h"
8
+ #if ENRICHED_TIPTAP_MATH
9
+ #import "ENRMMathInlineAttachment.h"
10
+ #endif
11
+ #import "LastElementUtils.h"
12
+ #import "ListItemRenderer.h"
13
+ #import "ParagraphStyleUtils.h"
14
+ #import "RenderContext.h"
15
+ #import "RuntimeKeys.h"
16
+ #import "StyleConfig.h"
17
+ #import "ThematicBreakAttachment.h"
18
+
19
+ static NSString *const kObjectReplacementChar = @"\uFFFC";
20
+ static const CGFloat kBlockquoteVerticalPadding = 8.0;
21
+ static const CGFloat kBlockquoteParagraphSpacing = 4.0;
22
+ static const CGFloat kNestedBlockquoteTopMargin = 8.0;
23
+ static const CGFloat kDefaultListIndent = 24.0;
24
+ static const CGFloat kCodePadding = 2.0;
25
+ static const CGFloat kCodeBorderRadius = 4.0;
26
+
27
+ #pragma mark - Paragraph Types
28
+
29
+ typedef NS_ENUM(NSInteger, ParagraphType) {
30
+ ParagraphTypeNormal,
31
+ ParagraphTypeHeading1,
32
+ ParagraphTypeHeading2,
33
+ ParagraphTypeHeading3,
34
+ ParagraphTypeHeading4,
35
+ ParagraphTypeHeading5,
36
+ ParagraphTypeHeading6,
37
+ ParagraphTypeCodeBlock,
38
+ ParagraphTypeBlockquote,
39
+ ParagraphTypeListItemUnordered,
40
+ ParagraphTypeListItemOrdered,
41
+ ParagraphTypeTaskList
42
+ };
43
+
44
+ typedef struct {
45
+ NSRange range;
46
+ ParagraphType type;
47
+ NSInteger depth;
48
+ NSInteger listNumber;
49
+ BOOL isTaskChecked;
50
+ } ParagraphData;
51
+
52
+ #pragma mark - Cached Style Config
53
+
54
+ /// Pre-fetched style values to avoid repeated StyleConfig method calls
55
+ @interface CachedStyles : NSObject
56
+ @property (nonatomic, copy) NSString *paragraphColor;
57
+ @property (nonatomic, copy) NSString *strongColor;
58
+ @property (nonatomic, copy) NSString *emphasisColor;
59
+ @property (nonatomic, copy) NSString *codeColor;
60
+ @property (nonatomic, copy) NSString *codeBackgroundColor;
61
+ @property (nonatomic, copy) NSString *codeBlockColor;
62
+ @property (nonatomic, copy) NSString *codeBlockBackgroundColor;
63
+ @property (nonatomic, copy) NSString *blockquoteColor;
64
+ @property (nonatomic, copy) NSString *blockquoteBackgroundColor;
65
+ @property (nonatomic, copy) NSString *blockquoteBorderColor;
66
+ @property (nonatomic, copy) NSString *listStyleColor;
67
+ @property (nonatomic, copy) NSString *taskCheckedColor;
68
+ @property (nonatomic, copy) NSString *taskBorderColor;
69
+ @property (nonatomic, copy) NSString *taskCheckmarkColor;
70
+ @property (nonatomic) CGFloat taskCheckboxSize;
71
+ @property (nonatomic) CGFloat taskCheckboxBorderRadius;
72
+ @property (nonatomic, copy) NSString *h1Color;
73
+ @property (nonatomic, copy) NSString *h2Color;
74
+ @property (nonatomic, copy) NSString *h3Color;
75
+ @property (nonatomic, copy) NSString *h4Color;
76
+ @property (nonatomic, copy) NSString *h5Color;
77
+ @property (nonatomic, copy) NSString *h6Color;
78
+ @property (nonatomic) CGFloat paragraphFontSize;
79
+ @property (nonatomic) CGFloat paragraphMarginBottom;
80
+ @property (nonatomic) CGFloat codeBlockFontSize;
81
+ @property (nonatomic) CGFloat codeBlockPadding;
82
+ @property (nonatomic) CGFloat codeBlockBorderRadius;
83
+ @property (nonatomic) CGFloat codeBlockMarginBottom;
84
+ @property (nonatomic) CGFloat blockquoteFontSize;
85
+ @property (nonatomic) CGFloat blockquoteBorderWidth;
86
+ @property (nonatomic) CGFloat blockquoteMarginBottom;
87
+ @property (nonatomic) CGFloat blockquoteGapWidth;
88
+ @property (nonatomic) CGFloat listStyleFontSize;
89
+ @property (nonatomic) CGFloat listStyleMarginBottom;
90
+ @property (nonatomic) CGFloat listStyleMarginLeft;
91
+ @property (nonatomic) CGFloat imageMarginBottom;
92
+ @property (nonatomic) CGFloat imageBorderRadius;
93
+ @property (nonatomic) CGFloat h1FontSize;
94
+ @property (nonatomic) CGFloat h2FontSize;
95
+ @property (nonatomic) CGFloat h3FontSize;
96
+ @property (nonatomic) CGFloat h4FontSize;
97
+ @property (nonatomic) CGFloat h5FontSize;
98
+ @property (nonatomic) CGFloat h6FontSize;
99
+ @property (nonatomic) CGFloat h1MarginBottom;
100
+ @property (nonatomic) CGFloat h2MarginBottom;
101
+ @property (nonatomic) CGFloat h3MarginBottom;
102
+ @property (nonatomic) CGFloat h4MarginBottom;
103
+ @property (nonatomic) CGFloat h5MarginBottom;
104
+ @property (nonatomic) CGFloat h6MarginBottom;
105
+ @property (nonatomic, copy) NSString *h1FontWeight;
106
+ @property (nonatomic, copy) NSString *h2FontWeight;
107
+ @property (nonatomic, copy) NSString *h3FontWeight;
108
+ @property (nonatomic, copy) NSString *h4FontWeight;
109
+ @property (nonatomic, copy) NSString *h5FontWeight;
110
+ @property (nonatomic, copy) NSString *h6FontWeight;
111
+ @property (nonatomic, copy) NSString *linkFontFamily;
112
+ @property (nonatomic, copy) NSString *linkColor;
113
+ @property (nonatomic) BOOL linkUnderline;
114
+ @property (nonatomic, copy) NSString *thematicBreakColor;
115
+ @property (nonatomic) CGFloat thematicBreakHeight;
116
+ @property (nonatomic) CGFloat thematicBreakMarginTop;
117
+ @property (nonatomic) CGFloat thematicBreakMarginBottom;
118
+ @property (nonatomic, copy) NSString *strikethroughColor;
119
+ @property (nonatomic, copy) NSString *underlineColor;
120
+ @property (nonatomic, copy) NSString *highlightColor;
121
+ @property (nonatomic, copy) NSString *highlightBackgroundColor;
122
+ @end
123
+
124
+ @implementation CachedStyles
125
+ @end
126
+
127
+ #pragma mark - Generator State
128
+
129
+ @interface GeneratorState : NSObject
130
+ @property (nonatomic) NSInteger currentListDepth;
131
+ @property (nonatomic) NSInteger currentBlockquoteDepth;
132
+ @property (nonatomic) BOOL inBlockquote;
133
+ @property (nonatomic) BOOL inCodeBlock;
134
+ @property (nonatomic) BOOL previousWasCodeBlock;
135
+ @property (nonatomic) BOOL previousWasBlockquote;
136
+ @property (nonatomic, strong) NSMutableArray<NSNumber *> *openListTypes;
137
+ @end
138
+
139
+ @implementation GeneratorState
140
+ - (instancetype)init
141
+ {
142
+ self = [super init];
143
+ if (self) {
144
+ _currentListDepth = -1;
145
+ _currentBlockquoteDepth = -1;
146
+ _openListTypes = [NSMutableArray array];
147
+ }
148
+ return self;
149
+ }
150
+ @end
151
+
152
+ #pragma mark - Color Conversion
153
+
154
+ static NSString *colorToCSS(RCTUIColor *color)
155
+ {
156
+ if (!color)
157
+ return @"inherit";
158
+
159
+ CGFloat r = 0, g = 0, b = 0, a = 1;
160
+ [color getRed:&r green:&g blue:&b alpha:&a];
161
+
162
+ if (a < 1.0) {
163
+ return [NSString stringWithFormat:@"rgba(%.0f, %.0f, %.0f, %.2f)", r * 255, g * 255, b * 255, a];
164
+ }
165
+ return [NSString stringWithFormat:@"#%02X%02X%02X", (int)(r * 255), (int)(g * 255), (int)(b * 255)];
166
+ }
167
+
168
+ #pragma mark - HTML Escaping
169
+
170
+ static void appendEscapedHTML(NSMutableString *output, NSString *text, BOOL preserveLineBreaks)
171
+ {
172
+ NSUInteger length = text.length;
173
+ if (length == 0)
174
+ return;
175
+
176
+ for (NSUInteger i = 0; i < length; i++) {
177
+ unichar c = [text characterAtIndex:i];
178
+ switch (c) {
179
+ case '&':
180
+ [output appendString:@"&amp;"];
181
+ break;
182
+ case '<':
183
+ [output appendString:@"&lt;"];
184
+ break;
185
+ case '>':
186
+ [output appendString:@"&gt;"];
187
+ break;
188
+ case '"':
189
+ [output appendString:@"&quot;"];
190
+ break;
191
+ case '\'':
192
+ [output appendString:@"&#39;"];
193
+ break;
194
+ case '\r':
195
+ case '\n':
196
+ case 0x2028:
197
+ case 0x2029:
198
+ if (preserveLineBreaks) {
199
+ if (c == '\r' && i + 1 < length && [text characterAtIndex:i + 1] == '\n')
200
+ i++;
201
+ [output appendString:@"<br>"];
202
+ } else {
203
+ [output appendFormat:@"%C", c];
204
+ }
205
+ break;
206
+ default:
207
+ [output appendFormat:@"%C", c];
208
+ break;
209
+ }
210
+ }
211
+ }
212
+
213
+ static NSString *escapeHTML(NSString *text, BOOL preserveLineBreaks)
214
+ {
215
+ // Fast path: skip if no special chars
216
+ static NSCharacterSet *escapeChars = nil;
217
+ static NSCharacterSet *inlineEscapeChars = nil;
218
+ static dispatch_once_t onceToken;
219
+ dispatch_once(&onceToken, ^{
220
+ escapeChars = [NSCharacterSet characterSetWithCharactersInString:@"&<>\"'"];
221
+ inlineEscapeChars = [NSCharacterSet characterSetWithCharactersInString:@"&<>\"'\n\r\u2028\u2029"];
222
+ });
223
+
224
+ if ([text rangeOfCharacterFromSet:preserveLineBreaks ? inlineEscapeChars : escapeChars].location == NSNotFound) {
225
+ return text;
226
+ }
227
+
228
+ NSMutableString *escaped = [NSMutableString stringWithCapacity:text.length + 16];
229
+ appendEscapedHTML(escaped, text, preserveLineBreaks);
230
+ return escaped;
231
+ }
232
+
233
+ #pragma mark - Font Weight Conversion
234
+
235
+ static NSString *fontWeightToCSS(NSString *fontWeight)
236
+ {
237
+ if (!fontWeight || fontWeight.length == 0)
238
+ return @"normal";
239
+
240
+ if ([fontWeight integerValue] > 0)
241
+ return fontWeight;
242
+
243
+ static NSDictionary *weightMap = nil;
244
+ static dispatch_once_t onceToken;
245
+ dispatch_once(&onceToken, ^{ weightMap = @{@"bold" : @"700", @"semibold" : @"600", @"medium" : @"500"}; });
246
+
247
+ NSString *mapped = weightMap[fontWeight.lowercaseString];
248
+ return mapped ?: @"normal";
249
+ }
250
+
251
+ #pragma mark - Style Caching
252
+
253
+ static CachedStyles *cacheStyles(StyleConfig *styleConfig)
254
+ {
255
+ CachedStyles *cache = [[CachedStyles alloc] init];
256
+
257
+ cache.paragraphColor = colorToCSS([styleConfig paragraphColor]);
258
+ cache.strongColor = colorToCSS([styleConfig strongColor]);
259
+ cache.emphasisColor = colorToCSS([styleConfig emphasisColor]);
260
+ cache.codeColor = colorToCSS([styleConfig codeColor]);
261
+ cache.codeBackgroundColor = colorToCSS([styleConfig codeBackgroundColor]);
262
+ cache.codeBlockColor = colorToCSS([styleConfig codeBlockColor]);
263
+ cache.codeBlockBackgroundColor = colorToCSS([styleConfig codeBlockBackgroundColor]);
264
+ cache.blockquoteColor = colorToCSS([styleConfig blockquoteColor]);
265
+ cache.blockquoteBackgroundColor = colorToCSS([styleConfig blockquoteBackgroundColor]);
266
+ cache.blockquoteBorderColor = colorToCSS([styleConfig blockquoteBorderColor]);
267
+ cache.listStyleColor = colorToCSS([styleConfig listStyleColor]);
268
+ cache.taskCheckedColor = colorToCSS([styleConfig taskListCheckedColor]);
269
+ cache.taskBorderColor = colorToCSS([styleConfig taskListBorderColor]);
270
+ cache.taskCheckmarkColor = colorToCSS([styleConfig taskListCheckmarkColor]);
271
+ cache.taskCheckboxSize = [styleConfig taskListCheckboxSize];
272
+ cache.taskCheckboxBorderRadius = [styleConfig taskListCheckboxBorderRadius];
273
+ cache.h1Color = colorToCSS([styleConfig h1Color]);
274
+ cache.h2Color = colorToCSS([styleConfig h2Color]);
275
+ cache.h3Color = colorToCSS([styleConfig h3Color]);
276
+ cache.h4Color = colorToCSS([styleConfig h4Color]);
277
+ cache.h5Color = colorToCSS([styleConfig h5Color]);
278
+ cache.h6Color = colorToCSS([styleConfig h6Color]);
279
+
280
+ cache.paragraphFontSize = [styleConfig paragraphFontSize];
281
+ cache.paragraphMarginBottom = [styleConfig paragraphMarginBottom];
282
+ cache.codeBlockFontSize = [styleConfig codeBlockFontSize];
283
+ cache.codeBlockPadding = [styleConfig codeBlockPadding];
284
+ cache.codeBlockBorderRadius = [styleConfig codeBlockBorderRadius];
285
+ cache.codeBlockMarginBottom = [styleConfig codeBlockMarginBottom];
286
+ cache.blockquoteFontSize = [styleConfig blockquoteFontSize];
287
+ cache.blockquoteBorderWidth = [styleConfig blockquoteBorderWidth];
288
+ cache.blockquoteMarginBottom = [styleConfig blockquoteMarginBottom];
289
+ cache.blockquoteGapWidth = [styleConfig blockquoteGapWidth];
290
+ cache.listStyleFontSize = [styleConfig listStyleFontSize];
291
+ cache.listStyleMarginBottom = [styleConfig listStyleMarginBottom];
292
+ cache.listStyleMarginLeft = [styleConfig listStyleMarginLeft];
293
+ cache.imageMarginBottom = [styleConfig imageMarginBottom];
294
+ cache.imageBorderRadius = [styleConfig imageBorderRadius];
295
+ cache.h1FontSize = [styleConfig h1FontSize];
296
+ cache.h2FontSize = [styleConfig h2FontSize];
297
+ cache.h3FontSize = [styleConfig h3FontSize];
298
+ cache.h4FontSize = [styleConfig h4FontSize];
299
+ cache.h5FontSize = [styleConfig h5FontSize];
300
+ cache.h6FontSize = [styleConfig h6FontSize];
301
+ cache.h1MarginBottom = [styleConfig h1MarginBottom];
302
+ cache.h2MarginBottom = [styleConfig h2MarginBottom];
303
+ cache.h3MarginBottom = [styleConfig h3MarginBottom];
304
+ cache.h4MarginBottom = [styleConfig h4MarginBottom];
305
+ cache.h5MarginBottom = [styleConfig h5MarginBottom];
306
+ cache.h6MarginBottom = [styleConfig h6MarginBottom];
307
+ cache.h1FontWeight = fontWeightToCSS([styleConfig h1FontWeight]);
308
+ cache.h2FontWeight = fontWeightToCSS([styleConfig h2FontWeight]);
309
+ cache.h3FontWeight = fontWeightToCSS([styleConfig h3FontWeight]);
310
+ cache.h4FontWeight = fontWeightToCSS([styleConfig h4FontWeight]);
311
+ cache.h5FontWeight = fontWeightToCSS([styleConfig h5FontWeight]);
312
+ cache.h6FontWeight = fontWeightToCSS([styleConfig h6FontWeight]);
313
+ cache.linkFontFamily = [styleConfig linkFontFamily];
314
+ cache.linkColor = colorToCSS([styleConfig linkColor]);
315
+ cache.linkUnderline = [styleConfig linkUnderline];
316
+ cache.thematicBreakColor = colorToCSS([styleConfig thematicBreakColor]);
317
+ cache.thematicBreakHeight = [styleConfig thematicBreakHeight];
318
+ cache.thematicBreakMarginTop = [styleConfig thematicBreakMarginTop];
319
+ cache.thematicBreakMarginBottom = [styleConfig thematicBreakMarginBottom];
320
+ cache.strikethroughColor = colorToCSS([styleConfig strikethroughColor]);
321
+ cache.underlineColor = colorToCSS([styleConfig underlineColor]);
322
+ cache.highlightColor = colorToCSS([styleConfig highlightColor]);
323
+ cache.highlightBackgroundColor = colorToCSS([styleConfig highlightBackgroundColor]);
324
+
325
+ return cache;
326
+ }
327
+
328
+ #pragma mark - Font Helpers
329
+
330
+ static BOOL isMonospaceFont(UIFont *font)
331
+ {
332
+ if (!font)
333
+ return NO;
334
+ NSString *fontName = font.fontName.lowercaseString;
335
+ return [fontName containsString:@"menlo"] || [fontName containsString:@"courier"] ||
336
+ [fontName containsString:@"monaco"] || [fontName containsString:@"consolas"];
337
+ }
338
+
339
+ static BOOL isCodeSpan(NSDictionary *attrs, BOOL isCodeBlock)
340
+ {
341
+ if (isCodeBlock)
342
+ return NO;
343
+
344
+ NSNumber *codeAttr = attrs[CodeAttributeName];
345
+ if ([codeAttr boolValue])
346
+ return YES;
347
+
348
+ return isMonospaceFont(attrs[NSFontAttributeName]);
349
+ }
350
+
351
+ #pragma mark - Paragraph Type Detection
352
+
353
+ static ParagraphType getParagraphType(NSDictionary *attrs)
354
+ {
355
+ NSNumber *isCodeBlock = attrs[CodeBlockAttributeName];
356
+ if ([isCodeBlock boolValue])
357
+ return ParagraphTypeCodeBlock;
358
+
359
+ NSString *tiptapType = attrs[TiptapTypeAttributeName];
360
+ if (tiptapType) {
361
+ if ([tiptapType hasPrefix:@"heading-"]) {
362
+ NSInteger level = [[tiptapType substringFromIndex:8] integerValue];
363
+ if (level >= 1 && level <= 6) {
364
+ return ParagraphTypeHeading1 + (level - 1);
365
+ }
366
+ }
367
+ if ([tiptapType isEqualToString:@"code-block"])
368
+ return ParagraphTypeCodeBlock;
369
+ }
370
+
371
+ NSNumber *blockquoteDepth = attrs[BlockquoteDepthAttributeName];
372
+ if (blockquoteDepth && [blockquoteDepth integerValue] >= 0)
373
+ return ParagraphTypeBlockquote;
374
+
375
+ NSNumber *listDepth = attrs[ListDepthAttribute];
376
+ if (listDepth && [listDepth integerValue] >= 0) {
377
+ if ([attrs[TaskItemAttribute] boolValue])
378
+ return ParagraphTypeTaskList;
379
+ NSNumber *listType = attrs[ListTypeAttribute];
380
+ if (listType && [listType integerValue] == ListTypeOrdered)
381
+ return ParagraphTypeListItemOrdered;
382
+ return ParagraphTypeListItemUnordered;
383
+ }
384
+
385
+ return ParagraphTypeNormal;
386
+ }
387
+
388
+ #pragma mark - Paragraph Collection
389
+
390
+ static NSData *collectParagraphsData(NSAttributedString *attributedString, NSUInteger *outCount)
391
+ {
392
+ NSString *string = attributedString.string;
393
+ NSMutableData *data = [NSMutableData dataWithCapacity:string.length / 20 * sizeof(ParagraphData)];
394
+ NSUInteger currentIndex = 0;
395
+ NSUInteger count = 0;
396
+
397
+ while (currentIndex < string.length) {
398
+ // Hard breaks render as U+2028 and must stay inside their block, so split on paragraphs, not lines.
399
+ NSRange lineRange = [string paragraphRangeForRange:NSMakeRange(currentIndex, 0)];
400
+
401
+ ParagraphData para = {
402
+ .range = lineRange, .type = ParagraphTypeNormal, .depth = 0, .listNumber = 1, .isTaskChecked = NO};
403
+
404
+ if (lineRange.location < attributedString.length) {
405
+ NSDictionary *attrs = [attributedString attributesAtIndex:lineRange.location effectiveRange:NULL];
406
+ para.type = getParagraphType(attrs);
407
+
408
+ NSNumber *listDepth = attrs[ListDepthAttribute];
409
+ NSNumber *blockquoteDepth = attrs[BlockquoteDepthAttributeName];
410
+ NSNumber *listNumber = attrs[ListItemNumberAttribute];
411
+
412
+ para.depth = listDepth ? [listDepth integerValue] : (blockquoteDepth ? [blockquoteDepth integerValue] : 0);
413
+ para.listNumber = listNumber ? [listNumber integerValue] : 1;
414
+
415
+ if (para.type == ParagraphTypeTaskList) {
416
+ para.isTaskChecked = [attrs[TaskCheckedAttribute] boolValue];
417
+ }
418
+ }
419
+
420
+ [data appendBytes:&para length:sizeof(ParagraphData)];
421
+ count++;
422
+ currentIndex = NSMaxRange(lineRange);
423
+ }
424
+
425
+ *outCount = count;
426
+ return data;
427
+ }
428
+
429
+ #pragma mark - Heading Helpers
430
+
431
+ static NSInteger headingLevel(ParagraphType type)
432
+ {
433
+ if (type >= ParagraphTypeHeading1 && type <= ParagraphTypeHeading6) {
434
+ return type - ParagraphTypeHeading1 + 1;
435
+ }
436
+ return 0;
437
+ }
438
+
439
+ #pragma mark - Container Closing Helpers
440
+
441
+ static void closeBlockquotes(NSMutableString *html, GeneratorState *state)
442
+ {
443
+ while (state.currentBlockquoteDepth >= 0) {
444
+ [html appendString:@"</blockquote>"];
445
+ state.currentBlockquoteDepth--;
446
+ }
447
+ state.inBlockquote = NO;
448
+ }
449
+
450
+ static void closeLists(NSMutableString *html, GeneratorState *state)
451
+ {
452
+ while (state.openListTypes.count > 0) {
453
+ NSString *closeTag = ([state.openListTypes.lastObject integerValue] == 1) ? @"</ol>" : @"</ul>";
454
+ [html appendString:closeTag];
455
+ [state.openListTypes removeLastObject];
456
+ }
457
+ state.currentListDepth = -1;
458
+ }
459
+
460
+ static void closeCodeBlock(NSMutableString *html, GeneratorState *state)
461
+ {
462
+ if (state.inCodeBlock) {
463
+ [html appendString:@"</code></pre>"];
464
+ state.inCodeBlock = NO;
465
+ }
466
+ }
467
+
468
+ #pragma mark - Inline Content Generation
469
+
470
+ static void generateInlineHTML(NSMutableString *html, NSAttributedString *attributedString, NSRange range,
471
+ CachedStyles *styles, BOOL isCodeBlock)
472
+ {
473
+ NSString *string = attributedString.string;
474
+
475
+ [attributedString
476
+ enumerateAttributesInRange:range
477
+ options:0
478
+ usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange attrRange, BOOL *stop) {
479
+ NSString *text = [string substringWithRange:attrRange];
480
+
481
+ if ([text isEqualToString:@"\n"])
482
+ return;
483
+
484
+ if ([text containsString:kObjectReplacementChar]) {
485
+ id attachment = attrs[NSAttachmentAttributeName];
486
+ if ([attachment isKindOfClass:[ENRMImageAttachment class]]) {
487
+ ENRMImageAttachment *img = (ENRMImageAttachment *)attachment;
488
+ if (img.imageURL) {
489
+ if (img.isInline) {
490
+ [html appendFormat:
491
+ @"<img src=\"%@\" style=\"height: 1.2em; width: auto; "
492
+ @"vertical-align: -0.2em;\">",
493
+ img.imageURL];
494
+ } else {
495
+ [html appendFormat:
496
+ @"</p><div style=\"margin-bottom: %.0fpx;\"><img src=\"%@\" "
497
+ @"style=\"max-width: 100%%; border-radius: %.0fpx;\"></div><p>",
498
+ styles.imageMarginBottom, img.imageURL, styles.imageBorderRadius];
499
+ }
500
+ }
501
+ } else if ([attachment isKindOfClass:[ThematicBreakAttachment class]]) {
502
+ [html appendFormat:
503
+ @"</p><hr style=\"border: none; border-top: %.0fpx solid %@; "
504
+ @"margin: %.0fpx 0 %.0fpx 0;\"><p>",
505
+ styles.thematicBreakHeight, styles.thematicBreakColor,
506
+ styles.thematicBreakMarginTop, styles.thematicBreakMarginBottom];
507
+ #if ENRICHED_TIPTAP_MATH
508
+ } else if ([attachment isKindOfClass:[ENRMMathInlineAttachment class]]) {
509
+ ENRMMathInlineAttachment *math = (ENRMMathInlineAttachment *)attachment;
510
+ if (math.latex.length > 0) {
511
+ [html appendFormat:
512
+ @"<code style=\"background-color: %@; color: %@; "
513
+ @"padding: %.0fpx %.0fpx; border-radius: %.0fpx; "
514
+ @"font-size: 1em; font-family: Menlo, Monaco, Consolas, monospace;\">%@</code>",
515
+ styles.codeBackgroundColor, styles.codeColor, kCodePadding, kCodePadding * 2,
516
+ kCodeBorderRadius, escapeHTML(math.latex, NO)];
517
+ }
518
+ #endif
519
+ }
520
+ return;
521
+ }
522
+
523
+ UIFont *font = attrs[NSFontAttributeName];
524
+ NSNumber *underline = attrs[NSUnderlineStyleAttributeName];
525
+ NSNumber *strikethrough = attrs[NSStrikethroughStyleAttributeName];
526
+ id linkAttr = attrs[NSLinkAttributeName];
527
+ BOOL isCode = isCodeSpan(attrs, isCodeBlock);
528
+
529
+ BOOL isBold = NO, isItalic = NO;
530
+ if (font) {
531
+ UIFontDescriptorSymbolicTraits traits = font.fontDescriptor.symbolicTraits;
532
+ isBold = (traits & UIFontDescriptorTraitBold) != 0;
533
+ isItalic = (traits & UIFontDescriptorTraitItalic) != 0;
534
+ }
535
+
536
+ BOOL isHighlight = [attrs[HighlightAttributeName] boolValue];
537
+
538
+ if (isHighlight) {
539
+ NSMutableString *markStyle = [NSMutableString
540
+ stringWithFormat:@"background-color: %@", styles.highlightBackgroundColor];
541
+ if (styles.highlightColor && ![styles.highlightColor isEqualToString:styles.paragraphColor]) {
542
+ [markStyle appendFormat:@"; color: %@", styles.highlightColor];
543
+ } else {
544
+ [markStyle appendString:@"; color: inherit"];
545
+ }
546
+ [html appendFormat:@"<mark style=\"%@\">", markStyle];
547
+ }
548
+
549
+ if (linkAttr) {
550
+ NSString *href =
551
+ [linkAttr isKindOfClass:[NSURL class]] ? [(NSURL *)linkAttr absoluteString] : linkAttr;
552
+ if (styles.linkFontFamily.length > 0) {
553
+ [html appendFormat:
554
+ @"<a href=\"%@\" style=\"color: %@; text-decoration: %@; font-family: '%@';\">",
555
+ escapeHTML(href, NO), styles.linkColor,
556
+ styles.linkUnderline ? @"underline" : @"none", styles.linkFontFamily];
557
+ } else {
558
+ [html appendFormat:@"<a href=\"%@\" style=\"color: %@; text-decoration: %@;\">",
559
+ escapeHTML(href, NO), styles.linkColor,
560
+ styles.linkUnderline ? @"underline" : @"none"];
561
+ }
562
+ }
563
+
564
+ if (isCode) {
565
+ [html appendFormat:
566
+ @"<code style=\"background-color: %@; color: %@; "
567
+ @"padding: %.0fpx %.0fpx; border-radius: %.0fpx; "
568
+ @"font-size: 1em; font-family: Menlo, Monaco, Consolas, monospace;\">",
569
+ styles.codeBackgroundColor, styles.codeColor, kCodePadding, kCodePadding * 2,
570
+ kCodeBorderRadius];
571
+ }
572
+
573
+ if (isBold) {
574
+ if (styles.strongColor && ![styles.strongColor isEqualToString:@"inherit"]) {
575
+ [html appendFormat:@"<strong style=\"color: %@;\">", styles.strongColor];
576
+ } else {
577
+ [html appendString:@"<strong>"];
578
+ }
579
+ }
580
+
581
+ if (isItalic) {
582
+ if (styles.emphasisColor && ![styles.emphasisColor isEqualToString:@"inherit"]) {
583
+ [html appendFormat:@"<em style=\"color: %@;\">", styles.emphasisColor];
584
+ } else {
585
+ [html appendString:@"<em>"];
586
+ }
587
+ }
588
+
589
+ if ([strikethrough integerValue] > 0) {
590
+ if (styles.strikethroughColor && ![styles.strikethroughColor isEqualToString:@"inherit"]) {
591
+ [html appendFormat:@"<s style=\"text-decoration-color: %@;\">", styles.strikethroughColor];
592
+ } else {
593
+ [html appendString:@"<s>"];
594
+ }
595
+ }
596
+ if ([underline integerValue] > 0 && !linkAttr) {
597
+ if (styles.underlineColor && ![styles.underlineColor isEqualToString:@"inherit"]) {
598
+ [html appendFormat:@"<u style=\"text-decoration-color: %@;\">", styles.underlineColor];
599
+ } else {
600
+ [html appendString:@"<u>"];
601
+ }
602
+ }
603
+
604
+ [html appendString:escapeHTML(text, !isCodeBlock)];
605
+
606
+ // Reverse order
607
+ if ([underline integerValue] > 0 && !linkAttr)
608
+ [html appendString:@"</u>"];
609
+ if ([strikethrough integerValue] > 0)
610
+ [html appendString:@"</s>"];
611
+ if (isItalic)
612
+ [html appendString:@"</em>"];
613
+ if (isBold)
614
+ [html appendString:@"</strong>"];
615
+ if (isCode)
616
+ [html appendString:@"</code>"];
617
+ if (linkAttr)
618
+ [html appendString:@"</a>"];
619
+ if (isHighlight)
620
+ [html appendString:@"</mark>"];
621
+ }];
622
+ }
623
+
624
+ #pragma mark - Block Handlers
625
+
626
+ static void handleCodeBlock(NSMutableString *html, NSMutableString *inlineContent, CachedStyles *styles,
627
+ GeneratorState *state)
628
+ {
629
+ if (!state.inCodeBlock) {
630
+ state.inCodeBlock = YES;
631
+ [html appendFormat:
632
+ @"<pre dir=\"ltr\" style=\"background-color: %@; padding: %.0fpx; border-radius: %.0fpx; "
633
+ @"margin: 0 0 %.0fpx 0; overflow-x: auto; text-align: left;\"><code style=\"font-family: Menlo, Monaco, "
634
+ @"Consolas, monospace; font-size: %.0fpx; color: %@;\">",
635
+ styles.codeBlockBackgroundColor, styles.codeBlockPadding, styles.codeBlockBorderRadius,
636
+ styles.codeBlockMarginBottom, styles.codeBlockFontSize, styles.codeBlockColor];
637
+ } else if (state.previousWasCodeBlock) {
638
+ [html appendString:@"\n"];
639
+ }
640
+
641
+ [html appendString:inlineContent];
642
+ state.previousWasCodeBlock = YES;
643
+ }
644
+
645
+ static void handleBlockquote(NSMutableString *html, ParagraphData *para, NSMutableString *inlineContent,
646
+ CachedStyles *styles, GeneratorState *state)
647
+ {
648
+ NSInteger depth = para->depth;
649
+
650
+ // Reset if starting a new blockquote block
651
+ if (!state.previousWasBlockquote && state.inBlockquote) {
652
+ closeBlockquotes(html, state);
653
+ }
654
+
655
+ while (state.currentBlockquoteDepth > depth) {
656
+ [html appendString:@"</blockquote>"];
657
+ state.currentBlockquoteDepth--;
658
+ }
659
+
660
+ while (state.currentBlockquoteDepth < depth) {
661
+ state.currentBlockquoteDepth++;
662
+ state.inBlockquote = YES;
663
+
664
+ if (state.currentBlockquoteDepth == 0) {
665
+ [html appendFormat:
666
+ @"<blockquote style=\"background-color: %@; border-inline-start: %.0fpx solid %@; "
667
+ @"padding: %.0fpx %.0fpx; margin: 0 0 %.0fpx 0; border-start-end-radius: 8px; border-end-end-radius: "
668
+ @"8px;\">",
669
+ styles.blockquoteBackgroundColor, styles.blockquoteBorderWidth, styles.blockquoteBorderColor,
670
+ kBlockquoteVerticalPadding, styles.blockquoteGapWidth, styles.blockquoteMarginBottom];
671
+ } else {
672
+ [html appendFormat:
673
+ @"<blockquote style=\"border-inline-start: %.0fpx solid %@; padding-inline-start: %.0fpx; "
674
+ @"margin: %.0fpx 0 0 0;\">",
675
+ styles.blockquoteBorderWidth, styles.blockquoteBorderColor, styles.blockquoteGapWidth,
676
+ kNestedBlockquoteTopMargin];
677
+ }
678
+ }
679
+
680
+ [html appendFormat:@"<p style=\"margin: 0 0 %.0fpx 0; color: %@; font-size: %.0fpx;\">%@</p>",
681
+ kBlockquoteParagraphSpacing, styles.blockquoteColor, styles.blockquoteFontSize, inlineContent];
682
+
683
+ state.previousWasBlockquote = YES;
684
+ }
685
+
686
+ static void handleListItem(NSMutableString *html, ParagraphData *para, NSMutableString *inlineContent,
687
+ CachedStyles *styles, GeneratorState *state)
688
+ {
689
+ NSInteger depth = para->depth;
690
+ BOOL isOrdered = (para->type == ParagraphTypeListItemOrdered);
691
+ BOOL isTask = (para->type == ParagraphTypeTaskList);
692
+ NSInteger listTypeValue = isOrdered ? 1 : 0;
693
+
694
+ while (state.currentListDepth > depth) {
695
+ NSString *closeTag = ([state.openListTypes.lastObject integerValue] == 1) ? @"</ol>" : @"</ul>";
696
+ [html appendString:closeTag];
697
+ [state.openListTypes removeLastObject];
698
+ state.currentListDepth--;
699
+ }
700
+
701
+ // List type change at same depth (ul <-> ol)
702
+ if (state.currentListDepth == depth && state.openListTypes.count > 0) {
703
+ NSInteger currentType = [state.openListTypes.lastObject integerValue];
704
+ if (currentType != listTypeValue) {
705
+ NSString *closeTag = (currentType == 1) ? @"</ol>" : @"</ul>";
706
+ [html appendString:closeTag];
707
+ [state.openListTypes removeLastObject];
708
+ state.currentListDepth--;
709
+ }
710
+ }
711
+
712
+ CGFloat indent = styles.listStyleMarginLeft > 0 ? styles.listStyleMarginLeft : kDefaultListIndent;
713
+
714
+ while (state.currentListDepth < depth) {
715
+ state.currentListDepth++;
716
+ if (isOrdered) {
717
+ [html appendFormat:@"<ol style=\"margin: 0; padding-inline-start: %.0fpx;\">", indent];
718
+ } else {
719
+ NSString *listStyleType = isTask ? @"none" : @"disc";
720
+ [html appendFormat:@"<ul style=\"margin: 0; padding-inline-start: %.0fpx; list-style-type: %@;\">", indent,
721
+ listStyleType];
722
+ }
723
+ [state.openListTypes addObject:@(listTypeValue)];
724
+ }
725
+
726
+ [html appendFormat:@"<li style=\"margin-bottom: %.0fpx; color: %@; font-size: %.0fpx;\">",
727
+ styles.listStyleMarginBottom, styles.listStyleColor, styles.listStyleFontSize];
728
+
729
+ if (isTask) {
730
+ CGFloat size = styles.taskCheckboxSize;
731
+ CGFloat radius = styles.taskCheckboxBorderRadius;
732
+ if (para->isTaskChecked) {
733
+ [html appendFormat:
734
+ @"<span style=\"display: inline-block; width: %.0fpx; height: %.0fpx; "
735
+ @"border-radius: %.0fpx; background-color: %@; color: %@; "
736
+ @"font-size: %.0fpx; line-height: %.0fpx; text-align: center; "
737
+ @"vertical-align: middle; margin-inline-end: 4px;\">&#10003;</span> ",
738
+ size, size, radius, styles.taskCheckedColor, styles.taskCheckmarkColor, size - 2, size];
739
+ } else {
740
+ [html appendFormat:
741
+ @"<span style=\"display: inline-block; width: %.0fpx; height: %.0fpx; "
742
+ @"border-radius: %.0fpx; border: 1.5px solid %@; "
743
+ @"vertical-align: middle; margin-inline-end: 4px;\"></span> ",
744
+ size, size, radius, styles.taskBorderColor];
745
+ }
746
+ }
747
+
748
+ [html appendFormat:@"%@</li>", inlineContent];
749
+ }
750
+
751
+ static void handleHeading(NSMutableString *html, ParagraphData *para, NSMutableString *inlineContent,
752
+ CachedStyles *styles)
753
+ {
754
+ NSInteger level = headingLevel(para->type);
755
+ CGFloat fontSize, marginBottom;
756
+ NSString *fontWeight, *color;
757
+
758
+ switch (level) {
759
+ case 1:
760
+ fontSize = styles.h1FontSize;
761
+ fontWeight = styles.h1FontWeight;
762
+ color = styles.h1Color;
763
+ marginBottom = styles.h1MarginBottom;
764
+ break;
765
+ case 2:
766
+ fontSize = styles.h2FontSize;
767
+ fontWeight = styles.h2FontWeight;
768
+ color = styles.h2Color;
769
+ marginBottom = styles.h2MarginBottom;
770
+ break;
771
+ case 3:
772
+ fontSize = styles.h3FontSize;
773
+ fontWeight = styles.h3FontWeight;
774
+ color = styles.h3Color;
775
+ marginBottom = styles.h3MarginBottom;
776
+ break;
777
+ case 4:
778
+ fontSize = styles.h4FontSize;
779
+ fontWeight = styles.h4FontWeight;
780
+ color = styles.h4Color;
781
+ marginBottom = styles.h4MarginBottom;
782
+ break;
783
+ case 5:
784
+ fontSize = styles.h5FontSize;
785
+ fontWeight = styles.h5FontWeight;
786
+ color = styles.h5Color;
787
+ marginBottom = styles.h5MarginBottom;
788
+ break;
789
+ case 6:
790
+ fontSize = styles.h6FontSize;
791
+ fontWeight = styles.h6FontWeight;
792
+ color = styles.h6Color;
793
+ marginBottom = styles.h6MarginBottom;
794
+ break;
795
+ default:
796
+ fontSize = 16;
797
+ fontWeight = @"normal";
798
+ color = @"inherit";
799
+ marginBottom = 0;
800
+ break;
801
+ }
802
+
803
+ [html appendFormat:@"<h%ld style=\"font-size: %.0fpx; font-weight: %@; color: %@; margin: 0 0 %.0fpx 0;\">%@</h%ld>",
804
+ (long)level, fontSize, fontWeight, color, marginBottom, inlineContent, (long)level];
805
+ }
806
+
807
+ static void handleParagraph(NSMutableString *html, NSMutableString *inlineContent, CachedStyles *styles)
808
+ {
809
+ [html appendFormat:@"<p style=\"margin: 0 0 %.0fpx 0; color: %@; font-size: %.0fpx;\">%@</p>",
810
+ styles.paragraphMarginBottom, styles.paragraphColor, styles.paragraphFontSize, inlineContent];
811
+ }
812
+
813
+ #pragma mark - Main Generator
814
+
815
+ static NSString *dirAttrFromParagraphStyle(NSParagraphStyle *_Nullable style)
816
+ {
817
+ if (!style || style.baseWritingDirection == NSWritingDirectionNatural) {
818
+ return @"auto";
819
+ }
820
+ return style.baseWritingDirection == NSWritingDirectionRightToLeft ? @"rtl" : @"ltr";
821
+ }
822
+
823
+ NSString *_Nullable generateHTML(NSAttributedString *attributedString, StyleConfig *styleConfig)
824
+ {
825
+ if (!attributedString || attributedString.length == 0)
826
+ return nil;
827
+
828
+ attributedString = ENRMAttributedStringByExpandingLinkPills(attributedString, NULL);
829
+
830
+ CachedStyles *styles = cacheStyles(styleConfig);
831
+
832
+ NSParagraphStyle *firstParagraphStyle = [attributedString attribute:NSParagraphStyleAttributeName
833
+ atIndex:0
834
+ effectiveRange:NULL];
835
+ NSString *dirAttr = dirAttrFromParagraphStyle(firstParagraphStyle);
836
+
837
+ NSMutableString *html = [NSMutableString stringWithCapacity:attributedString.length * 2];
838
+ [html appendFormat:
839
+ @"<!DOCTYPE html><html dir=\"%@\"><head><meta charset=\"UTF-8\"></head><body "
840
+ @"style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\">",
841
+ dirAttr];
842
+
843
+ NSUInteger paragraphCount = 0;
844
+ NSData *paragraphsData = collectParagraphsData(attributedString, &paragraphCount);
845
+ ParagraphData *paragraphs = (ParagraphData *)paragraphsData.bytes;
846
+
847
+ GeneratorState *state = [[GeneratorState alloc] init];
848
+ NSMutableString *inlineBuffer = [NSMutableString stringWithCapacity:256];
849
+
850
+ for (NSUInteger i = 0; i < paragraphCount; i++) {
851
+ ParagraphData *para = &paragraphs[i];
852
+
853
+ NSString *content = [[attributedString.string substringWithRange:para->range]
854
+ stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
855
+
856
+ if (content.length == 0) {
857
+ state.previousWasBlockquote = NO;
858
+ continue;
859
+ }
860
+
861
+ [inlineBuffer setString:@""];
862
+ NSRange contentRange = para->range;
863
+ if (contentRange.length > 0 &&
864
+ [[attributedString.string substringWithRange:NSMakeRange(NSMaxRange(contentRange) - 1, 1)]
865
+ isEqualToString:@"\n"]) {
866
+ contentRange.length--;
867
+ }
868
+
869
+ BOOL isCodeBlockPara = (para->type == ParagraphTypeCodeBlock);
870
+ generateInlineHTML(inlineBuffer, attributedString, contentRange, styles, isCodeBlockPara);
871
+
872
+ if (isCodeBlockPara) {
873
+ handleCodeBlock(html, inlineBuffer, styles, state);
874
+ continue;
875
+ }
876
+
877
+ if (state.inCodeBlock) {
878
+ closeCodeBlock(html, state);
879
+ }
880
+ state.previousWasCodeBlock = NO;
881
+
882
+ if (para->type == ParagraphTypeBlockquote) {
883
+ handleBlockquote(html, para, inlineBuffer, styles, state);
884
+ continue;
885
+ }
886
+
887
+ if (state.inBlockquote) {
888
+ closeBlockquotes(html, state);
889
+ }
890
+ state.previousWasBlockquote = NO;
891
+
892
+ if (para->type == ParagraphTypeListItemUnordered || para->type == ParagraphTypeListItemOrdered ||
893
+ para->type == ParagraphTypeTaskList) {
894
+ handleListItem(html, para, inlineBuffer, styles, state);
895
+ continue;
896
+ }
897
+
898
+ if (state.currentListDepth >= 0) {
899
+ closeLists(html, state);
900
+ }
901
+
902
+ NSInteger hLevel = headingLevel(para->type);
903
+ if (hLevel > 0) {
904
+ handleHeading(html, para, inlineBuffer, styles);
905
+ continue;
906
+ }
907
+
908
+ handleParagraph(html, inlineBuffer, styles);
909
+ }
910
+
911
+ closeCodeBlock(html, state);
912
+ closeBlockquotes(html, state);
913
+ closeLists(html, state);
914
+
915
+ [html appendString:@"</body></html>"];
916
+
917
+ return html;
918
+ }
919
+
920
+ #pragma mark - Table HTML Generator
921
+
922
+ typedef NSString *HTMLString;
923
+
924
+ static NSString *alignmentToCSS(NSTextAlignment alignment)
925
+ {
926
+ switch (alignment) {
927
+ case NSTextAlignmentCenter:
928
+ return @"center";
929
+ case NSTextAlignmentRight:
930
+ return @"right";
931
+ case NSTextAlignmentJustified:
932
+ return @"justify";
933
+ default:
934
+ return @"left";
935
+ }
936
+ }
937
+
938
+ static NSString *styleForCell(BOOL isHeader, NSUInteger rowIndex, StyleConfig *styleConfig, NSTextAlignment alignment)
939
+ {
940
+ NSString *backgroundColor = isHeader ? colorToCSS(styleConfig.tableHeaderBackgroundColor)
941
+ : (rowIndex % 2 == 0 ? colorToCSS(styleConfig.tableRowEvenBackgroundColor)
942
+ : colorToCSS(styleConfig.tableRowOddBackgroundColor));
943
+
944
+ NSString *textColor = isHeader ? colorToCSS(styleConfig.tableHeaderTextColor) : colorToCSS(styleConfig.tableColor);
945
+
946
+ return [NSString stringWithFormat:
947
+ @"padding: %.0fpx %.0fpx; "
948
+ "text-align: %@; "
949
+ "background-color: %@; "
950
+ "color: %@; "
951
+ "border: %.0fpx solid %@; "
952
+ "font-weight: %@;",
953
+ styleConfig.tableCellPaddingVertical, styleConfig.tableCellPaddingHorizontal,
954
+ alignmentToCSS(alignment), backgroundColor, textColor, styleConfig.tableBorderWidth,
955
+ colorToCSS(styleConfig.tableBorderColor), isHeader ? @"bold" : @"normal"];
956
+ }
957
+
958
+ static NSString *tableDirAttrFromRows(NSArray<NSArray<NSDictionary *> *> *rows)
959
+ {
960
+ for (NSArray<NSDictionary *> *row in rows) {
961
+ for (NSDictionary *cell in row) {
962
+ NSAttributedString *cellText = cell[@"attributedText"];
963
+ if (cellText.length == 0) {
964
+ continue;
965
+ }
966
+ NSParagraphStyle *style = [cellText attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:NULL];
967
+ return dirAttrFromParagraphStyle(style);
968
+ }
969
+ }
970
+ return @"auto";
971
+ }
972
+
973
+ HTMLString _Nullable generateTableHTML(NSArray<NSArray<NSDictionary *> *> *rows, StyleConfig *styleConfig)
974
+ {
975
+ if (rows.count == 0)
976
+ return nil;
977
+
978
+ CachedStyles *cachedStyles = cacheStyles(styleConfig);
979
+ NSMutableString *htmlOutput = [NSMutableString stringWithCapacity:rows.count * 250];
980
+
981
+ NSString *dirAttr = tableDirAttrFromRows(rows);
982
+
983
+ [htmlOutput appendFormat:
984
+ @"<table dir=\"%@\" style=\"border-collapse: separate; border-spacing: 0; "
985
+ "border: %.0fpx solid %@; border-radius: %.0fpx; overflow: hidden; font-size: %.0fpx;\">",
986
+ dirAttr, styleConfig.tableBorderWidth, colorToCSS(styleConfig.tableBorderColor),
987
+ styleConfig.tableBorderRadius, styleConfig.tableFontSize];
988
+
989
+ BOOL hasOpenedBody = NO;
990
+ NSUInteger bodyRowIndex = 0;
991
+
992
+ for (NSArray<NSDictionary *> *row in rows) {
993
+ if (row.count == 0)
994
+ continue;
995
+
996
+ BOOL isHeaderRow = [row.firstObject[@"isHeader"] boolValue];
997
+
998
+ if (isHeaderRow) {
999
+ [htmlOutput appendString:@"<thead>"];
1000
+ } else if (!hasOpenedBody) {
1001
+ [htmlOutput appendString:@"<tbody>"];
1002
+ hasOpenedBody = YES;
1003
+ }
1004
+
1005
+ [htmlOutput appendString:@"<tr>"];
1006
+
1007
+ for (NSDictionary *cellInfo in row) {
1008
+ BOOL isHeaderCell = [cellInfo[@"isHeader"] boolValue];
1009
+ NSTextAlignment alignment = (NSTextAlignment)[cellInfo[@"alignment"] integerValue];
1010
+ NSAttributedString *attributedText = cellInfo[@"attributedText"];
1011
+ if (attributedText)
1012
+ attributedText = ENRMAttributedStringByExpandingLinkPills(attributedText, NULL);
1013
+
1014
+ NSString *htmlTag = isHeaderCell ? @"th" : @"td";
1015
+ NSString *cellStyle = styleForCell(isHeaderCell, bodyRowIndex, styleConfig, alignment);
1016
+
1017
+ [htmlOutput appendFormat:@"<%@ style=\"%@\">", htmlTag, cellStyle];
1018
+
1019
+ if (attributedText.length > 0) {
1020
+ generateInlineHTML(htmlOutput, attributedText, NSMakeRange(0, attributedText.length), cachedStyles, NO);
1021
+ }
1022
+
1023
+ [htmlOutput appendFormat:@"</%@>", htmlTag];
1024
+ }
1025
+
1026
+ [htmlOutput appendString:@"</tr>"];
1027
+
1028
+ if (isHeaderRow) {
1029
+ [htmlOutput appendString:@"</thead>"];
1030
+ } else {
1031
+ bodyRowIndex++;
1032
+ }
1033
+ }
1034
+
1035
+ if (hasOpenedBody) {
1036
+ [htmlOutput appendString:@"</tbody>"];
1037
+ }
1038
+
1039
+ [htmlOutput appendString:@"</table>"];
1040
+ return [htmlOutput copy];
1041
+ }