react-native-enriched-markdown 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +551 -0
  3. package/ReactNativeEnrichedMarkdown.podspec +27 -0
  4. package/android/build.gradle +101 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +54 -0
  6. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +26 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
  9. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +33 -0
  10. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +31 -0
  11. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +82 -0
  12. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1388 -0
  13. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
  14. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
  15. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.cpp +16 -0
  16. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.h +20 -0
  17. package/android/gradle.properties +5 -0
  18. package/android/src/main/AndroidManifest.xml +2 -0
  19. package/android/src/main/baseline-prof.txt +65 -0
  20. package/android/src/main/cpp/jni-adapter.cpp +220 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +270 -0
  22. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +15 -0
  23. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +173 -0
  24. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextPackage.kt +17 -0
  25. package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +385 -0
  26. package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/MarkdownAccessibilityHelper.kt +279 -0
  27. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLongPressEvent.kt +23 -0
  28. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkPressEvent.kt +23 -0
  29. package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +31 -0
  30. package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +62 -0
  31. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +166 -0
  32. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +84 -0
  33. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +104 -0
  34. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +36 -0
  35. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +16 -0
  36. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +27 -0
  37. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +70 -0
  38. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +68 -0
  39. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +16 -0
  40. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +29 -0
  41. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt +105 -0
  42. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +59 -0
  43. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +76 -0
  44. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +103 -0
  45. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +80 -0
  46. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +109 -0
  47. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +86 -0
  48. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt +27 -0
  49. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +27 -0
  50. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +30 -0
  51. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +45 -0
  52. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/UnderlineRenderer.kt +27 -0
  53. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BaseListSpan.kt +136 -0
  54. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BlockquoteSpan.kt +135 -0
  55. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +180 -0
  56. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBlockSpan.kt +196 -0
  57. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeSpan.kt +27 -0
  58. package/android/src/main/java/com/swmansion/enriched/markdown/spans/EmphasisSpan.kt +34 -0
  59. package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +38 -0
  60. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +321 -0
  61. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +27 -0
  62. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +51 -0
  63. package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +76 -0
  64. package/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +87 -0
  65. package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrikethroughSpan.kt +12 -0
  66. package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrongSpan.kt +37 -0
  67. package/android/src/main/java/com/swmansion/enriched/markdown/spans/TextSpan.kt +26 -0
  68. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ThematicBreakSpan.kt +69 -0
  69. package/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +69 -0
  70. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +11 -0
  71. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +51 -0
  72. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +54 -0
  73. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeStyle.kt +21 -0
  74. package/android/src/main/java/com/swmansion/enriched/markdown/styles/EmphasisStyle.kt +17 -0
  75. package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +33 -0
  76. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +23 -0
  77. package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineImageStyle.kt +17 -0
  78. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +19 -0
  79. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +57 -0
  80. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +33 -0
  81. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrikethroughStyle.kt +17 -0
  82. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrongStyle.kt +17 -0
  83. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +211 -0
  84. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +92 -0
  85. package/android/src/main/java/com/swmansion/enriched/markdown/styles/TextAlignment.kt +32 -0
  86. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ThematicBreakStyle.kt +23 -0
  87. package/android/src/main/java/com/swmansion/enriched/markdown/styles/UnderlineStyle.kt +17 -0
  88. package/android/src/main/java/com/swmansion/enriched/markdown/utils/AsyncDrawable.kt +91 -0
  89. package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +827 -0
  90. package/android/src/main/java/com/swmansion/enriched/markdown/utils/LinkLongPressMovementMethod.kt +121 -0
  91. package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +375 -0
  92. package/android/src/main/java/com/swmansion/enriched/markdown/utils/SelectionActionMode.kt +139 -0
  93. package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +183 -0
  94. package/android/src/main/jni/CMakeLists.txt +70 -0
  95. package/android/src/main/jni/EnrichedMarkdownTextSpec.cpp +21 -0
  96. package/android/src/main/jni/EnrichedMarkdownTextSpec.h +25 -0
  97. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextComponentDescriptor.h +29 -0
  98. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.cpp +45 -0
  99. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.h +21 -0
  100. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +20 -0
  101. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +37 -0
  102. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +22 -0
  103. package/cpp/md4c/md4c.c +6492 -0
  104. package/cpp/md4c/md4c.h +402 -0
  105. package/cpp/parser/MD4CParser.cpp +327 -0
  106. package/cpp/parser/MD4CParser.hpp +27 -0
  107. package/cpp/parser/MarkdownASTNode.hpp +51 -0
  108. package/ios/EnrichedMarkdownText.h +18 -0
  109. package/ios/EnrichedMarkdownText.mm +1401 -0
  110. package/ios/attachments/EnrichedMarkdownImageAttachment.h +23 -0
  111. package/ios/attachments/EnrichedMarkdownImageAttachment.m +185 -0
  112. package/ios/attachments/ThematicBreakAttachment.h +15 -0
  113. package/ios/attachments/ThematicBreakAttachment.m +33 -0
  114. package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
  115. package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
  116. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +33 -0
  117. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +31 -0
  118. package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +82 -0
  119. package/ios/generated/EnrichedMarkdownTextSpec/Props.h +1388 -0
  120. package/ios/generated/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +20 -0
  121. package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
  122. package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
  123. package/ios/generated/EnrichedMarkdownTextSpec/States.cpp +16 -0
  124. package/ios/generated/EnrichedMarkdownTextSpec/States.h +20 -0
  125. package/ios/internals/EnrichedMarkdownTextComponentDescriptor.h +19 -0
  126. package/ios/internals/EnrichedMarkdownTextShadowNode.h +43 -0
  127. package/ios/internals/EnrichedMarkdownTextShadowNode.mm +85 -0
  128. package/ios/internals/EnrichedMarkdownTextState.h +24 -0
  129. package/ios/parser/MarkdownASTNode.h +35 -0
  130. package/ios/parser/MarkdownASTNode.m +32 -0
  131. package/ios/parser/MarkdownParser.h +17 -0
  132. package/ios/parser/MarkdownParser.mm +42 -0
  133. package/ios/parser/MarkdownParserBridge.mm +120 -0
  134. package/ios/renderer/AttributedRenderer.h +11 -0
  135. package/ios/renderer/AttributedRenderer.m +152 -0
  136. package/ios/renderer/BlockquoteRenderer.h +7 -0
  137. package/ios/renderer/BlockquoteRenderer.m +160 -0
  138. package/ios/renderer/CodeBlockRenderer.h +10 -0
  139. package/ios/renderer/CodeBlockRenderer.m +90 -0
  140. package/ios/renderer/CodeRenderer.h +10 -0
  141. package/ios/renderer/CodeRenderer.m +60 -0
  142. package/ios/renderer/EmphasisRenderer.h +6 -0
  143. package/ios/renderer/EmphasisRenderer.m +96 -0
  144. package/ios/renderer/HeadingRenderer.h +7 -0
  145. package/ios/renderer/HeadingRenderer.m +105 -0
  146. package/ios/renderer/ImageRenderer.h +12 -0
  147. package/ios/renderer/ImageRenderer.m +83 -0
  148. package/ios/renderer/LinkRenderer.h +7 -0
  149. package/ios/renderer/LinkRenderer.m +69 -0
  150. package/ios/renderer/ListItemRenderer.h +16 -0
  151. package/ios/renderer/ListItemRenderer.m +103 -0
  152. package/ios/renderer/ListRenderer.h +13 -0
  153. package/ios/renderer/ListRenderer.m +70 -0
  154. package/ios/renderer/NodeRenderer.h +8 -0
  155. package/ios/renderer/ParagraphRenderer.h +7 -0
  156. package/ios/renderer/ParagraphRenderer.m +80 -0
  157. package/ios/renderer/RenderContext.h +105 -0
  158. package/ios/renderer/RenderContext.m +312 -0
  159. package/ios/renderer/RendererFactory.h +12 -0
  160. package/ios/renderer/RendererFactory.m +116 -0
  161. package/ios/renderer/StrikethroughRenderer.h +6 -0
  162. package/ios/renderer/StrikethroughRenderer.m +40 -0
  163. package/ios/renderer/StrongRenderer.h +6 -0
  164. package/ios/renderer/StrongRenderer.m +83 -0
  165. package/ios/renderer/TextRenderer.h +6 -0
  166. package/ios/renderer/TextRenderer.m +16 -0
  167. package/ios/renderer/ThematicBreakRenderer.h +5 -0
  168. package/ios/renderer/ThematicBreakRenderer.m +53 -0
  169. package/ios/renderer/UnderlineRenderer.h +6 -0
  170. package/ios/renderer/UnderlineRenderer.m +39 -0
  171. package/ios/styles/StyleConfig.h +274 -0
  172. package/ios/styles/StyleConfig.mm +1806 -0
  173. package/ios/utils/AccessibilityInfo.h +35 -0
  174. package/ios/utils/AccessibilityInfo.m +24 -0
  175. package/ios/utils/BlockquoteBorder.h +20 -0
  176. package/ios/utils/BlockquoteBorder.m +92 -0
  177. package/ios/utils/CodeBackground.h +19 -0
  178. package/ios/utils/CodeBackground.m +191 -0
  179. package/ios/utils/CodeBlockBackground.h +17 -0
  180. package/ios/utils/CodeBlockBackground.m +82 -0
  181. package/ios/utils/EditMenuUtils.h +22 -0
  182. package/ios/utils/EditMenuUtils.m +118 -0
  183. package/ios/utils/FontUtils.h +25 -0
  184. package/ios/utils/FontUtils.m +27 -0
  185. package/ios/utils/HTMLGenerator.h +20 -0
  186. package/ios/utils/HTMLGenerator.m +793 -0
  187. package/ios/utils/LastElementUtils.h +53 -0
  188. package/ios/utils/ListMarkerDrawer.h +15 -0
  189. package/ios/utils/ListMarkerDrawer.m +127 -0
  190. package/ios/utils/MarkdownAccessibilityElementBuilder.h +45 -0
  191. package/ios/utils/MarkdownAccessibilityElementBuilder.m +323 -0
  192. package/ios/utils/MarkdownExtractor.h +17 -0
  193. package/ios/utils/MarkdownExtractor.m +308 -0
  194. package/ios/utils/ParagraphStyleUtils.h +21 -0
  195. package/ios/utils/ParagraphStyleUtils.m +111 -0
  196. package/ios/utils/PasteboardUtils.h +36 -0
  197. package/ios/utils/PasteboardUtils.m +134 -0
  198. package/ios/utils/RTFExportUtils.h +24 -0
  199. package/ios/utils/RTFExportUtils.m +297 -0
  200. package/ios/utils/RuntimeKeys.h +38 -0
  201. package/ios/utils/RuntimeKeys.m +11 -0
  202. package/ios/utils/TextViewLayoutManager.h +14 -0
  203. package/ios/utils/TextViewLayoutManager.mm +113 -0
  204. package/lib/module/EnrichedMarkdownText.js +65 -0
  205. package/lib/module/EnrichedMarkdownText.js.map +1 -0
  206. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +210 -0
  207. package/lib/module/index.js +4 -0
  208. package/lib/module/index.js.map +1 -0
  209. package/lib/module/normalizeMarkdownStyle.js +384 -0
  210. package/lib/module/normalizeMarkdownStyle.js.map +1 -0
  211. package/lib/module/package.json +1 -0
  212. package/lib/typescript/package.json +1 -0
  213. package/lib/typescript/src/EnrichedMarkdownText.d.ts +183 -0
  214. package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -0
  215. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +185 -0
  216. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -0
  217. package/lib/typescript/src/index.d.ts +4 -0
  218. package/lib/typescript/src/index.d.ts.map +1 -0
  219. package/lib/typescript/src/normalizeMarkdownStyle.d.ts +6 -0
  220. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -0
  221. package/package.json +186 -1
  222. package/react-native.config.js +13 -0
  223. package/src/EnrichedMarkdownText.tsx +280 -0
  224. package/src/EnrichedMarkdownTextNativeComponent.ts +210 -0
  225. package/src/index.tsx +10 -0
  226. package/src/normalizeMarkdownStyle.ts +423 -0
