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,308 @@
1
+ #import "MarkdownExtractor.h"
2
+ #import "BlockquoteBorder.h"
3
+ #import "EnrichedMarkdownImageAttachment.h"
4
+ #import "LastElementUtils.h"
5
+ #import "RuntimeKeys.h"
6
+ #import "ThematicBreakAttachment.h"
7
+
8
+ #pragma mark - Extraction Context
9
+
10
+ typedef struct {
11
+ NSInteger blockquoteDepth; // -1 = not in blockquote
12
+ NSInteger listDepth; // -1 = not in list
13
+ BOOL needsBlankLine;
14
+ } ExtractionState;
15
+
16
+ #pragma mark - Helper Functions
17
+
18
+ static void ensureBlankLine(NSMutableString *result)
19
+ {
20
+ if (result.length == 0)
21
+ return;
22
+ if ([result hasSuffix:@"\n\n"])
23
+ return;
24
+
25
+ [result appendString:[result hasSuffix:@"\n"] ? @"\n" : @"\n\n"];
26
+ }
27
+
28
+ static BOOL isAtLineStart(NSMutableString *result)
29
+ {
30
+ return result.length == 0 || [result hasSuffix:@"\n"];
31
+ }
32
+
33
+ /// Depth 0 = "> ", Depth 1 = "> > ", etc.
34
+ static NSString *buildBlockquotePrefix(NSInteger depth)
35
+ {
36
+ NSMutableString *prefix = [NSMutableString string];
37
+ for (NSInteger i = 0; i <= depth; i++) {
38
+ [prefix appendString:@"> "];
39
+ }
40
+ return prefix;
41
+ }
42
+
43
+ static NSString *buildListPrefix(NSInteger depth, BOOL isOrdered, NSInteger itemNumber)
44
+ {
45
+ NSString *indent = [@"" stringByPaddingToLength:(depth * 2) withString:@" " startingAtIndex:0];
46
+ NSString *marker = isOrdered ? [NSString stringWithFormat:@"%ld.", (long)itemNumber] : @"-";
47
+ return [NSString stringWithFormat:@"%@%@ ", indent, marker];
48
+ }
49
+
50
+ static NSString *buildHeadingPrefix(NSInteger level)
51
+ {
52
+ return [NSString stringWithFormat:@"%@ ", [@"" stringByPaddingToLength:level withString:@"#" startingAtIndex:0]];
53
+ }
54
+
55
+ static void extractFontTraits(NSDictionary *attrs, BOOL *isBold, BOOL *isItalic, BOOL *isMonospace)
56
+ {
57
+ UIFont *font = attrs[NSFontAttributeName];
58
+ *isBold = NO;
59
+ *isItalic = NO;
60
+ *isMonospace = NO;
61
+
62
+ if (font) {
63
+ UIFontDescriptorSymbolicTraits traits = font.fontDescriptor.symbolicTraits;
64
+ *isBold = (traits & UIFontDescriptorTraitBold) != 0;
65
+ *isItalic = (traits & UIFontDescriptorTraitItalic) != 0;
66
+ *isMonospace = (traits & UIFontDescriptorTraitMonoSpace) != 0;
67
+ }
68
+ }
69
+
70
+ static NSString *applyInlineFormatting(NSString *text, BOOL isBold, BOOL isItalic, BOOL isMonospace,
71
+ BOOL isStrikethrough, BOOL isUnderline, NSString *linkURL)
72
+ {
73
+ NSMutableString *result = [NSMutableString stringWithString:text];
74
+
75
+ // Innermost first
76
+ if (isMonospace && !linkURL) {
77
+ result = [NSMutableString stringWithFormat:@"`%@`", result];
78
+ }
79
+ if (isStrikethrough) {
80
+ result = [NSMutableString stringWithFormat:@"~~%@~~", result];
81
+ }
82
+ if (isUnderline && !linkURL) {
83
+ result = [NSMutableString stringWithFormat:@"<u>%@</u>", result];
84
+ }
85
+ if (isItalic) {
86
+ result = [NSMutableString stringWithFormat:@"*%@*", result];
87
+ }
88
+ if (isBold) {
89
+ result = [NSMutableString stringWithFormat:@"**%@**", result];
90
+ }
91
+ if (linkURL) {
92
+ result = [NSMutableString stringWithFormat:@"[%@](%@)", text, linkURL];
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ #pragma mark - Main Extraction Function
99
+
100
+ NSString *_Nullable extractMarkdownFromAttributedString(NSAttributedString *attributedText, NSRange range)
101
+ {
102
+ if (!attributedText || range.length == 0 || range.location >= attributedText.length) {
103
+ return nil;
104
+ }
105
+
106
+ range.length = MIN(range.length, attributedText.length - range.location);
107
+
108
+ NSMutableString *result = [NSMutableString string];
109
+
110
+ // Headings may span multiple attribute runs
111
+ __block NSString *currentHeadingType = nil;
112
+ __block NSMutableString *headingContent = nil;
113
+ __block ExtractionState state = {.blockquoteDepth = -1, .listDepth = -1, .needsBlankLine = NO};
114
+
115
+ void (^flushHeading)(void) = ^{
116
+ if (!currentHeadingType || headingContent.length == 0)
117
+ return;
118
+
119
+ ensureBlankLine(result);
120
+ NSInteger level = [[currentHeadingType substringFromIndex:8] integerValue];
121
+ [result appendFormat:@"%@%@\n", buildHeadingPrefix(level), headingContent];
122
+
123
+ currentHeadingType = nil;
124
+ headingContent = nil;
125
+ state.needsBlankLine = YES;
126
+ };
127
+
128
+ [attributedText
129
+ enumerateAttributesInRange:range
130
+ options:0
131
+ usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange attrRange, BOOL *stop) {
132
+ NSString *text = [[attributedText attributedSubstringFromRange:attrRange] string];
133
+ if (text.length == 0)
134
+ return;
135
+
136
+ // Images and Thematic Breaks
137
+ NSTextAttachment *attachment = attrs[NSAttachmentAttributeName];
138
+ if ([attachment isKindOfClass:[EnrichedMarkdownImageAttachment class]]) {
139
+ EnrichedMarkdownImageAttachment *img = (EnrichedMarkdownImageAttachment *)attachment;
140
+ if (!img.imageURL)
141
+ return;
142
+
143
+ if (img.isInline) {
144
+ [result appendFormat:@"![image](%@)", img.imageURL];
145
+ } else {
146
+ ensureBlankLine(result);
147
+ [result appendFormat:@"![image](%@)\n", img.imageURL];
148
+ state.needsBlankLine = YES;
149
+ state.blockquoteDepth = -1;
150
+ state.listDepth = -1;
151
+ }
152
+ return;
153
+ }
154
+
155
+ if ([attachment isKindOfClass:[ThematicBreakAttachment class]]) {
156
+ ensureBlankLine(result);
157
+ [result appendString:@"---\n"];
158
+ state.needsBlankLine = YES;
159
+ state.blockquoteDepth = -1;
160
+ state.listDepth = -1;
161
+ return;
162
+ }
163
+
164
+ if ([text isEqualToString:@"\uFFFC"])
165
+ return;
166
+
167
+ // Newlines
168
+ if ([text isEqualToString:@"\n"] || [text isEqualToString:@"\n\n"]) {
169
+ NSNumber *bqDepth = attrs[BlockquoteDepthAttributeName];
170
+ NSNumber *listDepth = attrs[@"ListDepth"];
171
+ BOOL inBlockquote = (bqDepth != nil);
172
+ BOOL inList = (listDepth != nil);
173
+
174
+ if (!inBlockquote && state.blockquoteDepth >= 0) {
175
+ ensureBlankLine(result);
176
+ state.blockquoteDepth = -1;
177
+ return;
178
+ }
179
+
180
+ if (!inList && state.listDepth >= 0) {
181
+ ensureBlankLine(result);
182
+ state.listDepth = -1;
183
+ return;
184
+ }
185
+
186
+ if (inBlockquote || inList) {
187
+ if (![result hasSuffix:@"\n"]) {
188
+ [result appendString:@"\n"];
189
+ }
190
+ return;
191
+ }
192
+
193
+ ensureBlankLine(result);
194
+ return;
195
+ }
196
+
197
+ // Headings
198
+ NSString *markdownType = attrs[MarkdownTypeAttributeName];
199
+
200
+ if (markdownType && [markdownType hasPrefix:@"heading-"]) {
201
+ if (![markdownType isEqualToString:currentHeadingType]) {
202
+ flushHeading();
203
+ currentHeadingType = markdownType;
204
+ headingContent = [NSMutableString string];
205
+ }
206
+ [headingContent
207
+ appendString:[text stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]];
208
+ return;
209
+ } else if (currentHeadingType) {
210
+ flushHeading();
211
+ }
212
+
213
+ // Code blocks
214
+ NSNumber *isCodeBlock = attrs[CodeBlockAttributeName];
215
+ if ([isCodeBlock boolValue]) {
216
+ if (state.needsBlankLine) {
217
+ ensureBlankLine(result);
218
+ state.needsBlankLine = NO;
219
+ }
220
+
221
+ BOOL needsFence = (result.length == 0) || [result hasSuffix:@"\n\n"];
222
+ if (needsFence) {
223
+ [result appendString:@"```\n"];
224
+ }
225
+
226
+ [result appendString:text];
227
+
228
+ if ([text hasSuffix:@"\n"]) {
229
+ [result appendString:@"```\n"];
230
+ state.needsBlankLine = YES;
231
+ }
232
+ return;
233
+ }
234
+
235
+ // Blockquotes
236
+ NSNumber *bqDepthNum = attrs[BlockquoteDepthAttributeName];
237
+ NSInteger currentBqDepth = bqDepthNum ? [bqDepthNum integerValue] : -1;
238
+ NSString *blockquotePrefix = nil;
239
+
240
+ if (currentBqDepth >= 0) {
241
+ blockquotePrefix = buildBlockquotePrefix(currentBqDepth);
242
+ state.blockquoteDepth = currentBqDepth;
243
+ } else if (state.blockquoteDepth >= 0) {
244
+ ensureBlankLine(result);
245
+ state.blockquoteDepth = -1;
246
+ }
247
+
248
+ // Lists
249
+ NSNumber *listDepthNum = attrs[@"ListDepth"];
250
+ NSNumber *listTypeNum = attrs[@"ListType"];
251
+ NSNumber *listItemNum = attrs[@"ListItemNumber"];
252
+ NSInteger currentListDepth = listDepthNum ? [listDepthNum integerValue] : -1;
253
+
254
+ if (currentListDepth >= 0) {
255
+ state.listDepth = currentListDepth;
256
+ } else if (state.listDepth >= 0) {
257
+ ensureBlankLine(result);
258
+ state.listDepth = -1;
259
+ }
260
+
261
+ // Inline formatting
262
+ BOOL isBold, isItalic, isMonospace;
263
+ extractFontTraits(attrs, &isBold, &isItalic, &isMonospace);
264
+
265
+ NSNumber *strikethroughStyle = attrs[NSStrikethroughStyleAttributeName];
266
+ BOOL isStrikethrough = (strikethroughStyle != nil && [strikethroughStyle integerValue] != 0);
267
+ NSNumber *underlineStyle = attrs[NSUnderlineStyleAttributeName];
268
+ BOOL isUnderline = (underlineStyle != nil && [underlineStyle integerValue] != 0);
269
+
270
+ NSString *linkURL = attrs[NSLinkAttributeName];
271
+ NSString *segment = applyInlineFormatting(text, isBold, isItalic, isMonospace, isStrikethrough,
272
+ isUnderline, linkURL);
273
+
274
+ // Add block prefixes at line start
275
+ if (isAtLineStart(result)) {
276
+ NSMutableString *prefixedSegment = [NSMutableString string];
277
+
278
+ if (listDepthNum && ![text hasPrefix:@"\n"]) {
279
+ BOOL isOrdered = ([listTypeNum integerValue] == 1);
280
+ NSInteger itemNumber = listItemNum ? [listItemNum integerValue] : 1;
281
+ [prefixedSegment appendString:buildListPrefix(currentListDepth, isOrdered, itemNumber)];
282
+ }
283
+
284
+ if (blockquotePrefix) {
285
+ [prefixedSegment insertString:blockquotePrefix atIndex:0];
286
+ }
287
+
288
+ [prefixedSegment appendString:segment];
289
+ segment = prefixedSegment;
290
+ }
291
+
292
+ if (state.needsBlankLine && result.length > 0) {
293
+ ensureBlankLine(result);
294
+ state.needsBlankLine = NO;
295
+ }
296
+
297
+ [result appendString:segment];
298
+ }];
299
+
300
+ // Flush remaining heading
301
+ if (currentHeadingType && headingContent.length > 0) {
302
+ ensureBlankLine(result);
303
+ NSInteger level = [[currentHeadingType substringFromIndex:8] integerValue];
304
+ [result appendFormat:@"%@%@\n", buildHeadingPrefix(level), headingContent];
305
+ }
306
+
307
+ return result.length > 0 ? result : nil;
308
+ }
@@ -0,0 +1,21 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ __BEGIN_DECLS
7
+
8
+ extern NSAttributedString *kNewlineAttributedString;
9
+
10
+ NSMutableParagraphStyle *getOrCreateParagraphStyle(NSMutableAttributedString *output, NSUInteger index);
11
+ void applyParagraphSpacingAfter(NSMutableAttributedString *output, NSUInteger start, CGFloat marginBottom);
12
+ void applyParagraphSpacingBefore(NSMutableAttributedString *output, NSRange range, CGFloat marginTop);
13
+ NSUInteger applyBlockSpacingBefore(NSMutableAttributedString *output, NSUInteger insertionPoint, CGFloat marginTop);
14
+ void applyBlockSpacingAfter(NSMutableAttributedString *output, CGFloat marginBottom);
15
+ void applyLineHeight(NSMutableAttributedString *output, NSRange range, CGFloat lineHeight);
16
+ void applyTextAlignment(NSMutableAttributedString *output, NSRange range, NSTextAlignment textAlign);
17
+ NSTextAlignment textAlignmentFromString(NSString *textAlign);
18
+
19
+ __END_DECLS
20
+
21
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,111 @@
1
+ #import "ParagraphStyleUtils.h"
2
+
3
+ NSAttributedString *kNewlineAttributedString;
4
+ static NSParagraphStyle *kBlockSpacerTemplate;
5
+
6
+ __attribute__((constructor)) static void initParagraphStyleUtils(void)
7
+ {
8
+ kNewlineAttributedString = [[NSAttributedString alloc] initWithString:@"\n"];
9
+
10
+ NSMutableParagraphStyle *template = [[NSMutableParagraphStyle alloc] init];
11
+ template.minimumLineHeight = 1;
12
+ template.maximumLineHeight = 1;
13
+ kBlockSpacerTemplate = [template copy];
14
+ }
15
+
16
+ NSMutableParagraphStyle *getOrCreateParagraphStyle(NSMutableAttributedString *output, NSUInteger index)
17
+ {
18
+ NSParagraphStyle *existing = [output attribute:NSParagraphStyleAttributeName atIndex:index effectiveRange:NULL];
19
+ return existing ? [existing mutableCopy] : [[NSMutableParagraphStyle alloc] init];
20
+ }
21
+
22
+ void applyParagraphSpacingAfter(NSMutableAttributedString *output, NSUInteger start, CGFloat marginBottom)
23
+ {
24
+ [output appendAttributedString:kNewlineAttributedString];
25
+
26
+ NSMutableParagraphStyle *style = getOrCreateParagraphStyle(output, start);
27
+ style.paragraphSpacing = marginBottom;
28
+
29
+ NSRange range = NSMakeRange(start, output.length - start);
30
+ [output addAttribute:NSParagraphStyleAttributeName value:style range:range];
31
+ }
32
+
33
+ void applyParagraphSpacingBefore(NSMutableAttributedString *output, NSRange range, CGFloat marginTop)
34
+ {
35
+ if (marginTop <= 0) {
36
+ return;
37
+ }
38
+
39
+ // Note: paragraphSpacingBefore does not work at index 0 in TextKit.
40
+ // Leading spacing for the first element is handled by renderers via applyBlockSpacingBefore.
41
+ NSMutableParagraphStyle *style = getOrCreateParagraphStyle(output, range.location);
42
+ style.paragraphSpacingBefore = marginTop;
43
+ [output addAttribute:NSParagraphStyleAttributeName value:style range:range];
44
+ }
45
+
46
+ NSUInteger applyBlockSpacingBefore(NSMutableAttributedString *output, NSUInteger insertionPoint, CGFloat marginTop)
47
+ {
48
+ if (marginTop <= 0) {
49
+ return 0;
50
+ }
51
+
52
+ NSMutableParagraphStyle *spacerStyle = [kBlockSpacerTemplate mutableCopy];
53
+ spacerStyle.paragraphSpacing = marginTop;
54
+
55
+ NSAttributedString *spacer =
56
+ [[NSAttributedString alloc] initWithString:@"\n" attributes:@{NSParagraphStyleAttributeName : spacerStyle}];
57
+
58
+ [output insertAttributedString:spacer atIndex:insertionPoint];
59
+ return 1;
60
+ }
61
+
62
+ void applyBlockSpacingAfter(NSMutableAttributedString *output, CGFloat marginBottom)
63
+ {
64
+ if (marginBottom <= 0) {
65
+ return;
66
+ }
67
+
68
+ NSUInteger spacerLocation = output.length;
69
+ [output appendAttributedString:kNewlineAttributedString];
70
+
71
+ NSMutableParagraphStyle *spacerStyle = [kBlockSpacerTemplate mutableCopy];
72
+ spacerStyle.paragraphSpacing = marginBottom;
73
+
74
+ [output addAttribute:NSParagraphStyleAttributeName value:spacerStyle range:NSMakeRange(spacerLocation, 1)];
75
+ }
76
+
77
+ void applyLineHeight(NSMutableAttributedString *output, NSRange range, CGFloat lineHeight)
78
+ {
79
+ if (lineHeight <= 0) {
80
+ return;
81
+ }
82
+
83
+ NSMutableParagraphStyle *style = getOrCreateParagraphStyle(output, range.location);
84
+
85
+ style.minimumLineHeight = lineHeight;
86
+ style.maximumLineHeight = lineHeight;
87
+
88
+ [output addAttribute:NSParagraphStyleAttributeName value:style range:range];
89
+ }
90
+
91
+ void applyTextAlignment(NSMutableAttributedString *output, NSRange range, NSTextAlignment textAlign)
92
+ {
93
+ NSMutableParagraphStyle *style = getOrCreateParagraphStyle(output, range.location);
94
+ style.alignment = textAlign;
95
+ [output addAttribute:NSParagraphStyleAttributeName value:style range:range];
96
+ }
97
+
98
+ NSTextAlignment textAlignmentFromString(NSString *textAlign)
99
+ {
100
+ if ([textAlign isEqualToString:@"center"]) {
101
+ return NSTextAlignmentCenter;
102
+ } else if ([textAlign isEqualToString:@"right"]) {
103
+ return NSTextAlignmentRight;
104
+ } else if ([textAlign isEqualToString:@"justify"]) {
105
+ return NSTextAlignmentJustified;
106
+ } else if ([textAlign isEqualToString:@"auto"]) {
107
+ return NSTextAlignmentNatural;
108
+ }
109
+ // Default to left for "left" or any unknown value
110
+ return NSTextAlignmentLeft;
111
+ }
@@ -0,0 +1,36 @@
1
+ #pragma once
2
+ #import <Foundation/Foundation.h>
3
+ #import <UIKit/UIKit.h>
4
+
5
+ @class StyleConfig;
6
+
7
+ NS_ASSUME_NONNULL_BEGIN
8
+
9
+ #ifdef __cplusplus
10
+ extern "C" {
11
+ #endif
12
+
13
+ /**
14
+ * Copies attributed string to pasteboard with multiple representations
15
+ * (plain text, Markdown, HTML, RTFD, RTF). Receiving apps pick the richest format they support.
16
+ */
17
+ void copyAttributedStringToPasteboard(NSAttributedString *attributedString, NSString *_Nullable markdown,
18
+ StyleConfig *_Nullable styleConfig);
19
+
20
+ /**
21
+ * Extracts markdown for the given range.
22
+ * Full selection returns cached markdown; partial selection reverse-engineers from attributes.
23
+ */
24
+ NSString *_Nullable markdownForRange(NSAttributedString *attributedText, NSRange range,
25
+ NSString *_Nullable cachedMarkdown);
26
+
27
+ /**
28
+ * Returns remote image URLs (http/https only) from EnrichedMarkdownImageAttachments in the given range.
29
+ */
30
+ NSArray<NSString *> *imageURLsInRange(NSAttributedString *attributedText, NSRange range);
31
+
32
+ #ifdef __cplusplus
33
+ }
34
+ #endif
35
+
36
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,134 @@
1
+ #import "PasteboardUtils.h"
2
+ #import "EnrichedMarkdownImageAttachment.h"
3
+ #import "HTMLGenerator.h"
4
+ #import "MarkdownExtractor.h"
5
+ #import "RTFExportUtils.h"
6
+ #import "StyleConfig.h"
7
+ #import <UIKit/UIPasteboard.h>
8
+
9
+ static NSString *const kUTIPlainText = @"public.utf8-plain-text";
10
+ static NSString *const kUTIMarkdown = @"net.daringfireball.markdown";
11
+ static NSString *const kUTIHTML = @"public.html";
12
+ static NSString *const kUTIRTFD = @"com.apple.rtfd";
13
+ static NSString *const kUTIFlatRTFD = @"com.apple.flat-rtfd";
14
+ static NSString *const kUTIRTF = @"public.rtf";
15
+
16
+ #pragma mark - Private Helpers
17
+
18
+ static void addRTFData(NSMutableDictionary *items, NSAttributedString *attributedString, NSRange range,
19
+ NSString *documentType, NSString *uti)
20
+ {
21
+ NSError *error = nil;
22
+ NSData *data = [attributedString dataFromRange:range
23
+ documentAttributes:@{NSDocumentTypeDocumentAttribute : documentType}
24
+ error:&error];
25
+ if (data && !error) {
26
+ items[uti] = data;
27
+ }
28
+ }
29
+
30
+ static void addRTFDData(NSMutableDictionary *items, NSAttributedString *attributedString, NSRange range)
31
+ {
32
+ NSError *error = nil;
33
+ NSFileWrapper *wrapper =
34
+ [attributedString fileWrapperFromRange:range
35
+ documentAttributes:@{NSDocumentTypeDocumentAttribute : NSRTFDTextDocumentType}
36
+ error:&error];
37
+ if (wrapper && !error) {
38
+ NSData *data = [wrapper serializedRepresentation];
39
+ if (data) {
40
+ items[kUTIRTFD] = data;
41
+ items[kUTIFlatRTFD] = data;
42
+ }
43
+ }
44
+ }
45
+
46
+ static void addHTMLData(NSMutableDictionary *items, NSAttributedString *attributedString, StyleConfig *styleConfig)
47
+ {
48
+ NSString *html = generateHTML(attributedString, styleConfig);
49
+ if (html) {
50
+ NSData *data = [html dataUsingEncoding:NSUTF8StringEncoding];
51
+ if (data) {
52
+ items[kUTIHTML] = data;
53
+ }
54
+ }
55
+ }
56
+
57
+ #pragma mark - Public API
58
+
59
+ void copyAttributedStringToPasteboard(NSAttributedString *attributedString, NSString *_Nullable markdown,
60
+ StyleConfig *_Nullable styleConfig)
61
+ {
62
+ if (!attributedString || attributedString.length == 0)
63
+ return;
64
+
65
+ NSMutableDictionary *items = [NSMutableDictionary dictionary];
66
+
67
+ items[kUTIPlainText] = attributedString.string;
68
+
69
+ if (markdown.length > 0) {
70
+ items[kUTIMarkdown] = markdown;
71
+ }
72
+
73
+ if (styleConfig) {
74
+ addHTMLData(items, attributedString, styleConfig);
75
+ }
76
+
77
+ // RTF export requires preprocessing (backgrounds, markers, normalized spacing)
78
+ NSAttributedString *rtfPrepared = prepareAttributedStringForRTFExport(attributedString, styleConfig);
79
+ NSRange rtfRange = NSMakeRange(0, rtfPrepared.length);
80
+
81
+ addRTFDData(items, rtfPrepared, rtfRange);
82
+ addRTFData(items, rtfPrepared, rtfRange, NSRTFTextDocumentType, kUTIRTF);
83
+
84
+ [[UIPasteboard generalPasteboard] setItems:@[ items ]];
85
+ }
86
+
87
+ #pragma mark - Content Extraction
88
+
89
+ NSString *_Nullable markdownForRange(NSAttributedString *attributedText, NSRange range,
90
+ NSString *_Nullable cachedMarkdown)
91
+ {
92
+ if (!cachedMarkdown || range.length == 0)
93
+ return nil;
94
+
95
+ if (!attributedText || range.location >= attributedText.length)
96
+ return nil;
97
+
98
+ range.length = MIN(range.length, attributedText.length - range.location);
99
+
100
+ // Full selection: use cached markdown directly
101
+ BOOL isFullSelection = (range.location == 0 && range.length >= attributedText.length - 1);
102
+ if (isFullSelection) {
103
+ return cachedMarkdown;
104
+ }
105
+
106
+ // Partial selection: reverse-engineer from attributes
107
+ return extractMarkdownFromAttributedString(attributedText, range);
108
+ }
109
+
110
+ NSArray<NSString *> *imageURLsInRange(NSAttributedString *attributedText, NSRange range)
111
+ {
112
+ if (!attributedText || range.location == NSNotFound || range.length == 0 || range.location >= attributedText.length) {
113
+ return @[];
114
+ }
115
+
116
+ range.length = MIN(range.length, attributedText.length - range.location);
117
+
118
+ NSMutableArray<NSString *> *urls = [NSMutableArray array];
119
+
120
+ [attributedText enumerateAttribute:NSAttachmentAttributeName
121
+ inRange:range
122
+ options:0
123
+ usingBlock:^(id value, NSRange r, BOOL *stop) {
124
+ if (![value isKindOfClass:[EnrichedMarkdownImageAttachment class]])
125
+ return;
126
+
127
+ NSString *url = ((EnrichedMarkdownImageAttachment *)value).imageURL;
128
+ if ([url hasPrefix:@"http://"] || [url hasPrefix:@"https://"]) {
129
+ [urls addObject:url];
130
+ }
131
+ }];
132
+
133
+ return urls;
134
+ }
@@ -0,0 +1,24 @@
1
+ #pragma once
2
+ #import <Foundation/Foundation.h>
3
+ #import <UIKit/UIKit.h>
4
+
5
+ @class StyleConfig;
6
+
7
+ NS_ASSUME_NONNULL_BEGIN
8
+
9
+ #ifdef __cplusplus
10
+ extern "C" {
11
+ #endif
12
+
13
+ /**
14
+ * Prepares NSAttributedString for RTF/RTFD export by adding backgrounds,
15
+ * inserting text markers (>, •, 1.), and normalizing paragraph styles.
16
+ */
17
+ NSAttributedString *prepareAttributedStringForRTFExport(NSAttributedString *attributedString,
18
+ StyleConfig *_Nullable styleConfig);
19
+
20
+ #ifdef __cplusplus
21
+ }
22
+ #endif
23
+
24
+ NS_ASSUME_NONNULL_END