react-native-enriched-markdown 0.1.0 → 0.1.1

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 (211) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +479 -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 +39 -0
  6. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +21 -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 +24 -0
  10. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
  11. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +57 -0
  12. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1164 -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 +203 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +153 -0
  22. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +30 -0
  23. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +119 -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 +165 -0
  26. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkPressEvent.kt +23 -0
  27. package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +29 -0
  28. package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +48 -0
  29. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +166 -0
  30. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +89 -0
  31. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +105 -0
  32. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +35 -0
  33. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +15 -0
  34. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +26 -0
  35. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +54 -0
  36. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +52 -0
  37. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +15 -0
  38. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +28 -0
  39. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt +105 -0
  40. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +58 -0
  41. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +69 -0
  42. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +99 -0
  43. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +66 -0
  44. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +95 -0
  45. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +85 -0
  46. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +26 -0
  47. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +29 -0
  48. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +44 -0
  49. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BaseListSpan.kt +136 -0
  50. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BlockquoteSpan.kt +135 -0
  51. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +180 -0
  52. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBlockSpan.kt +196 -0
  53. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeSpan.kt +27 -0
  54. package/android/src/main/java/com/swmansion/enriched/markdown/spans/EmphasisSpan.kt +34 -0
  55. package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +38 -0
  56. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +320 -0
  57. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +36 -0
  58. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +37 -0
  59. package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +76 -0
  60. package/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +87 -0
  61. package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrongSpan.kt +37 -0
  62. package/android/src/main/java/com/swmansion/enriched/markdown/spans/TextSpan.kt +26 -0
  63. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ThematicBreakSpan.kt +69 -0
  64. package/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +69 -0
  65. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +10 -0
  66. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +48 -0
  67. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +51 -0
  68. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeStyle.kt +21 -0
  69. package/android/src/main/java/com/swmansion/enriched/markdown/styles/EmphasisStyle.kt +17 -0
  70. package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +29 -0
  71. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +21 -0
  72. package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineImageStyle.kt +17 -0
  73. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +19 -0
  74. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +54 -0
  75. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +29 -0
  76. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrongStyle.kt +17 -0
  77. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +180 -0
  78. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +75 -0
  79. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ThematicBreakStyle.kt +23 -0
  80. package/android/src/main/java/com/swmansion/enriched/markdown/utils/AsyncDrawable.kt +91 -0
  81. package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +809 -0
  82. package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +365 -0
  83. package/android/src/main/java/com/swmansion/enriched/markdown/utils/SelectionActionMode.kt +139 -0
  84. package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +181 -0
  85. package/android/src/main/jni/CMakeLists.txt +82 -0
  86. package/android/src/main/jni/EnrichedMarkdownTextSpec.cpp +21 -0
  87. package/android/src/main/jni/EnrichedMarkdownTextSpec.h +25 -0
  88. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextComponentDescriptor.h +29 -0
  89. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.cpp +45 -0
  90. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.h +21 -0
  91. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +33 -0
  92. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +49 -0
  93. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +9 -0
  94. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +25 -0
  95. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +19 -0
  96. package/cpp/md4c/md4c.c +6492 -0
  97. package/cpp/md4c/md4c.h +402 -0
  98. package/cpp/parser/MD4CParser.cpp +314 -0
  99. package/cpp/parser/MD4CParser.hpp +23 -0
  100. package/cpp/parser/MarkdownASTNode.hpp +49 -0
  101. package/ios/EnrichedMarkdownText.h +18 -0
  102. package/ios/EnrichedMarkdownText.mm +1074 -0
  103. package/ios/attachments/ImageAttachment.h +23 -0
  104. package/ios/attachments/ImageAttachment.m +185 -0
  105. package/ios/attachments/ThematicBreakAttachment.h +15 -0
  106. package/ios/attachments/ThematicBreakAttachment.m +33 -0
  107. package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
  108. package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
  109. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +24 -0
  110. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
  111. package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +57 -0
  112. package/ios/generated/EnrichedMarkdownTextSpec/Props.h +1164 -0
  113. package/ios/generated/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +20 -0
  114. package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
  115. package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
  116. package/ios/generated/EnrichedMarkdownTextSpec/States.cpp +16 -0
  117. package/ios/generated/EnrichedMarkdownTextSpec/States.h +20 -0
  118. package/ios/internals/EnrichedMarkdownTextComponentDescriptor.h +19 -0
  119. package/ios/internals/EnrichedMarkdownTextShadowNode.h +43 -0
  120. package/ios/internals/EnrichedMarkdownTextShadowNode.mm +85 -0
  121. package/ios/internals/EnrichedMarkdownTextState.h +24 -0
  122. package/ios/parser/MarkdownASTNode.h +33 -0
  123. package/ios/parser/MarkdownASTNode.m +32 -0
  124. package/ios/parser/MarkdownParser.h +8 -0
  125. package/ios/parser/MarkdownParser.mm +13 -0
  126. package/ios/parser/MarkdownParserBridge.mm +110 -0
  127. package/ios/renderer/AttributedRenderer.h +9 -0
  128. package/ios/renderer/AttributedRenderer.m +119 -0
  129. package/ios/renderer/BlockquoteRenderer.h +7 -0
  130. package/ios/renderer/BlockquoteRenderer.m +159 -0
  131. package/ios/renderer/CodeBlockRenderer.h +10 -0
  132. package/ios/renderer/CodeBlockRenderer.m +89 -0
  133. package/ios/renderer/CodeRenderer.h +10 -0
  134. package/ios/renderer/CodeRenderer.m +60 -0
  135. package/ios/renderer/EmphasisRenderer.h +6 -0
  136. package/ios/renderer/EmphasisRenderer.m +96 -0
  137. package/ios/renderer/HeadingRenderer.h +7 -0
  138. package/ios/renderer/HeadingRenderer.m +98 -0
  139. package/ios/renderer/ImageRenderer.h +12 -0
  140. package/ios/renderer/ImageRenderer.m +62 -0
  141. package/ios/renderer/LinkRenderer.h +7 -0
  142. package/ios/renderer/LinkRenderer.m +69 -0
  143. package/ios/renderer/ListItemRenderer.h +16 -0
  144. package/ios/renderer/ListItemRenderer.m +91 -0
  145. package/ios/renderer/ListRenderer.h +13 -0
  146. package/ios/renderer/ListRenderer.m +67 -0
  147. package/ios/renderer/NodeRenderer.h +8 -0
  148. package/ios/renderer/ParagraphRenderer.h +7 -0
  149. package/ios/renderer/ParagraphRenderer.m +69 -0
  150. package/ios/renderer/RenderContext.h +88 -0
  151. package/ios/renderer/RenderContext.m +248 -0
  152. package/ios/renderer/RendererFactory.h +12 -0
  153. package/ios/renderer/RendererFactory.m +110 -0
  154. package/ios/renderer/StrongRenderer.h +6 -0
  155. package/ios/renderer/StrongRenderer.m +83 -0
  156. package/ios/renderer/TextRenderer.h +6 -0
  157. package/ios/renderer/TextRenderer.m +16 -0
  158. package/ios/renderer/ThematicBreakRenderer.h +5 -0
  159. package/ios/renderer/ThematicBreakRenderer.m +53 -0
  160. package/ios/styles/StyleConfig.h +228 -0
  161. package/ios/styles/StyleConfig.mm +1467 -0
  162. package/ios/utils/BlockquoteBorder.h +20 -0
  163. package/ios/utils/BlockquoteBorder.m +92 -0
  164. package/ios/utils/CodeBackground.h +19 -0
  165. package/ios/utils/CodeBackground.m +191 -0
  166. package/ios/utils/CodeBlockBackground.h +17 -0
  167. package/ios/utils/CodeBlockBackground.m +87 -0
  168. package/ios/utils/EditMenuUtils.h +22 -0
  169. package/ios/utils/EditMenuUtils.m +118 -0
  170. package/ios/utils/FontUtils.h +20 -0
  171. package/ios/utils/FontUtils.m +13 -0
  172. package/ios/utils/HTMLGenerator.h +20 -0
  173. package/ios/utils/HTMLGenerator.m +779 -0
  174. package/ios/utils/LastElementUtils.h +53 -0
  175. package/ios/utils/ListMarkerDrawer.h +15 -0
  176. package/ios/utils/ListMarkerDrawer.m +127 -0
  177. package/ios/utils/MarkdownExtractor.h +17 -0
  178. package/ios/utils/MarkdownExtractor.m +295 -0
  179. package/ios/utils/ParagraphStyleUtils.h +13 -0
  180. package/ios/utils/ParagraphStyleUtils.m +56 -0
  181. package/ios/utils/PasteboardUtils.h +36 -0
  182. package/ios/utils/PasteboardUtils.m +134 -0
  183. package/ios/utils/RTFExportUtils.h +24 -0
  184. package/ios/utils/RTFExportUtils.m +297 -0
  185. package/ios/utils/RuntimeKeys.h +38 -0
  186. package/ios/utils/RuntimeKeys.m +11 -0
  187. package/ios/utils/TextViewLayoutManager.h +14 -0
  188. package/ios/utils/TextViewLayoutManager.mm +113 -0
  189. package/lib/module/EnrichedMarkdownText.js +34 -0
  190. package/lib/module/EnrichedMarkdownText.js.map +1 -0
  191. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +130 -0
  192. package/lib/module/index.js +5 -0
  193. package/lib/module/index.js.map +1 -0
  194. package/lib/module/normalizeMarkdownStyle.js +340 -0
  195. package/lib/module/normalizeMarkdownStyle.js.map +1 -0
  196. package/lib/module/package.json +1 -0
  197. package/lib/typescript/package.json +1 -0
  198. package/lib/typescript/src/EnrichedMarkdownText.d.ts +101 -0
  199. package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -0
  200. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +111 -0
  201. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -0
  202. package/lib/typescript/src/index.d.ts +5 -0
  203. package/lib/typescript/src/index.d.ts.map +1 -0
  204. package/lib/typescript/src/normalizeMarkdownStyle.d.ts +6 -0
  205. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -0
  206. package/package.json +186 -1
  207. package/react-native.config.js +13 -0
  208. package/src/EnrichedMarkdownText.tsx +152 -0
  209. package/src/EnrichedMarkdownTextNativeComponent.ts +130 -0
  210. package/src/index.tsx +7 -0
  211. package/src/normalizeMarkdownStyle.ts +377 -0
