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,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,45 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ @class AccessibilityInfo;
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ /**
8
+ * Builds UIAccessibilityElement objects from markdown content for VoiceOver.
9
+ * Handles headings, links, images, lists, and custom rotor navigation.
10
+ */
11
+ @interface MarkdownAccessibilityElementBuilder : NSObject
12
+
13
+ /**
14
+ * Builds accessibility elements for the given text view content.
15
+ * - Headings get UIAccessibilityTraitHeader
16
+ * - Links get UIAccessibilityTraitLink
17
+ * - Images get UIAccessibilityTraitImage
18
+ * - List items get position hints (e.g., "bullet point", "list item 1")
19
+ * - Other content gets UIAccessibilityTraitStaticText
20
+ */
21
+ + (NSMutableArray<UIAccessibilityElement *> *)buildElementsForTextView:(UITextView *)textView
22
+ info:(AccessibilityInfo *)info
23
+ container:(id)container;
24
+
25
+ /** Filters elements with UIAccessibilityTraitHeader trait. */
26
+ + (NSArray<UIAccessibilityElement *> *)filterHeadingElements:(NSArray<UIAccessibilityElement *> *)elements;
27
+
28
+ /** Filters elements with UIAccessibilityTraitLink trait. */
29
+ + (NSArray<UIAccessibilityElement *> *)filterLinkElements:(NSArray<UIAccessibilityElement *> *)elements;
30
+
31
+ /** Filters elements with UIAccessibilityTraitImage trait. */
32
+ + (NSArray<UIAccessibilityElement *> *)filterImageElements:(NSArray<UIAccessibilityElement *> *)elements;
33
+
34
+ /** Creates a custom rotor for heading navigation. */
35
+ + (UIAccessibilityCustomRotor *)createHeadingRotorWithElements:(NSArray<UIAccessibilityElement *> *)elements;
36
+
37
+ /** Creates a custom rotor for link navigation. */
38
+ + (UIAccessibilityCustomRotor *)createLinkRotorWithElements:(NSArray<UIAccessibilityElement *> *)elements;
39
+
40
+ /** Creates a custom rotor for image navigation. */
41
+ + (UIAccessibilityCustomRotor *)createImageRotorWithElements:(NSArray<UIAccessibilityElement *> *)elements;
42
+
43
+ @end
44
+
45
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,323 @@
1
+ #import "MarkdownAccessibilityElementBuilder.h"
2
+ #import "AccessibilityInfo.h"
3
+
4
+ typedef NS_ENUM(NSInteger, ElementType) { ElementTypeText, ElementTypeLink, ElementTypeImage };
5
+
6
+ @implementation MarkdownAccessibilityElementBuilder
7
+
8
+ #pragma mark - Public API
9
+
10
+ + (NSMutableArray<UIAccessibilityElement *> *)buildElementsForTextView:(UITextView *)textView
11
+ info:(AccessibilityInfo *)info
12
+ container:(id)container
13
+ {
14
+ NSString *fullString = textView.attributedText.string;
15
+ if (fullString.length == 0)
16
+ return [NSMutableArray array];
17
+
18
+ [textView.layoutManager ensureLayoutForTextContainer:textView.textContainer];
19
+
20
+ NSMutableArray<UIAccessibilityElement *> *elements = [NSMutableArray array];
21
+ NSUInteger currentPos = 0;
22
+
23
+ while (currentPos < fullString.length) {
24
+ NSRange paragraphRange = [fullString paragraphRangeForRange:NSMakeRange(currentPos, 0)];
25
+ NSString *trimmed = [[fullString substringWithRange:paragraphRange]
26
+ stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
27
+
28
+ if (trimmed.length > 0) {
29
+ NSArray *links = [self linksInRange:paragraphRange info:info];
30
+ NSArray *images = [self imagesInRange:paragraphRange info:info];
31
+ NSArray *specials = [links arrayByAddingObjectsFromArray:images];
32
+
33
+ NSInteger level = [self headingLevelForRange:paragraphRange info:info];
34
+ NSDictionary *list = [self listItemInfoForRange:paragraphRange info:info];
35
+
36
+ if (specials.count == 0) {
37
+ [self addTextElementsPerLineTo:elements
38
+ range:paragraphRange
39
+ fullText:fullString
40
+ heading:level
41
+ listInfo:list
42
+ view:textView
43
+ container:container];
44
+ } else {
45
+ [elements addObjectsFromArray:[self segmentedElementsForParagraph:paragraphRange
46
+ fullText:fullString
47
+ headingLevel:level
48
+ listInfo:list
49
+ specials:specials
50
+ inTextView:textView
51
+ container:container]];
52
+ }
53
+ }
54
+ currentPos = NSMaxRange(paragraphRange);
55
+ }
56
+ return elements;
57
+ }
58
+
59
+ #pragma mark - Segmentation
60
+
61
+ + (NSArray<UIAccessibilityElement *> *)segmentedElementsForParagraph:(NSRange)paragraphRange
62
+ fullText:(NSString *)fullText
63
+ headingLevel:(NSInteger)headingLevel
64
+ listInfo:(NSDictionary *)listInfo
65
+ specials:(NSArray *)specials
66
+ inTextView:(UITextView *)textView
67
+ container:(id)container
68
+ {
69
+ NSMutableArray<UIAccessibilityElement *> *elements = [NSMutableArray array];
70
+ NSArray *sortedSpecials = [specials sortedArrayUsingComparator:^NSComparisonResult(NSDictionary *a, NSDictionary *b) {
71
+ return [@([a[@"range"] rangeValue].location) compare:@([b[@"range"] rangeValue].location)];
72
+ }];
73
+
74
+ NSUInteger segmentStart = paragraphRange.location;
75
+ for (NSDictionary *item in sortedSpecials) {
76
+ NSRange itemRange = [item[@"range"] rangeValue];
77
+
78
+ if (itemRange.location > segmentStart) {
79
+ NSRange beforeRange = NSMakeRange(segmentStart, itemRange.location - segmentStart);
80
+ [self addTextElementsPerLineTo:elements
81
+ range:beforeRange
82
+ fullText:fullText
83
+ heading:headingLevel
84
+ listInfo:listInfo
85
+ view:textView
86
+ container:container];
87
+ }
88
+
89
+ BOOL isImg = item[@"altText"] != nil;
90
+ NSString *label = isImg ? item[@"altText"] : [fullText substringWithRange:itemRange];
91
+ [elements addObject:[self createElementForRange:itemRange
92
+ type:isImg ? ElementTypeImage : ElementTypeLink
93
+ text:label
94
+ isLinked:isImg ? [item[@"isLinked"] boolValue] : YES
95
+ heading:0
96
+ listInfo:listInfo
97
+ view:textView
98
+ container:container]];
99
+ segmentStart = NSMaxRange(itemRange);
100
+ }
101
+
102
+ if (segmentStart < NSMaxRange(paragraphRange)) {
103
+ NSRange afterRange = NSMakeRange(segmentStart, NSMaxRange(paragraphRange) - segmentStart);
104
+ [self addTextElementsPerLineTo:elements
105
+ range:afterRange
106
+ fullText:fullText
107
+ heading:headingLevel
108
+ listInfo:listInfo
109
+ view:textView
110
+ container:container];
111
+ }
112
+ return elements;
113
+ }
114
+
115
+ #pragma mark - Factory & Precise Splitting
116
+
117
+ + (UIAccessibilityElement *)createElementForRange:(NSRange)range
118
+ type:(ElementType)type
119
+ text:(NSString *)text
120
+ isLinked:(BOOL)linked
121
+ heading:(NSInteger)level
122
+ listInfo:(NSDictionary *)listInfo
123
+ view:(UITextView *)tv
124
+ container:(id)c
125
+ {
126
+ UIAccessibilityElement *el = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:c];
127
+ el.accessibilityLabel = (type == ElementTypeImage && text.length == 0) ? NSLocalizedString(@"Image", @"") : text;
128
+ el.accessibilityFrameInContainerSpace = [self frameForRange:range inTextView:tv container:c];
129
+
130
+ NSMutableArray *values = [NSMutableArray array];
131
+
132
+ if (type == ElementTypeImage) {
133
+ el.accessibilityTraits =
134
+ linked ? (UIAccessibilityTraitImage | UIAccessibilityTraitLink) : UIAccessibilityTraitImage;
135
+ } else if (type == ElementTypeLink) {
136
+ el.accessibilityTraits = UIAccessibilityTraitLink;
137
+ } else if (level > 0) {
138
+ el.accessibilityTraits = UIAccessibilityTraitHeader;
139
+ [values addObject:[NSString stringWithFormat:NSLocalizedString(@"heading level %ld", @""), (long)level]];
140
+ }
141
+
142
+ if (el.accessibilityTraits & UIAccessibilityTraitLink) {
143
+ el.accessibilityHint = NSLocalizedString(@"Tap to open link", @"");
144
+ }
145
+
146
+ // Append List Info to values if it exists
147
+ if (listInfo && type != ElementTypeImage) {
148
+ [values addObject:[self formatListAnnouncement:listInfo]];
149
+ }
150
+
151
+ // Combine all values (Heading Level + List Position) into one string
152
+ if (values.count > 0) {
153
+ el.accessibilityValue = [values componentsJoinedByString:@", "];
154
+ }
155
+
156
+ return el;
157
+ }
158
+
159
+ + (void)addTextElementsPerLineTo:(NSMutableArray *)elements
160
+ range:(NSRange)range
161
+ fullText:(NSString *)fullText
162
+ heading:(NSInteger)level
163
+ listInfo:(NSDictionary *)listInfo
164
+ view:(UITextView *)tv
165
+ container:(id)c
166
+ {
167
+ NSLayoutManager *lm = tv.layoutManager;
168
+ NSRange glyphRange = [lm glyphRangeForCharacterRange:range actualCharacterRange:NULL];
169
+
170
+ [lm enumerateLineFragmentsForGlyphRange:glyphRange
171
+ usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *tc, NSRange lineGlyphRange,
172
+ BOOL *stop) {
173
+ NSRange intersection = NSIntersectionRange(glyphRange, lineGlyphRange);
174
+ if (intersection.length > 0) {
175
+ NSRange charRange = [lm characterRangeForGlyphRange:intersection
176
+ actualGlyphRange:NULL];
177
+ NSString *trimmed = [[fullText substringWithRange:charRange]
178
+ stringByTrimmingCharactersInSet:[NSCharacterSet
179
+ whitespaceAndNewlineCharacterSet]];
180
+ if (trimmed.length > 0) {
181
+ [elements addObject:[self createElementForRange:charRange
182
+ type:ElementTypeText
183
+ text:trimmed
184
+ isLinked:NO
185
+ heading:level
186
+ listInfo:listInfo
187
+ view:tv
188
+ container:c]];
189
+ }
190
+ }
191
+ }];
192
+ }
193
+
194
+ #pragma mark - Helpers
195
+
196
+ + (NSString *)formatListAnnouncement:(NSDictionary *)info
197
+ {
198
+ NSString *prefix = [info[@"depth"] integerValue] > 1 ? @"nested " : @"";
199
+ return [info[@"isOrdered"] boolValue]
200
+ ? [NSString stringWithFormat:@"%@list item %ld", prefix, (long)[info[@"position"] integerValue]]
201
+ : [NSString stringWithFormat:@"%@bullet point", prefix];
202
+ }
203
+
204
+ + (CGRect)frameForRange:(NSRange)range inTextView:(UITextView *)textView container:(id)container
205
+ {
206
+ NSRange glyphRange = [textView.layoutManager glyphRangeForCharacterRange:range actualCharacterRange:NULL];
207
+ CGRect rect = [textView.layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textView.textContainer];
208
+ rect = CGRectOffset(CGRectInset(rect, -2, -2), textView.textContainerInset.left, textView.textContainerInset.top);
209
+ return [(UIView *)container convertRect:CGRectIntegral(rect) fromView:textView];
210
+ }
211
+
212
+ #pragma mark - Data Helpers
213
+
214
+ + (NSInteger)headingLevelForRange:(NSRange)range info:(AccessibilityInfo *)info
215
+ {
216
+ for (NSUInteger i = 0; i < info.headingRanges.count; i++) {
217
+ if (NSIntersectionRange(range, [info.headingRanges[i] rangeValue]).length > 0)
218
+ return [info.headingLevels[i] integerValue];
219
+ }
220
+ return 0;
221
+ }
222
+
223
+ + (NSArray *)linksInRange:(NSRange)range info:(AccessibilityInfo *)info
224
+ {
225
+ NSMutableArray *links = [NSMutableArray array];
226
+ for (NSUInteger i = 0; i < info.linkRanges.count; i++) {
227
+ if (NSIntersectionRange(range, [info.linkRanges[i] rangeValue]).length > 0) {
228
+ [links addObject:@{@"range" : info.linkRanges[i], @"url" : info.linkURLs[i] ?: @""}];
229
+ }
230
+ }
231
+ return links;
232
+ }
233
+
234
+ + (NSArray *)imagesInRange:(NSRange)range info:(AccessibilityInfo *)info
235
+ {
236
+ NSMutableArray *images = [NSMutableArray array];
237
+ for (NSUInteger i = 0; i < info.imageRanges.count; i++) {
238
+ NSRange imgRange = [info.imageRanges[i] rangeValue];
239
+ if (NSIntersectionRange(range, imgRange).length > 0) {
240
+ BOOL linked = NO;
241
+ for (NSValue *val in info.linkRanges)
242
+ if (NSIntersectionRange(imgRange, val.rangeValue).length > 0) {
243
+ linked = YES;
244
+ break;
245
+ }
246
+ [images addObject:@{
247
+ @"range" : info.imageRanges[i],
248
+ @"altText" : info.imageAltTexts[i] ?: @"",
249
+ @"isLinked" : @(linked)
250
+ }];
251
+ }
252
+ }
253
+ return images;
254
+ }
255
+
256
+ + (NSDictionary *)listItemInfoForRange:(NSRange)range info:(AccessibilityInfo *)info
257
+ {
258
+ if (!info)
259
+ return nil;
260
+ for (NSUInteger i = 0; i < info.listItemRanges.count; i++) {
261
+ if (NSIntersectionRange(range, [info.listItemRanges[i] rangeValue]).length > 0) {
262
+ return @{
263
+ @"position" : info.listItemPositions[i],
264
+ @"depth" : info.listItemDepths[i],
265
+ @"isOrdered" : info.listItemOrdered[i]
266
+ };
267
+ }
268
+ }
269
+ return nil;
270
+ }
271
+
272
+ #pragma mark - Rotors
273
+
274
+ + (NSArray *)filterElements:(NSArray *)els withTrait:(UIAccessibilityTraits)trait
275
+ {
276
+ return [els filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UIAccessibilityElement *el, id b) {
277
+ return (el.accessibilityTraits & trait) != 0;
278
+ }]];
279
+ }
280
+
281
+ + (UIAccessibilityCustomRotor *)createRotorWithName:(NSString *)name elements:(NSArray *)els
282
+ {
283
+ return [[UIAccessibilityCustomRotor alloc]
284
+ initWithName:name
285
+ itemSearchBlock:^UIAccessibilityCustomRotorItemResult *(UIAccessibilityCustomRotorSearchPredicate *p) {
286
+ if (els.count == 0)
287
+ return nil;
288
+ NSInteger idx = p.currentItem.targetElement ? [els indexOfObject:p.currentItem.targetElement] : NSNotFound;
289
+ NSInteger next = (p.searchDirection == UIAccessibilityCustomRotorDirectionNext)
290
+ ? (idx == NSNotFound ? 0 : idx + 1)
291
+ : (idx == NSNotFound ? els.count - 1 : idx - 1);
292
+ return (next >= 0 && next < els.count)
293
+ ? [[UIAccessibilityCustomRotorItemResult alloc] initWithTargetElement:els[next] targetRange:nil]
294
+ : nil;
295
+ }];
296
+ }
297
+
298
+ + (NSArray<UIAccessibilityElement *> *)filterHeadingElements:(NSArray *)els
299
+ {
300
+ return [self filterElements:els withTrait:UIAccessibilityTraitHeader];
301
+ }
302
+ + (NSArray<UIAccessibilityElement *> *)filterLinkElements:(NSArray *)els
303
+ {
304
+ return [self filterElements:els withTrait:UIAccessibilityTraitLink];
305
+ }
306
+ + (NSArray<UIAccessibilityElement *> *)filterImageElements:(NSArray *)els
307
+ {
308
+ return [self filterElements:els withTrait:UIAccessibilityTraitImage];
309
+ }
310
+ + (UIAccessibilityCustomRotor *)createHeadingRotorWithElements:(NSArray *)els
311
+ {
312
+ return [self createRotorWithName:NSLocalizedString(@"Headings", @"") elements:els];
313
+ }
314
+ + (UIAccessibilityCustomRotor *)createLinkRotorWithElements:(NSArray *)els
315
+ {
316
+ return [self createRotorWithName:NSLocalizedString(@"Links", @"") elements:els];
317
+ }
318
+ + (UIAccessibilityCustomRotor *)createImageRotorWithElements:(NSArray *)els
319
+ {
320
+ return [self createRotorWithName:NSLocalizedString(@"Images", @"") elements:els];
321
+ }
322
+
323
+ @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