@@ -0,0 +1,793 @@
1
+ #import "HTMLGenerator.h"
2
+ #import "BlockquoteBorder.h"
3
+ #import "CodeBackground.h"
4
+ #import "EnrichedMarkdownImageAttachment.h"
5
+ #import "LastElementUtils.h"
6
+ #import "ListItemRenderer.h"
7
+ #import "RenderContext.h"
8
+ #import "RuntimeKeys.h"
9
+ #import "StyleConfig.h"
10
+ #import "ThematicBreakAttachment.h"
11
+
12
+ static NSString *const kObjectReplacementChar = @"\uFFFC";
13
+ static const CGFloat kBlockquoteVerticalPadding = 8.0;
14
+ static const CGFloat kBlockquoteParagraphSpacing = 4.0;
15
+ static const CGFloat kNestedBlockquoteTopMargin = 8.0;
16
+ static const CGFloat kDefaultListIndent = 24.0;
17
+ static const CGFloat kCodePadding = 2.0;
18
+ static const CGFloat kCodeBorderRadius = 4.0;
19
+
20
+ #pragma mark - Paragraph Types
21
+
22
+ typedef NS_ENUM(NSInteger, ParagraphType) {
23
+ ParagraphTypeNormal,
24
+ ParagraphTypeHeading1,
25
+ ParagraphTypeHeading2,
26
+ ParagraphTypeHeading3,
27
+ ParagraphTypeHeading4,
28
+ ParagraphTypeHeading5,
29
+ ParagraphTypeHeading6,
30
+ ParagraphTypeCodeBlock,
31
+ ParagraphTypeBlockquote,
32
+ ParagraphTypeListItemUnordered,
33
+ ParagraphTypeListItemOrdered
34
+ };
35
+
36
+ typedef struct {
37
+ NSRange range;
38
+ ParagraphType type;
39
+ NSInteger depth;
40
+ NSInteger listNumber;
41
+ } ParagraphData;
42
+
43
+ #pragma mark - Cached Style Config
44
+
45
+ /// Pre-fetched style values to avoid repeated StyleConfig method calls
46
+ @interface CachedStyles : NSObject
47
+ @property (nonatomic, copy) NSString *paragraphColor;
48
+ @property (nonatomic, copy) NSString *strongColor;
49
+ @property (nonatomic, copy) NSString *emphasisColor;
50
+ @property (nonatomic, copy) NSString *linkColor;
51
+ @property (nonatomic, copy) NSString *codeColor;
52
+ @property (nonatomic, copy) NSString *codeBackgroundColor;
53
+ @property (nonatomic, copy) NSString *codeBlockColor;
54
+ @property (nonatomic, copy) NSString *codeBlockBackgroundColor;
55
+ @property (nonatomic, copy) NSString *blockquoteColor;
56
+ @property (nonatomic, copy) NSString *blockquoteBackgroundColor;
57
+ @property (nonatomic, copy) NSString *blockquoteBorderColor;
58
+ @property (nonatomic, copy) NSString *listStyleColor;
59
+ @property (nonatomic, copy) NSString *h1Color;
60
+ @property (nonatomic, copy) NSString *h2Color;
61
+ @property (nonatomic, copy) NSString *h3Color;
62
+ @property (nonatomic, copy) NSString *h4Color;
63
+ @property (nonatomic, copy) NSString *h5Color;
64
+ @property (nonatomic, copy) NSString *h6Color;
65
+ @property (nonatomic) CGFloat paragraphFontSize;
66
+ @property (nonatomic) CGFloat paragraphMarginBottom;
67
+ @property (nonatomic) CGFloat codeBlockFontSize;
68
+ @property (nonatomic) CGFloat codeBlockPadding;
69
+ @property (nonatomic) CGFloat codeBlockBorderRadius;
70
+ @property (nonatomic) CGFloat codeBlockMarginBottom;
71
+ @property (nonatomic) CGFloat blockquoteFontSize;
72
+ @property (nonatomic) CGFloat blockquoteBorderWidth;
73
+ @property (nonatomic) CGFloat blockquoteMarginBottom;
74
+ @property (nonatomic) CGFloat blockquoteGapWidth;
75
+ @property (nonatomic) CGFloat listStyleFontSize;
76
+ @property (nonatomic) CGFloat listStyleMarginBottom;
77
+ @property (nonatomic) CGFloat listStyleMarginLeft;
78
+ @property (nonatomic) CGFloat imageMarginBottom;
79
+ @property (nonatomic) CGFloat imageBorderRadius;
80
+ @property (nonatomic) CGFloat h1FontSize;
81
+ @property (nonatomic) CGFloat h2FontSize;
82
+ @property (nonatomic) CGFloat h3FontSize;
83
+ @property (nonatomic) CGFloat h4FontSize;
84
+ @property (nonatomic) CGFloat h5FontSize;
85
+ @property (nonatomic) CGFloat h6FontSize;
86
+ @property (nonatomic) CGFloat h1MarginBottom;
87
+ @property (nonatomic) CGFloat h2MarginBottom;
88
+ @property (nonatomic) CGFloat h3MarginBottom;
89
+ @property (nonatomic) CGFloat h4MarginBottom;
90
+ @property (nonatomic) CGFloat h5MarginBottom;
91
+ @property (nonatomic) CGFloat h6MarginBottom;
92
+ @property (nonatomic, copy) NSString *h1FontWeight;
93
+ @property (nonatomic, copy) NSString *h2FontWeight;
94
+ @property (nonatomic, copy) NSString *h3FontWeight;
95
+ @property (nonatomic, copy) NSString *h4FontWeight;
96
+ @property (nonatomic, copy) NSString *h5FontWeight;
97
+ @property (nonatomic, copy) NSString *h6FontWeight;
98
+ @property (nonatomic) BOOL linkUnderline;
99
+ @property (nonatomic, copy) NSString *thematicBreakColor;
100
+ @property (nonatomic) CGFloat thematicBreakHeight;
101
+ @property (nonatomic) CGFloat thematicBreakMarginTop;
102
+ @property (nonatomic) CGFloat thematicBreakMarginBottom;
103
+ @property (nonatomic, copy) NSString *strikethroughColor;
104
+ @property (nonatomic, copy) NSString *underlineColor;
105
+ @end
106
+
107
+ @implementation CachedStyles
108
+ @end
109
+
110
+ #pragma mark - Generator State
111
+
112
+ @interface GeneratorState : NSObject
113
+ @property (nonatomic) NSInteger currentListDepth;
114
+ @property (nonatomic) NSInteger currentBlockquoteDepth;
115
+ @property (nonatomic) BOOL inBlockquote;
116
+ @property (nonatomic) BOOL inCodeBlock;
117
+ @property (nonatomic) BOOL previousWasCodeBlock;
118
+ @property (nonatomic) BOOL previousWasBlockquote;
119
+ @property (nonatomic, strong) NSMutableArray<NSNumber *> *openListTypes;
120
+ @end
121
+
122
+ @implementation GeneratorState
123
+ - (instancetype)init
124
+ {
125
+ self = [super init];
126
+ if (self) {
127
+ _currentListDepth = -1;
128
+ _currentBlockquoteDepth = -1;
129
+ _openListTypes = [NSMutableArray array];
130
+ }
131
+ return self;
132
+ }
133
+ @end
134
+
135
+ #pragma mark - Color Conversion
136
+
137
+ static NSString *colorToCSS(UIColor *color)
138
+ {
139
+ if (!color)
140
+ return @"inherit";
141
+
142
+ CGFloat r = 0, g = 0, b = 0, a = 1;
143
+ [color getRed:&r green:&g blue:&b alpha:&a];
144
+
145
+ if (a < 1.0) {
146
+ return [NSString stringWithFormat:@"rgba(%.0f, %.0f, %.0f, %.2f)", r * 255, g * 255, b * 255, a];
147
+ }
148
+ return [NSString stringWithFormat:@"#%02X%02X%02X", (int)(r * 255), (int)(g * 255), (int)(b * 255)];
149
+ }
150
+
151
+ #pragma mark - HTML Escaping
152
+
153
+ static void appendEscapedHTML(NSMutableString *output, NSString *text)
154
+ {
155
+ NSUInteger length = text.length;
156
+ if (length == 0)
157
+ return;
158
+
159
+ for (NSUInteger i = 0; i < length; i++) {
160
+ unichar c = [text characterAtIndex:i];
161
+ switch (c) {
162
+ case '&':
163
+ [output appendString:@"&amp;"];
164
+ break;
165
+ case '<':
166
+ [output appendString:@"&lt;"];
167
+ break;
168
+ case '>':
169
+ [output appendString:@"&gt;"];
170
+ break;
171
+ case '"':
172
+ [output appendString:@"&quot;"];
173
+ break;
174
+ case '\'':
175
+ [output appendString:@"&#39;"];
176
+ break;
177
+ default:
178
+ [output appendFormat:@"%C", c];
179
+ break;
180
+ }
181
+ }
182
+ }
183
+
184
+ static NSString *escapeHTML(NSString *text)
185
+ {
186
+ // Fast path: skip if no special chars
187
+ static NSCharacterSet *escapeChars = nil;
188
+ static dispatch_once_t onceToken;
189
+ dispatch_once(&onceToken, ^{ escapeChars = [NSCharacterSet characterSetWithCharactersInString:@"&<>\"'"]; });
190
+
191
+ if ([text rangeOfCharacterFromSet:escapeChars].location == NSNotFound) {
192
+ return text;
193
+ }
194
+
195
+ NSMutableString *escaped = [NSMutableString stringWithCapacity:text.length + 16];
196
+ appendEscapedHTML(escaped, text);
197
+ return escaped;
198
+ }
199
+
200
+ #pragma mark - Font Weight Conversion
201
+
202
+ static NSString *fontWeightToCSS(NSString *fontWeight)
203
+ {
204
+ if (!fontWeight || fontWeight.length == 0)
205
+ return @"normal";
206
+
207
+ if ([fontWeight integerValue] > 0)
208
+ return fontWeight;
209
+
210
+ static NSDictionary *weightMap = nil;
211
+ static dispatch_once_t onceToken;
212
+ dispatch_once(&onceToken, ^{ weightMap = @{@"bold" : @"700", @"semibold" : @"600", @"medium" : @"500"}; });
213
+
214
+ NSString *mapped = weightMap[fontWeight.lowercaseString];
215
+ return mapped ?: @"normal";
216
+ }
217
+
218
+ #pragma mark - Style Caching
219
+
220
+ static CachedStyles *cacheStyles(StyleConfig *styleConfig)
221
+ {
222
+ CachedStyles *cache = [[CachedStyles alloc] init];
223
+
224
+ cache.paragraphColor = colorToCSS([styleConfig paragraphColor]);
225
+ cache.strongColor = colorToCSS([styleConfig strongColor]);
226
+ cache.emphasisColor = colorToCSS([styleConfig emphasisColor]);
227
+ cache.linkColor = colorToCSS([styleConfig linkColor]);
228
+ cache.codeColor = colorToCSS([styleConfig codeColor]);
229
+ cache.codeBackgroundColor = colorToCSS([styleConfig codeBackgroundColor]);
230
+ cache.codeBlockColor = colorToCSS([styleConfig codeBlockColor]);
231
+ cache.codeBlockBackgroundColor = colorToCSS([styleConfig codeBlockBackgroundColor]);
232
+ cache.blockquoteColor = colorToCSS([styleConfig blockquoteColor]);
233
+ cache.blockquoteBackgroundColor = colorToCSS([styleConfig blockquoteBackgroundColor]);
234
+ cache.blockquoteBorderColor = colorToCSS([styleConfig blockquoteBorderColor]);
235
+ cache.listStyleColor = colorToCSS([styleConfig listStyleColor]);
236
+ cache.h1Color = colorToCSS([styleConfig h1Color]);
237
+ cache.h2Color = colorToCSS([styleConfig h2Color]);
238
+ cache.h3Color = colorToCSS([styleConfig h3Color]);
239
+ cache.h4Color = colorToCSS([styleConfig h4Color]);
240
+ cache.h5Color = colorToCSS([styleConfig h5Color]);
241
+ cache.h6Color = colorToCSS([styleConfig h6Color]);
242
+
243
+ cache.paragraphFontSize = [styleConfig paragraphFontSize];
244
+ cache.paragraphMarginBottom = [styleConfig paragraphMarginBottom];
245
+ cache.codeBlockFontSize = [styleConfig codeBlockFontSize];
246
+ cache.codeBlockPadding = [styleConfig codeBlockPadding];
247
+ cache.codeBlockBorderRadius = [styleConfig codeBlockBorderRadius];
248
+ cache.codeBlockMarginBottom = [styleConfig codeBlockMarginBottom];
249
+ cache.blockquoteFontSize = [styleConfig blockquoteFontSize];
250
+ cache.blockquoteBorderWidth = [styleConfig blockquoteBorderWidth];
251
+ cache.blockquoteMarginBottom = [styleConfig blockquoteMarginBottom];
252
+ cache.blockquoteGapWidth = [styleConfig blockquoteGapWidth];
253
+ cache.listStyleFontSize = [styleConfig listStyleFontSize];
254
+ cache.listStyleMarginBottom = [styleConfig listStyleMarginBottom];
255
+ cache.listStyleMarginLeft = [styleConfig listStyleMarginLeft];
256
+ cache.imageMarginBottom = [styleConfig imageMarginBottom];
257
+ cache.imageBorderRadius = [styleConfig imageBorderRadius];
258
+ cache.h1FontSize = [styleConfig h1FontSize];
259
+ cache.h2FontSize = [styleConfig h2FontSize];
260
+ cache.h3FontSize = [styleConfig h3FontSize];
261
+ cache.h4FontSize = [styleConfig h4FontSize];
262
+ cache.h5FontSize = [styleConfig h5FontSize];
263
+ cache.h6FontSize = [styleConfig h6FontSize];
264
+ cache.h1MarginBottom = [styleConfig h1MarginBottom];
265
+ cache.h2MarginBottom = [styleConfig h2MarginBottom];
266
+ cache.h3MarginBottom = [styleConfig h3MarginBottom];
267
+ cache.h4MarginBottom = [styleConfig h4MarginBottom];
268
+ cache.h5MarginBottom = [styleConfig h5MarginBottom];
269
+ cache.h6MarginBottom = [styleConfig h6MarginBottom];
270
+ cache.h1FontWeight = fontWeightToCSS([styleConfig h1FontWeight]);
271
+ cache.h2FontWeight = fontWeightToCSS([styleConfig h2FontWeight]);
272
+ cache.h3FontWeight = fontWeightToCSS([styleConfig h3FontWeight]);
273
+ cache.h4FontWeight = fontWeightToCSS([styleConfig h4FontWeight]);
274
+ cache.h5FontWeight = fontWeightToCSS([styleConfig h5FontWeight]);
275
+ cache.h6FontWeight = fontWeightToCSS([styleConfig h6FontWeight]);
276
+ cache.linkUnderline = [styleConfig linkUnderline];
277
+ cache.thematicBreakColor = colorToCSS([styleConfig thematicBreakColor]);
278
+ cache.thematicBreakHeight = [styleConfig thematicBreakHeight];
279
+ cache.thematicBreakMarginTop = [styleConfig thematicBreakMarginTop];
280
+ cache.thematicBreakMarginBottom = [styleConfig thematicBreakMarginBottom];
281
+ cache.strikethroughColor = colorToCSS([styleConfig strikethroughColor]);
282
+ cache.underlineColor = colorToCSS([styleConfig underlineColor]);
283
+
284
+ return cache;
285
+ }
286
+
287
+ #pragma mark - Font Helpers
288
+
289
+ static BOOL isMonospaceFont(UIFont *font)
290
+ {
291
+ if (!font)
292
+ return NO;
293
+ NSString *fontName = font.fontName.lowercaseString;
294
+ return [fontName containsString:@"menlo"] || [fontName containsString:@"courier"] ||
295
+ [fontName containsString:@"monaco"] || [fontName containsString:@"consolas"];
296
+ }
297
+
298
+ static BOOL isCodeSpan(NSDictionary *attrs, BOOL isCodeBlock)
299
+ {
300
+ if (isCodeBlock)
301
+ return NO;
302
+
303
+ NSNumber *codeAttr = attrs[CodeAttributeName];
304
+ if ([codeAttr boolValue])
305
+ return YES;
306
+
307
+ return isMonospaceFont(attrs[NSFontAttributeName]);
308
+ }
309
+
310
+ #pragma mark - Paragraph Type Detection
311
+
312
+ static ParagraphType getParagraphType(NSDictionary *attrs)
313
+ {
314
+ NSNumber *isCodeBlock = attrs[CodeBlockAttributeName];
315
+ if ([isCodeBlock boolValue])
316
+ return ParagraphTypeCodeBlock;
317
+
318
+ NSString *markdownType = attrs[MarkdownTypeAttributeName];
319
+ if (markdownType) {
320
+ if ([markdownType hasPrefix:@"heading-"]) {
321
+ NSInteger level = [[markdownType substringFromIndex:8] integerValue];
322
+ if (level >= 1 && level <= 6) {
323
+ return ParagraphTypeHeading1 + (level - 1);
324
+ }
325
+ }
326
+ if ([markdownType isEqualToString:@"code-block"])
327
+ return ParagraphTypeCodeBlock;
328
+ }
329
+
330
+ NSNumber *blockquoteDepth = attrs[BlockquoteDepthAttributeName];
331
+ if (blockquoteDepth && [blockquoteDepth integerValue] >= 0)
332
+ return ParagraphTypeBlockquote;
333
+
334
+ NSNumber *listDepth = attrs[ListDepthAttribute];
335
+ if (listDepth && [listDepth integerValue] >= 0) {
336
+ NSNumber *listType = attrs[ListTypeAttribute];
337
+ if (listType && [listType integerValue] == ListTypeOrdered)
338
+ return ParagraphTypeListItemOrdered;
339
+ return ParagraphTypeListItemUnordered;
340
+ }
341
+
342
+ return ParagraphTypeNormal;
343
+ }
344
+
345
+ #pragma mark - Paragraph Collection
346
+
347
+ static NSData *collectParagraphsData(NSAttributedString *attributedString, NSUInteger *outCount)
348
+ {
349
+ NSString *string = attributedString.string;
350
+ NSMutableData *data = [NSMutableData dataWithCapacity:string.length / 20 * sizeof(ParagraphData)];
351
+ NSUInteger currentIndex = 0;
352
+ NSUInteger count = 0;
353
+
354
+ while (currentIndex < string.length) {
355
+ NSRange lineRange = [string lineRangeForRange:NSMakeRange(currentIndex, 0)];
356
+
357
+ ParagraphData para = {.range = lineRange, .type = ParagraphTypeNormal, .depth = 0, .listNumber = 1};
358
+
359
+ if (lineRange.location < attributedString.length) {
360
+ NSDictionary *attrs = [attributedString attributesAtIndex:lineRange.location effectiveRange:NULL];
361
+ para.type = getParagraphType(attrs);
362
+
363
+ NSNumber *listDepth = attrs[ListDepthAttribute];
364
+ NSNumber *blockquoteDepth = attrs[BlockquoteDepthAttributeName];
365
+ NSNumber *listNumber = attrs[ListItemNumberAttribute];
366
+
367
+ para.depth = listDepth ? [listDepth integerValue] : (blockquoteDepth ? [blockquoteDepth integerValue] : 0);
368
+ para.listNumber = listNumber ? [listNumber integerValue] : 1;
369
+ }
370
+
371
+ [data appendBytes:&para length:sizeof(ParagraphData)];
372
+ count++;
373
+ currentIndex = NSMaxRange(lineRange);
374
+ }
375
+
376
+ *outCount = count;
377
+ return data;
378
+ }
379
+
380
+ #pragma mark - Heading Helpers
381
+
382
+ static NSInteger headingLevel(ParagraphType type)
383
+ {
384
+ if (type >= ParagraphTypeHeading1 && type <= ParagraphTypeHeading6) {
385
+ return type - ParagraphTypeHeading1 + 1;
386
+ }
387
+ return 0;
388
+ }
389
+
390
+ #pragma mark - Container Closing Helpers
391
+
392
+ static void closeBlockquotes(NSMutableString *html, GeneratorState *state)
393
+ {
394
+ while (state.currentBlockquoteDepth >= 0) {
395
+ [html appendString:@"</blockquote>"];
396
+ state.currentBlockquoteDepth--;
397
+ }
398
+ state.inBlockquote = NO;
399
+ }
400
+
401
+ static void closeLists(NSMutableString *html, GeneratorState *state)
402
+ {
403
+ while (state.openListTypes.count > 0) {
404
+ NSString *closeTag = ([state.openListTypes.lastObject integerValue] == 1) ? @"</ol>" : @"</ul>";
405
+ [html appendString:closeTag];
406
+ [state.openListTypes removeLastObject];
407
+ }
408
+ state.currentListDepth = -1;
409
+ }
410
+
411
+ static void closeCodeBlock(NSMutableString *html, GeneratorState *state)
412
+ {
413
+ if (state.inCodeBlock) {
414
+ [html appendString:@"</code></pre>"];
415
+ state.inCodeBlock = NO;
416
+ }
417
+ }
418
+
419
+ #pragma mark - Inline Content Generation
420
+
421
+ static void generateInlineHTML(NSMutableString *html, NSAttributedString *attributedString, NSRange range,
422
+ CachedStyles *styles, BOOL isCodeBlock)
423
+ {
424
+ NSString *string = attributedString.string;
425
+
426
+ [attributedString
427
+ enumerateAttributesInRange:range
428
+ options:0
429
+ usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange attrRange, BOOL *stop) {
430
+ NSString *text = [string substringWithRange:attrRange];
431
+
432
+ if ([text isEqualToString:@"\n"])
433
+ return;
434
+
435
+ if ([text containsString:kObjectReplacementChar]) {
436
+ id attachment = attrs[NSAttachmentAttributeName];
437
+ if ([attachment isKindOfClass:[EnrichedMarkdownImageAttachment class]]) {
438
+ EnrichedMarkdownImageAttachment *img = (EnrichedMarkdownImageAttachment *)attachment;
439
+ if (img.imageURL) {
440
+ if (img.isInline) {
441
+ [html appendFormat:
442
+ @"<img src=\"%@\" style=\"height: 1.2em; width: auto; "
443
+ @"vertical-align: -0.2em;\">",
444
+ img.imageURL];
445
+ } else {
446
+ [html appendFormat:
447
+ @"</p><div style=\"margin-bottom: %.0fpx;\"><img src=\"%@\" "
448
+ @"style=\"max-width: 100%%; border-radius: %.0fpx;\"></div><p>",
449
+ styles.imageMarginBottom, img.imageURL, styles.imageBorderRadius];
450
+ }
451
+ }
452
+ } else if ([attachment isKindOfClass:[ThematicBreakAttachment class]]) {
453
+ [html appendFormat:
454
+ @"</p><hr style=\"border: none; border-top: %.0fpx solid %@; "
455
+ @"margin: %.0fpx 0 %.0fpx 0;\"><p>",
456
+ styles.thematicBreakHeight, styles.thematicBreakColor,
457
+ styles.thematicBreakMarginTop, styles.thematicBreakMarginBottom];
458
+ }
459
+ return;
460
+ }
461
+
462
+ UIFont *font = attrs[NSFontAttributeName];
463
+ NSNumber *underline = attrs[NSUnderlineStyleAttributeName];
464
+ NSNumber *strikethrough = attrs[NSStrikethroughStyleAttributeName];
465
+ id linkAttr = attrs[NSLinkAttributeName];
466
+ BOOL isCode = isCodeSpan(attrs, isCodeBlock);
467
+
468
+ BOOL isBold = NO, isItalic = NO;
469
+ if (font) {
470
+ UIFontDescriptorSymbolicTraits traits = font.fontDescriptor.symbolicTraits;
471
+ isBold = (traits & UIFontDescriptorTraitBold) != 0;
472
+ isItalic = (traits & UIFontDescriptorTraitItalic) != 0;
473
+ }
474
+
475
+ if (linkAttr) {
476
+ NSString *href =
477
+ [linkAttr isKindOfClass:[NSURL class]] ? [(NSURL *)linkAttr absoluteString] : linkAttr;
478
+ [html appendFormat:@"<a href=\"%@\" style=\"color: %@; text-decoration: %@;\">",
479
+ escapeHTML(href), styles.linkColor,
480
+ styles.linkUnderline ? @"underline" : @"none"];
481
+ }
482
+
483
+ if (isCode) {
484
+ [html appendFormat:
485
+ @"<code style=\"background-color: %@; color: %@; "
486
+ @"padding: %.0fpx %.0fpx; border-radius: %.0fpx; "
487
+ @"font-size: 0.7em; font-family: Menlo, Monaco, Consolas, monospace;\">",
488
+ styles.codeBackgroundColor, styles.codeColor, kCodePadding, kCodePadding * 2,
489
+ kCodeBorderRadius];
490
+ }
491
+
492
+ if (isBold) {
493
+ if (styles.strongColor && ![styles.strongColor isEqualToString:@"inherit"]) {
494
+ [html appendFormat:@"<strong style=\"color: %@;\">", styles.strongColor];
495
+ } else {
496
+ [html appendString:@"<strong>"];
497
+ }
498
+ }
499
+
500
+ if (isItalic) {
501
+ if (styles.emphasisColor && ![styles.emphasisColor isEqualToString:@"inherit"]) {
502
+ [html appendFormat:@"<em style=\"color: %@;\">", styles.emphasisColor];
503
+ } else {
504
+ [html appendString:@"<em>"];
505
+ }
506
+ }
507
+
508
+ if ([strikethrough integerValue] > 0) {
509
+ if (styles.strikethroughColor && ![styles.strikethroughColor isEqualToString:@"inherit"]) {
510
+ [html appendFormat:@"<s style=\"text-decoration-color: %@;\">", styles.strikethroughColor];
511
+ } else {
512
+ [html appendString:@"<s>"];
513
+ }
514
+ }
515
+ if ([underline integerValue] > 0 && !linkAttr) {
516
+ if (styles.underlineColor && ![styles.underlineColor isEqualToString:@"inherit"]) {
517
+ [html appendFormat:@"<u style=\"text-decoration-color: %@;\">", styles.underlineColor];
518
+ } else {
519
+ [html appendString:@"<u>"];
520
+ }
521
+ }
522
+
523
+ [html appendString:escapeHTML(text)];
524
+
525
+ // Reverse order
526
+ if ([underline integerValue] > 0 && !linkAttr)
527
+ [html appendString:@"</u>"];
528
+ if ([strikethrough integerValue] > 0)
529
+ [html appendString:@"</s>"];
530
+ if (isItalic)
531
+ [html appendString:@"</em>"];
532
+ if (isBold)
533
+ [html appendString:@"</strong>"];
534
+ if (isCode)
535
+ [html appendString:@"</code>"];
536
+ if (linkAttr)
537
+ [html appendString:@"</a>"];
538
+ }];
539
+ }
540
+
541
+ #pragma mark - Block Handlers
542
+
543
+ static void handleCodeBlock(NSMutableString *html, NSMutableString *inlineContent, CachedStyles *styles,
544
+ GeneratorState *state)
545
+ {
546
+ if (!state.inCodeBlock) {
547
+ state.inCodeBlock = YES;
548
+ [html appendFormat:
549
+ @"<pre style=\"background-color: %@; padding: %.0fpx; border-radius: %.0fpx; "
550
+ @"margin: 0 0 %.0fpx 0; overflow-x: auto;\"><code style=\"font-family: Menlo, Monaco, "
551
+ @"Consolas, monospace; font-size: %.0fpx; color: %@;\">",
552
+ styles.codeBlockBackgroundColor, styles.codeBlockPadding, styles.codeBlockBorderRadius,
553
+ styles.codeBlockMarginBottom, styles.codeBlockFontSize, styles.codeBlockColor];
554
+ } else if (state.previousWasCodeBlock) {
555
+ [html appendString:@"\n"];
556
+ }
557
+
558
+ [html appendString:inlineContent];
559
+ state.previousWasCodeBlock = YES;
560
+ }
561
+
562
+ static void handleBlockquote(NSMutableString *html, ParagraphData *para, NSMutableString *inlineContent,
563
+ CachedStyles *styles, GeneratorState *state)
564
+ {
565
+ NSInteger depth = para->depth;
566
+
567
+ // Reset if starting a new blockquote block
568
+ if (!state.previousWasBlockquote && state.inBlockquote) {
569
+ closeBlockquotes(html, state);
570
+ }
571
+
572
+ while (state.currentBlockquoteDepth > depth) {
573
+ [html appendString:@"</blockquote>"];
574
+ state.currentBlockquoteDepth--;
575
+ }
576
+
577
+ while (state.currentBlockquoteDepth < depth) {
578
+ state.currentBlockquoteDepth++;
579
+ state.inBlockquote = YES;
580
+
581
+ if (state.currentBlockquoteDepth == 0) {
582
+ [html appendFormat:
583
+ @"<blockquote style=\"background-color: %@; border-left: %.0fpx solid %@; "
584
+ @"padding: %.0fpx %.0fpx; margin: 0 0 %.0fpx 0; border-radius: 0 8px 8px 0;\">",
585
+ styles.blockquoteBackgroundColor, styles.blockquoteBorderWidth, styles.blockquoteBorderColor,
586
+ kBlockquoteVerticalPadding, styles.blockquoteGapWidth, styles.blockquoteMarginBottom];
587
+ } else {
588
+ [html appendFormat:
589
+ @"<blockquote style=\"border-left: %.0fpx solid %@; padding-left: %.0fpx; "
590
+ @"margin: %.0fpx 0 0 0;\">",
591
+ styles.blockquoteBorderWidth, styles.blockquoteBorderColor, styles.blockquoteGapWidth,
592
+ kNestedBlockquoteTopMargin];
593
+ }
594
+ }
595
+
596
+ [html appendFormat:@"<p style=\"margin: 0 0 %.0fpx 0; color: %@; font-size: %.0fpx;\">%@</p>",
597
+ kBlockquoteParagraphSpacing, styles.blockquoteColor, styles.blockquoteFontSize, inlineContent];
598
+
599
+ state.previousWasBlockquote = YES;
600
+ }
601
+
602
+ static void handleListItem(NSMutableString *html, ParagraphData *para, NSMutableString *inlineContent,
603
+ CachedStyles *styles, GeneratorState *state)
604
+ {
605
+ NSInteger depth = para->depth;
606
+ BOOL isOrdered = (para->type == ParagraphTypeListItemOrdered);
607
+ NSInteger listTypeValue = isOrdered ? 1 : 0;
608
+
609
+ while (state.currentListDepth > depth) {
610
+ NSString *closeTag = ([state.openListTypes.lastObject integerValue] == 1) ? @"</ol>" : @"</ul>";
611
+ [html appendString:closeTag];
612
+ [state.openListTypes removeLastObject];
613
+ state.currentListDepth--;
614
+ }
615
+
616
+ // List type change at same depth (ul <-> ol)
617
+ if (state.currentListDepth == depth && state.openListTypes.count > 0) {
618
+ NSInteger currentType = [state.openListTypes.lastObject integerValue];
619
+ if (currentType != listTypeValue) {
620
+ NSString *closeTag = (currentType == 1) ? @"</ol>" : @"</ul>";
621
+ [html appendString:closeTag];
622
+ [state.openListTypes removeLastObject];
623
+ state.currentListDepth--;
624
+ }
625
+ }
626
+
627
+ CGFloat indent = styles.listStyleMarginLeft > 0 ? styles.listStyleMarginLeft : kDefaultListIndent;
628
+
629
+ while (state.currentListDepth < depth) {
630
+ state.currentListDepth++;
631
+ if (isOrdered) {
632
+ [html appendFormat:@"<ol style=\"margin: 0; padding-left: %.0fpx;\">", indent];
633
+ } else {
634
+ [html appendFormat:@"<ul style=\"margin: 0; padding-left: %.0fpx; list-style-type: disc;\">", indent];
635
+ }
636
+ [state.openListTypes addObject:@(listTypeValue)];
637
+ }
638
+
639
+ [html appendFormat:@"<li style=\"margin-bottom: %.0fpx; color: %@; font-size: %.0fpx;\">%@</li>",
640
+ styles.listStyleMarginBottom, styles.listStyleColor, styles.listStyleFontSize, inlineContent];
641
+ }
642
+
643
+ static void handleHeading(NSMutableString *html, ParagraphData *para, NSMutableString *inlineContent,
644
+ CachedStyles *styles)
645
+ {
646
+ NSInteger level = headingLevel(para->type);
647
+ CGFloat fontSize, marginBottom;
648
+ NSString *fontWeight, *color;
649
+
650
+ switch (level) {
651
+ case 1:
652
+ fontSize = styles.h1FontSize;
653
+ fontWeight = styles.h1FontWeight;
654
+ color = styles.h1Color;
655
+ marginBottom = styles.h1MarginBottom;
656
+ break;
657
+ case 2:
658
+ fontSize = styles.h2FontSize;
659
+ fontWeight = styles.h2FontWeight;
660
+ color = styles.h2Color;
661
+ marginBottom = styles.h2MarginBottom;
662
+ break;
663
+ case 3:
664
+ fontSize = styles.h3FontSize;
665
+ fontWeight = styles.h3FontWeight;
666
+ color = styles.h3Color;
667
+ marginBottom = styles.h3MarginBottom;
668
+ break;
669
+ case 4:
670
+ fontSize = styles.h4FontSize;
671
+ fontWeight = styles.h4FontWeight;
672
+ color = styles.h4Color;
673
+ marginBottom = styles.h4MarginBottom;
674
+ break;
675
+ case 5:
676
+ fontSize = styles.h5FontSize;
677
+ fontWeight = styles.h5FontWeight;
678
+ color = styles.h5Color;
679
+ marginBottom = styles.h5MarginBottom;
680
+ break;
681
+ case 6:
682
+ fontSize = styles.h6FontSize;
683
+ fontWeight = styles.h6FontWeight;
684
+ color = styles.h6Color;
685
+ marginBottom = styles.h6MarginBottom;
686
+ break;
687
+ default:
688
+ fontSize = 16;
689
+ fontWeight = @"normal";
690
+ color = @"inherit";
691
+ marginBottom = 0;
692
+ break;
693
+ }
694
+
695
+ [html appendFormat:@"<h%ld style=\"font-size: %.0fpx; font-weight: %@; color: %@; margin: 0 0 %.0fpx 0;\">%@</h%ld>",
696
+ (long)level, fontSize, fontWeight, color, marginBottom, inlineContent, (long)level];
697
+ }
698
+
699
+ static void handleParagraph(NSMutableString *html, NSMutableString *inlineContent, CachedStyles *styles)
700
+ {
701
+ [html appendFormat:@"<p style=\"margin: 0 0 %.0fpx 0; color: %@; font-size: %.0fpx;\">%@</p>",
702
+ styles.paragraphMarginBottom, styles.paragraphColor, styles.paragraphFontSize, inlineContent];
703
+ }
704
+
705
+ #pragma mark - Main Generator
706
+
707
+ NSString *_Nullable generateHTML(NSAttributedString *attributedString, StyleConfig *styleConfig)
708
+ {
709
+ if (!attributedString || attributedString.length == 0)
710
+ return nil;
711
+
712
+ CachedStyles *styles = cacheStyles(styleConfig);
713
+
714
+ NSMutableString *html = [NSMutableString stringWithCapacity:attributedString.length * 2];
715
+ [html appendString:
716
+ @"<!DOCTYPE html><html><head><meta charset=\"UTF-8\"></head><body "
717
+ @"style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\">"];
718
+
719
+ NSUInteger paragraphCount = 0;
720
+ NSData *paragraphsData = collectParagraphsData(attributedString, &paragraphCount);
721
+ ParagraphData *paragraphs = (ParagraphData *)paragraphsData.bytes;
722
+
723
+ GeneratorState *state = [[GeneratorState alloc] init];
724
+ NSMutableString *inlineBuffer = [NSMutableString stringWithCapacity:256];
725
+
726
+ for (NSUInteger i = 0; i < paragraphCount; i++) {
727
+ ParagraphData *para = &paragraphs[i];
728
+
729
+ NSString *content = [[attributedString.string substringWithRange:para->range]
730
+ stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
731
+
732
+ if (content.length == 0) {
733
+ state.previousWasBlockquote = NO;
734
+ continue;
735
+ }
736
+
737
+ [inlineBuffer setString:@""];
738
+ NSRange contentRange = para->range;
739
+ if (contentRange.length > 0 &&
740
+ [[attributedString.string substringWithRange:NSMakeRange(NSMaxRange(contentRange) - 1, 1)]
741
+ isEqualToString:@"\n"]) {
742
+ contentRange.length--;
743
+ }
744
+
745
+ BOOL isCodeBlockPara = (para->type == ParagraphTypeCodeBlock);
746
+ generateInlineHTML(inlineBuffer, attributedString, contentRange, styles, isCodeBlockPara);
747
+
748
+ if (isCodeBlockPara) {
749
+ handleCodeBlock(html, inlineBuffer, styles, state);
750
+ continue;
751
+ }
752
+
753
+ if (state.inCodeBlock) {
754
+ closeCodeBlock(html, state);
755
+ }
756
+ state.previousWasCodeBlock = NO;
757
+
758
+ if (para->type == ParagraphTypeBlockquote) {
759
+ handleBlockquote(html, para, inlineBuffer, styles, state);
760
+ continue;
761
+ }
762
+
763
+ if (state.inBlockquote) {
764
+ closeBlockquotes(html, state);
765
+ }
766
+ state.previousWasBlockquote = NO;
767
+
768
+ if (para->type == ParagraphTypeListItemUnordered || para->type == ParagraphTypeListItemOrdered) {
769
+ handleListItem(html, para, inlineBuffer, styles, state);
770
+ continue;
771
+ }
772
+
773
+ if (state.currentListDepth >= 0) {
774
+ closeLists(html, state);
775
+ }
776
+
777
+ NSInteger hLevel = headingLevel(para->type);
778
+ if (hLevel > 0) {
779
+ handleHeading(html, para, inlineBuffer, styles);
780
+ continue;
781
+ }
782
+
783
+ handleParagraph(html, inlineBuffer, styles);
784
+ }
785
+
786
+ closeCodeBlock(html, state);
787
+ closeBlockquotes(html, state);
788
+ closeLists(html, state);
789
+
790
+ [html appendString:@"</body></html>"];
791
+
792
+ return html;
793
+ }