@@ -0,0 +1,53 @@
1
+ #pragma once
2
+ #import <UIKit/UIKit.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ static NSString *const CodeBlockAttributeName = @"CodeBlock";
7
+
8
+ /**
9
+ * Checks if the last element in the attributed string is a code block.
10
+ * Used to compensate for iOS text APIs not measuring/drawing trailing newlines with custom line heights.
11
+ */
12
+ static inline BOOL isLastElementCodeBlock(NSAttributedString *text)
13
+ {
14
+ if (text.length == 0)
15
+ return NO;
16
+
17
+ NSRange lastContent = [text.string rangeOfCharacterFromSet:[[NSCharacterSet newlineCharacterSet] invertedSet]
18
+ options:NSBackwardsSearch];
19
+ if (lastContent.location == NSNotFound)
20
+ return NO;
21
+
22
+ NSNumber *isCodeBlock = [text attribute:CodeBlockAttributeName atIndex:lastContent.location effectiveRange:nil];
23
+ if (!isCodeBlock.boolValue)
24
+ return NO;
25
+
26
+ NSRange codeBlockRange;
27
+ [text attribute:CodeBlockAttributeName atIndex:lastContent.location effectiveRange:&codeBlockRange];
28
+ return NSMaxRange(codeBlockRange) == text.length;
29
+ }
30
+
31
+ /**
32
+ * Checks if the last element in the attributed string is an image attachment.
33
+ * Used to compensate for iOS text attachment baseline spacing issues.
34
+ */
35
+ static inline BOOL isLastElementImage(NSAttributedString *text)
36
+ {
37
+ if (text.length == 0)
38
+ return NO;
39
+
40
+ NSRange lastContent = [text.string rangeOfCharacterFromSet:[[NSCharacterSet newlineCharacterSet] invertedSet]
41
+ options:NSBackwardsSearch];
42
+ if (lastContent.location == NSNotFound)
43
+ return NO;
44
+
45
+ unichar lastChar = [text.string characterAtIndex:lastContent.location];
46
+ if (lastChar != 0xFFFC)
47
+ return NO;
48
+
49
+ id attachment = [text attribute:NSAttachmentAttributeName atIndex:lastContent.location effectiveRange:nil];
50
+ return attachment != nil;
51
+ }
52
+
53
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,15 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ @class StyleConfig;
5
+
6
+ @interface ListMarkerDrawer : NSObject
7
+
8
+ - (instancetype)initWithConfig:(StyleConfig *)config;
9
+
10
+ - (void)drawMarkersForGlyphRange:(NSRange)glyphsToShow
11
+ layoutManager:(NSLayoutManager *)layoutManager
12
+ textContainer:(NSTextContainer *)textContainer
13
+ atPoint:(CGPoint)origin;
14
+
15
+ @end
@@ -0,0 +1,127 @@
1
+ #import "ListMarkerDrawer.h"
2
+ #import "ListItemRenderer.h"
3
+ #import "RenderContext.h"
4
+ #import "StyleConfig.h"
5
+
6
+ // Reference external symbols defined in ListItemRenderer.m
7
+ extern NSString *const ListDepthAttribute;
8
+ extern NSString *const ListTypeAttribute;
9
+ extern NSString *const ListItemNumberAttribute;
10
+
11
+ @implementation ListMarkerDrawer {
12
+ StyleConfig *_config;
13
+ }
14
+
15
+ - (instancetype)initWithConfig:(StyleConfig *)config
16
+ {
17
+ if (self = [super init]) {
18
+ _config = config;
19
+ }
20
+ return self;
21
+ }
22
+
23
+ - (void)drawMarkersForGlyphRange:(NSRange)glyphsToShow
24
+ layoutManager:(NSLayoutManager *)layoutManager
25
+ textContainer:(NSTextContainer *)textContainer
26
+ atPoint:(CGPoint)origin
27
+ {
28
+ NSTextStorage *storage = layoutManager.textStorage;
29
+ if (!storage || storage.length == 0)
30
+ return;
31
+
32
+ // Cache gap and track paragraphs to prevent double-drawing on wrapped lines
33
+ CGFloat gap = [_config effectiveListGapWidth];
34
+ NSMutableSet *drawnParagraphs = [NSMutableSet set];
35
+
36
+ [layoutManager
37
+ enumerateLineFragmentsForGlyphRange:glyphsToShow
38
+ usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *container,
39
+ NSRange glyphRange, BOOL *stop) {
40
+ NSRange charRange = [layoutManager characterRangeForGlyphRange:glyphRange
41
+ actualGlyphRange:NULL];
42
+ if (charRange.location == NSNotFound)
43
+ return;
44
+
45
+ // 1. Fetch all attributes at once for efficiency
46
+ NSDictionary *attrs = [storage attributesAtIndex:charRange.location
47
+ effectiveRange:NULL];
48
+ if (!attrs[ListDepthAttribute])
49
+ return;
50
+
51
+ // 2. Identify the start of the paragraph
52
+ NSRange paraRange = [storage.string paragraphRangeForRange:charRange];
53
+ if (charRange.location != paraRange.location ||
54
+ [drawnParagraphs containsObject:@(paraRange.location)])
55
+ return;
56
+ [drawnParagraphs addObject:@(paraRange.location)];
57
+
58
+ // 3. Calculate Layout Coordinates
59
+ CGPoint glyphLoc = [layoutManager locationForGlyphAtIndex:glyphRange.location];
60
+ CGFloat baselineY = origin.y + rect.origin.y + glyphLoc.y;
61
+ CGFloat textStartX = origin.x + usedRect.origin.x;
62
+
63
+ // 4. Draw marker based on type
64
+ if ([attrs[ListTypeAttribute] integerValue] == ListTypeUnordered) {
65
+ UIFont *font = attrs[NSFontAttributeName] ?: [self defaultFont];
66
+ [self drawBulletAtX:textStartX - gap centerY:baselineY - (font.xHeight / 2.0)];
67
+ } else {
68
+ [self drawOrderedMarkerAtX:textStartX - gap attrs:attrs baselineY:baselineY];
69
+ }
70
+ }];
71
+ }
72
+
73
+ #pragma mark - Drawing Helpers
74
+
75
+ - (void)drawBulletAtX:(CGFloat)x centerY:(CGFloat)y
76
+ {
77
+ [self
78
+ executeDrawing:^(CGContextRef ctx) {
79
+ [[_config listStyleBulletColor] ?: [UIColor blackColor] setFill];
80
+ CGFloat size = [_config listStyleBulletSize];
81
+ CGContextFillEllipseInRect(ctx, CGRectMake(x - size / 2.0, y - size / 2.0, size, size));
82
+ }
83
+ atX:x
84
+ y:y];
85
+ }
86
+
87
+ - (void)drawOrderedMarkerAtX:(CGFloat)rightBoundaryX attrs:(NSDictionary *)attrs baselineY:(CGFloat)baselineY
88
+ {
89
+ NSNumber *num = attrs[ListItemNumberAttribute];
90
+ if (!num)
91
+ return;
92
+
93
+ NSString *text = [NSString stringWithFormat:@"%ld.", (long)num.integerValue];
94
+ UIFont *font = [_config listMarkerFont] ?: [self defaultFont];
95
+
96
+ NSDictionary *mAttrs = @{
97
+ NSFontAttributeName : font,
98
+ NSForegroundColorAttributeName : [_config listStyleMarkerColor] ?: [UIColor blackColor]
99
+ };
100
+ CGSize size = [text sizeWithAttributes:mAttrs];
101
+
102
+ if ([self isValidX:rightBoundaryX - size.width y:baselineY]) {
103
+ [text drawAtPoint:CGPointMake(rightBoundaryX - size.width, baselineY - font.ascender) withAttributes:mAttrs];
104
+ }
105
+ }
106
+
107
+ - (void)executeDrawing:(void (^)(CGContextRef))block atX:(CGFloat)x y:(CGFloat)y
108
+ {
109
+ CGContextRef ctx = UIGraphicsGetCurrentContext();
110
+ if (ctx && [self isValidX:x y:y]) {
111
+ CGContextSaveGState(ctx);
112
+ block(ctx);
113
+ CGContextRestoreGState(ctx);
114
+ }
115
+ }
116
+
117
+ - (UIFont *)defaultFont
118
+ {
119
+ return [UIFont systemFontOfSize:[_config listStyleFontSize]];
120
+ }
121
+
122
+ - (BOOL)isValidX:(CGFloat)x y:(CGFloat)y
123
+ {
124
+ return !isnan(x) && !isinf(x) && !isnan(y) && !isinf(y);
125
+ }
126
+
127
+ @end
@@ -0,0 +1,17 @@
1
+ #pragma once
2
+ #import <UIKit/UIKit.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ #ifdef __cplusplus
7
+ extern "C" {
8
+ #endif
9
+
10
+ /// Extracts markdown from an attributed string (best-effort reconstruction).
11
+ NSString *_Nullable extractMarkdownFromAttributedString(NSAttributedString *attributedText, NSRange range);
12
+
13
+ #ifdef __cplusplus
14
+ }
15
+ #endif
16
+
17
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,295 @@
1
+ #import "MarkdownExtractor.h"
2
+ #import "BlockquoteBorder.h"
3
+ #import "ImageAttachment.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, NSString *linkURL)
71
+ {
72
+ NSMutableString *result = [NSMutableString stringWithString:text];
73
+
74
+ // Innermost first
75
+ if (isMonospace && !linkURL) {
76
+ result = [NSMutableString stringWithFormat:@"`%@`", result];
77
+ }
78
+ if (isItalic) {
79
+ result = [NSMutableString stringWithFormat:@"*%@*", result];
80
+ }
81
+ if (isBold) {
82
+ result = [NSMutableString stringWithFormat:@"**%@**", result];
83
+ }
84
+ if (linkURL) {
85
+ result = [NSMutableString stringWithFormat:@"[%@](%@)", text, linkURL];
86
+ }
87
+
88
+ return result;
89
+ }
90
+
91
+ #pragma mark - Main Extraction Function
92
+
93
+ NSString *_Nullable extractMarkdownFromAttributedString(NSAttributedString *attributedText, NSRange range)
94
+ {
95
+ if (!attributedText || range.length == 0 || range.location >= attributedText.length) {
96
+ return nil;
97
+ }
98
+
99
+ range.length = MIN(range.length, attributedText.length - range.location);
100
+
101
+ NSMutableString *result = [NSMutableString string];
102
+
103
+ // Headings may span multiple attribute runs
104
+ __block NSString *currentHeadingType = nil;
105
+ __block NSMutableString *headingContent = nil;
106
+ __block ExtractionState state = {.blockquoteDepth = -1, .listDepth = -1, .needsBlankLine = NO};
107
+
108
+ void (^flushHeading)(void) = ^{
109
+ if (!currentHeadingType || headingContent.length == 0)
110
+ return;
111
+
112
+ ensureBlankLine(result);
113
+ NSInteger level = [[currentHeadingType substringFromIndex:8] integerValue];
114
+ [result appendFormat:@"%@%@\n", buildHeadingPrefix(level), headingContent];
115
+
116
+ currentHeadingType = nil;
117
+ headingContent = nil;
118
+ state.needsBlankLine = YES;
119
+ };
120
+
121
+ [attributedText
122
+ enumerateAttributesInRange:range
123
+ options:0
124
+ usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange attrRange, BOOL *stop) {
125
+ NSString *text = [[attributedText attributedSubstringFromRange:attrRange] string];
126
+ if (text.length == 0)
127
+ return;
128
+
129
+ // Images and Thematic Breaks
130
+ NSTextAttachment *attachment = attrs[NSAttachmentAttributeName];
131
+ if ([attachment isKindOfClass:[ImageAttachment class]]) {
132
+ ImageAttachment *img = (ImageAttachment *)attachment;
133
+ if (!img.imageURL)
134
+ return;
135
+
136
+ if (img.isInline) {
137
+ [result appendFormat:@"![image](%@)", img.imageURL];
138
+ } else {
139
+ ensureBlankLine(result);
140
+ [result appendFormat:@"![image](%@)\n", img.imageURL];
141
+ state.needsBlankLine = YES;
142
+ state.blockquoteDepth = -1;
143
+ state.listDepth = -1;
144
+ }
145
+ return;
146
+ }
147
+
148
+ if ([attachment isKindOfClass:[ThematicBreakAttachment class]]) {
149
+ ensureBlankLine(result);
150
+ [result appendString:@"---\n"];
151
+ state.needsBlankLine = YES;
152
+ state.blockquoteDepth = -1;
153
+ state.listDepth = -1;
154
+ return;
155
+ }
156
+
157
+ if ([text isEqualToString:@"\uFFFC"])
158
+ return;
159
+
160
+ // Newlines
161
+ if ([text isEqualToString:@"\n"] || [text isEqualToString:@"\n\n"]) {
162
+ NSNumber *bqDepth = attrs[BlockquoteDepthAttributeName];
163
+ NSNumber *listDepth = attrs[@"ListDepth"];
164
+ BOOL inBlockquote = (bqDepth != nil);
165
+ BOOL inList = (listDepth != nil);
166
+
167
+ if (!inBlockquote && state.blockquoteDepth >= 0) {
168
+ ensureBlankLine(result);
169
+ state.blockquoteDepth = -1;
170
+ return;
171
+ }
172
+
173
+ if (!inList && state.listDepth >= 0) {
174
+ ensureBlankLine(result);
175
+ state.listDepth = -1;
176
+ return;
177
+ }
178
+
179
+ if (inBlockquote || inList) {
180
+ if (![result hasSuffix:@"\n"]) {
181
+ [result appendString:@"\n"];
182
+ }
183
+ return;
184
+ }
185
+
186
+ ensureBlankLine(result);
187
+ return;
188
+ }
189
+
190
+ // Headings
191
+ NSString *markdownType = attrs[MarkdownTypeAttributeName];
192
+
193
+ if (markdownType && [markdownType hasPrefix:@"heading-"]) {
194
+ if (![markdownType isEqualToString:currentHeadingType]) {
195
+ flushHeading();
196
+ currentHeadingType = markdownType;
197
+ headingContent = [NSMutableString string];
198
+ }
199
+ [headingContent
200
+ appendString:[text stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]];
201
+ return;
202
+ } else if (currentHeadingType) {
203
+ flushHeading();
204
+ }
205
+
206
+ // Code blocks
207
+ NSNumber *isCodeBlock = attrs[CodeBlockAttributeName];
208
+ if ([isCodeBlock boolValue]) {
209
+ if (state.needsBlankLine) {
210
+ ensureBlankLine(result);
211
+ state.needsBlankLine = NO;
212
+ }
213
+
214
+ BOOL needsFence = (result.length == 0) || [result hasSuffix:@"\n\n"];
215
+ if (needsFence) {
216
+ [result appendString:@"```\n"];
217
+ }
218
+
219
+ [result appendString:text];
220
+
221
+ if ([text hasSuffix:@"\n"]) {
222
+ [result appendString:@"```\n"];
223
+ state.needsBlankLine = YES;
224
+ }
225
+ return;
226
+ }
227
+
228
+ // Blockquotes
229
+ NSNumber *bqDepthNum = attrs[BlockquoteDepthAttributeName];
230
+ NSInteger currentBqDepth = bqDepthNum ? [bqDepthNum integerValue] : -1;
231
+ NSString *blockquotePrefix = nil;
232
+
233
+ if (currentBqDepth >= 0) {
234
+ blockquotePrefix = buildBlockquotePrefix(currentBqDepth);
235
+ state.blockquoteDepth = currentBqDepth;
236
+ } else if (state.blockquoteDepth >= 0) {
237
+ ensureBlankLine(result);
238
+ state.blockquoteDepth = -1;
239
+ }
240
+
241
+ // Lists
242
+ NSNumber *listDepthNum = attrs[@"ListDepth"];
243
+ NSNumber *listTypeNum = attrs[@"ListType"];
244
+ NSNumber *listItemNum = attrs[@"ListItemNumber"];
245
+ NSInteger currentListDepth = listDepthNum ? [listDepthNum integerValue] : -1;
246
+
247
+ if (currentListDepth >= 0) {
248
+ state.listDepth = currentListDepth;
249
+ } else if (state.listDepth >= 0) {
250
+ ensureBlankLine(result);
251
+ state.listDepth = -1;
252
+ }
253
+
254
+ // Inline formatting
255
+ BOOL isBold, isItalic, isMonospace;
256
+ extractFontTraits(attrs, &isBold, &isItalic, &isMonospace);
257
+
258
+ NSString *linkURL = attrs[NSLinkAttributeName];
259
+ NSString *segment = applyInlineFormatting(text, isBold, isItalic, isMonospace, linkURL);
260
+
261
+ // Add block prefixes at line start
262
+ if (isAtLineStart(result)) {
263
+ NSMutableString *prefixedSegment = [NSMutableString string];
264
+
265
+ if (listDepthNum && ![text hasPrefix:@"\n"]) {
266
+ BOOL isOrdered = ([listTypeNum integerValue] == 1);
267
+ NSInteger itemNumber = listItemNum ? [listItemNum integerValue] : 1;
268
+ [prefixedSegment appendString:buildListPrefix(currentListDepth, isOrdered, itemNumber)];
269
+ }
270
+
271
+ if (blockquotePrefix) {
272
+ [prefixedSegment insertString:blockquotePrefix atIndex:0];
273
+ }
274
+
275
+ [prefixedSegment appendString:segment];
276
+ segment = prefixedSegment;
277
+ }
278
+
279
+ if (state.needsBlankLine && result.length > 0) {
280
+ ensureBlankLine(result);
281
+ state.needsBlankLine = NO;
282
+ }
283
+
284
+ [result appendString:segment];
285
+ }];
286
+
287
+ // Flush remaining heading
288
+ if (currentHeadingType && headingContent.length > 0) {
289
+ ensureBlankLine(result);
290
+ NSInteger level = [[currentHeadingType substringFromIndex:8] integerValue];
291
+ [result appendFormat:@"%@%@\n", buildHeadingPrefix(level), headingContent];
292
+ }
293
+
294
+ return result.length > 0 ? result : nil;
295
+ }
@@ -0,0 +1,13 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ extern NSAttributedString *kNewlineAttributedString;
7
+
8
+ NSMutableParagraphStyle *getOrCreateParagraphStyle(NSMutableAttributedString *output, NSUInteger index);
9
+ void applyParagraphSpacing(NSMutableAttributedString *output, NSUInteger start, CGFloat marginBottom);
10
+ void applyBlockSpacing(NSMutableAttributedString *output, CGFloat marginBottom);
11
+ void applyLineHeight(NSMutableAttributedString *output, NSRange range, CGFloat lineHeight);
12
+
13
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,56 @@
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 applyParagraphSpacing(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 applyBlockSpacing(NSMutableAttributedString *output, CGFloat marginBottom)
34
+ {
35
+ NSUInteger spacerLocation = output.length;
36
+ [output appendAttributedString:kNewlineAttributedString];
37
+
38
+ NSMutableParagraphStyle *spacerStyle = [kBlockSpacerTemplate mutableCopy];
39
+ spacerStyle.paragraphSpacing = marginBottom;
40
+
41
+ [output addAttribute:NSParagraphStyleAttributeName value:spacerStyle range:NSMakeRange(spacerLocation, 1)];
42
+ }
43
+
44
+ void applyLineHeight(NSMutableAttributedString *output, NSRange range, CGFloat lineHeight)
45
+ {
46
+ if (lineHeight <= 0) {
47
+ return;
48
+ }
49
+
50
+ NSMutableParagraphStyle *style = getOrCreateParagraphStyle(output, range.location);
51
+
52
+ style.minimumLineHeight = lineHeight;
53
+ style.maximumLineHeight = lineHeight;
54
+
55
+ [output addAttribute:NSParagraphStyleAttributeName value:style range:range];
56
+ }
@@ -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 ImageAttachments 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