react-native-enriched-markdown 0.1.1 → 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 (127) hide show
  1. package/README.md +80 -8
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +17 -2
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +6 -1
  4. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +9 -0
  5. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +6 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +28 -3
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +225 -1
  8. package/android/src/main/cpp/jni-adapter.cpp +28 -11
  9. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +132 -15
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +1 -16
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +67 -13
  12. package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +241 -21
  13. package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/MarkdownAccessibilityHelper.kt +279 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLongPressEvent.kt +23 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +2 -0
  16. package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +17 -3
  17. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +13 -18
  18. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +23 -24
  19. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +1 -0
  20. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +2 -1
  21. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +2 -1
  22. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +18 -2
  23. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +22 -6
  24. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +1 -0
  25. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +3 -2
  26. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +2 -1
  27. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +16 -9
  28. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +5 -1
  29. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +23 -9
  30. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +24 -10
  31. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +1 -0
  32. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt +27 -0
  33. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +2 -1
  34. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +1 -0
  35. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +1 -0
  36. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/UnderlineRenderer.kt +27 -0
  37. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +1 -0
  38. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +8 -17
  39. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +19 -5
  40. package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +1 -1
  41. package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrikethroughSpan.kt +12 -0
  42. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +1 -0
  43. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +3 -0
  44. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +3 -0
  45. package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +5 -1
  46. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +3 -1
  47. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +3 -0
  48. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +5 -1
  49. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrikethroughStyle.kt +17 -0
  50. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +32 -1
  51. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +22 -5
  52. package/android/src/main/java/com/swmansion/enriched/markdown/styles/TextAlignment.kt +32 -0
  53. package/android/src/main/java/com/swmansion/enriched/markdown/styles/UnderlineStyle.kt +17 -0
  54. package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +23 -5
  55. package/android/src/main/java/com/swmansion/enriched/markdown/utils/LinkLongPressMovementMethod.kt +121 -0
  56. package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +10 -0
  57. package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +58 -56
  58. package/android/src/main/jni/CMakeLists.txt +1 -13
  59. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +0 -13
  60. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +2 -14
  61. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +3 -0
  62. package/cpp/parser/MD4CParser.cpp +21 -8
  63. package/cpp/parser/MD4CParser.hpp +5 -1
  64. package/cpp/parser/MarkdownASTNode.hpp +2 -0
  65. package/ios/EnrichedMarkdownText.mm +356 -29
  66. package/ios/attachments/{ImageAttachment.h → EnrichedMarkdownImageAttachment.h} +1 -1
  67. package/ios/attachments/{ImageAttachment.m → EnrichedMarkdownImageAttachment.m} +4 -4
  68. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +9 -0
  69. package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +6 -0
  70. package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +28 -3
  71. package/ios/generated/EnrichedMarkdownTextSpec/Props.h +225 -1
  72. package/ios/parser/MarkdownASTNode.h +2 -0
  73. package/ios/parser/MarkdownParser.h +9 -0
  74. package/ios/parser/MarkdownParser.mm +31 -2
  75. package/ios/parser/MarkdownParserBridge.mm +13 -3
  76. package/ios/renderer/AttributedRenderer.h +2 -0
  77. package/ios/renderer/AttributedRenderer.m +52 -19
  78. package/ios/renderer/BlockquoteRenderer.m +7 -6
  79. package/ios/renderer/CodeBlockRenderer.m +9 -8
  80. package/ios/renderer/HeadingRenderer.m +31 -24
  81. package/ios/renderer/ImageRenderer.m +31 -10
  82. package/ios/renderer/ListItemRenderer.m +51 -39
  83. package/ios/renderer/ListRenderer.m +21 -18
  84. package/ios/renderer/ParagraphRenderer.m +27 -16
  85. package/ios/renderer/RenderContext.h +17 -0
  86. package/ios/renderer/RenderContext.m +66 -2
  87. package/ios/renderer/RendererFactory.m +6 -0
  88. package/ios/renderer/StrikethroughRenderer.h +6 -0
  89. package/ios/renderer/StrikethroughRenderer.m +40 -0
  90. package/ios/renderer/UnderlineRenderer.h +6 -0
  91. package/ios/renderer/UnderlineRenderer.m +39 -0
  92. package/ios/styles/StyleConfig.h +46 -0
  93. package/ios/styles/StyleConfig.mm +351 -12
  94. package/ios/utils/AccessibilityInfo.h +35 -0
  95. package/ios/utils/AccessibilityInfo.m +24 -0
  96. package/ios/utils/CodeBlockBackground.m +4 -9
  97. package/ios/utils/FontUtils.h +5 -0
  98. package/ios/utils/FontUtils.m +14 -0
  99. package/ios/utils/HTMLGenerator.m +21 -7
  100. package/ios/utils/MarkdownAccessibilityElementBuilder.h +45 -0
  101. package/ios/utils/MarkdownAccessibilityElementBuilder.m +323 -0
  102. package/ios/utils/MarkdownExtractor.m +18 -5
  103. package/ios/utils/ParagraphStyleUtils.h +10 -2
  104. package/ios/utils/ParagraphStyleUtils.m +57 -2
  105. package/ios/utils/PasteboardUtils.h +1 -1
  106. package/ios/utils/PasteboardUtils.m +3 -3
  107. package/lib/module/EnrichedMarkdownText.js +33 -2
  108. package/lib/module/EnrichedMarkdownText.js.map +1 -1
  109. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +83 -3
  110. package/lib/module/index.js +0 -1
  111. package/lib/module/index.js.map +1 -1
  112. package/lib/module/normalizeMarkdownStyle.js +58 -14
  113. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  114. package/lib/typescript/src/EnrichedMarkdownText.d.ts +85 -3
  115. package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -1
  116. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +75 -1
  117. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  118. package/lib/typescript/src/index.d.ts +2 -3
  119. package/lib/typescript/src/index.d.ts.map +1 -1
  120. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  121. package/package.json +1 -1
  122. package/src/EnrichedMarkdownText.tsx +133 -5
  123. package/src/EnrichedMarkdownTextNativeComponent.ts +83 -3
  124. package/src/index.tsx +5 -2
  125. package/src/normalizeMarkdownStyle.ts +46 -0
  126. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +0 -9
  127. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +0 -25
@@ -0,0 +1,35 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ @class RenderContext;
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ /**
8
+ * Container for accessibility-related data extracted from RenderContext.
9
+ * Used by MarkdownAccessibilityElementBuilder to build VoiceOver elements.
10
+ */
11
+ @interface AccessibilityInfo : NSObject
12
+
13
+ // Headings
14
+ @property (nonatomic, copy, readonly) NSArray<NSValue *> *headingRanges;
15
+ @property (nonatomic, copy, readonly) NSArray<NSNumber *> *headingLevels;
16
+
17
+ // Links
18
+ @property (nonatomic, copy, readonly) NSArray<NSValue *> *linkRanges;
19
+ @property (nonatomic, copy, readonly) NSArray<NSString *> *linkURLs;
20
+
21
+ // Images
22
+ @property (nonatomic, copy, readonly) NSArray<NSValue *> *imageRanges;
23
+ @property (nonatomic, copy, readonly) NSArray<NSString *> *imageAltTexts;
24
+
25
+ // List items
26
+ @property (nonatomic, copy, readonly) NSArray<NSValue *> *listItemRanges;
27
+ @property (nonatomic, copy, readonly) NSArray<NSNumber *> *listItemPositions;
28
+ @property (nonatomic, copy, readonly) NSArray<NSNumber *> *listItemDepths;
29
+ @property (nonatomic, copy, readonly) NSArray<NSNumber *> *listItemOrdered; // YES = ordered, NO = bullet
30
+
31
+ + (instancetype)infoFromContext:(RenderContext *)context;
32
+
33
+ @end
34
+
35
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,24 @@
1
+ #import "AccessibilityInfo.h"
2
+ #import "RenderContext.h"
3
+
4
+ @implementation AccessibilityInfo
5
+
6
+ + (instancetype)infoFromContext:(RenderContext *)context
7
+ {
8
+ AccessibilityInfo *info = [[AccessibilityInfo alloc] init];
9
+ if (info) {
10
+ info->_headingRanges = [context.headingRanges copy];
11
+ info->_headingLevels = [context.headingLevels copy];
12
+ info->_linkRanges = [context.linkRanges copy];
13
+ info->_linkURLs = [context.linkURLs copy];
14
+ info->_imageRanges = [context.imageRanges copy];
15
+ info->_imageAltTexts = [context.imageAltTexts copy];
16
+ info->_listItemRanges = [context.listItemRanges copy];
17
+ info->_listItemPositions = [context.listItemPositions copy];
18
+ info->_listItemDepths = [context.listItemDepths copy];
19
+ info->_listItemOrdered = [context.listItemOrdered copy];
20
+ }
21
+ return info;
22
+ }
23
+
24
+ @end
@@ -50,22 +50,17 @@
50
50
  blockRect.origin.y += origin.y;
51
51
  blockRect.size.width = textContainer.size.width;
52
52
 
53
- // For the last code block, extend to the full view height
54
- // (iOS doesn't properly measure trailing newlines with custom line heights)
53
+ // We extend the background specifically to cover the bottom padding spacer,
54
+ // excluding any additional marginBottom applied via measurement.
55
55
  BOOL isLastCodeBlock = (NSMaxRange(range) == layoutManager.textStorage.length);
56
56
  if (isLastCodeBlock) {
57
- CGFloat viewHeight = textContainer.size.height;
58
- if (viewHeight > 0 && viewHeight < CGFLOAT_MAX) {
59
- blockRect.size.height = viewHeight - blockRect.origin.y + origin.y;
60
- } else {
61
- // Fallback: add padding if container height not set
62
- blockRect.size.height += [_config codeBlockPadding];
63
- }
57
+ blockRect.size.height += [_config codeBlockPadding];
64
58
  }
65
59
 
66
60
  CGFloat borderWidth = [_config codeBlockBorderWidth];
67
61
  CGFloat borderRadius = [_config codeBlockBorderRadius];
68
62
  CGFloat inset = borderWidth / 2.0;
63
+
69
64
  CGRect insetRect = CGRectInset(blockRect, inset, inset);
70
65
  UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:insetRect cornerRadius:MAX(0, borderRadius - inset)];
71
66
 
@@ -13,6 +13,11 @@ extern "C" {
13
13
  /** Returns a cached UIFont from BlockStyle properties via RenderContext. */
14
14
  extern UIFont *cachedFontFromBlockStyle(BlockStyle *blockStyle, RenderContext *context);
15
15
 
16
+ /** Returns the font scale multiplier, capped by maxFontSizeMultiplier.
17
+ * Uses React Native's RCTFontSizeMultiplier() internally.
18
+ * @param maxFontSizeMultiplier Values >= 1.0 cap the result, < 1.0 means no cap. */
19
+ extern CGFloat RCTFontSizeMultiplierWithMax(CGFloat maxFontSizeMultiplier);
20
+
16
21
  #ifdef __cplusplus
17
22
  }
18
23
  #endif
@@ -1,5 +1,6 @@
1
1
  #import "FontUtils.h"
2
2
  #import "RenderContext.h"
3
+ #import <React/RCTUtils.h>
3
4
 
4
5
  UIFont *cachedFontFromBlockStyle(BlockStyle *blockStyle, RenderContext *context)
5
6
  {
@@ -11,3 +12,16 @@ UIFont *cachedFontFromBlockStyle(BlockStyle *blockStyle, RenderContext *context)
11
12
  }
12
13
  return [context cachedFontForSize:blockStyle.fontSize family:blockStyle.fontFamily weight:blockStyle.fontWeight];
13
14
  }
15
+
16
+ CGFloat RCTFontSizeMultiplierWithMax(CGFloat maxFontSizeMultiplier)
17
+ {
18
+ CGFloat multiplier = RCTFontSizeMultiplier();
19
+
20
+ // Apply maxFontSizeMultiplier cap if >= 1.0
21
+ // Values < 1.0 (including 0 and NaN) mean no cap is applied
22
+ if (!isnan(maxFontSizeMultiplier) && maxFontSizeMultiplier >= 1.0) {
23
+ return fmin(maxFontSizeMultiplier, multiplier);
24
+ }
25
+
26
+ return multiplier;
27
+ }
@@ -1,7 +1,7 @@
1
1
  #import "HTMLGenerator.h"
2
2
  #import "BlockquoteBorder.h"
3
3
  #import "CodeBackground.h"
4
- #import "ImageAttachment.h"
4
+ #import "EnrichedMarkdownImageAttachment.h"
5
5
  #import "LastElementUtils.h"
6
6
  #import "ListItemRenderer.h"
7
7
  #import "RenderContext.h"
@@ -100,6 +100,8 @@ typedef struct {
100
100
  @property (nonatomic) CGFloat thematicBreakHeight;
101
101
  @property (nonatomic) CGFloat thematicBreakMarginTop;
102
102
  @property (nonatomic) CGFloat thematicBreakMarginBottom;
103
+ @property (nonatomic, copy) NSString *strikethroughColor;
104
+ @property (nonatomic, copy) NSString *underlineColor;
103
105
  @end
104
106
 
105
107
  @implementation CachedStyles
@@ -276,6 +278,8 @@ static CachedStyles *cacheStyles(StyleConfig *styleConfig)
276
278
  cache.thematicBreakHeight = [styleConfig thematicBreakHeight];
277
279
  cache.thematicBreakMarginTop = [styleConfig thematicBreakMarginTop];
278
280
  cache.thematicBreakMarginBottom = [styleConfig thematicBreakMarginBottom];
281
+ cache.strikethroughColor = colorToCSS([styleConfig strikethroughColor]);
282
+ cache.underlineColor = colorToCSS([styleConfig underlineColor]);
279
283
 
280
284
  return cache;
281
285
  }
@@ -430,8 +434,8 @@ static void generateInlineHTML(NSMutableString *html, NSAttributedString *attrib
430
434
 
431
435
  if ([text containsString:kObjectReplacementChar]) {
432
436
  id attachment = attrs[NSAttachmentAttributeName];
433
- if ([attachment isKindOfClass:[ImageAttachment class]]) {
434
- ImageAttachment *img = (ImageAttachment *)attachment;
437
+ if ([attachment isKindOfClass:[EnrichedMarkdownImageAttachment class]]) {
438
+ EnrichedMarkdownImageAttachment *img = (EnrichedMarkdownImageAttachment *)attachment;
435
439
  if (img.imageURL) {
436
440
  if (img.isInline) {
437
441
  [html appendFormat:
@@ -501,10 +505,20 @@ static void generateInlineHTML(NSMutableString *html, NSAttributedString *attrib
501
505
  }
502
506
  }
503
507
 
504
- if ([strikethrough integerValue] > 0)
505
- [html appendString:@"<s>"];
506
- if ([underline integerValue] > 0 && !linkAttr)
507
- [html appendString:@"<u>"];
508
+ if ([strikethrough integerValue] > 0) {
509
+ if (styles.strikethroughColor && ![styles.strikethroughColor isEqualToString:@"inherit"]) {
510
+ [html appendFormat:@"<s style=\"text-decoration-color: %@;\">", styles.strikethroughColor];
511
+ } else {
512
+ [html appendString:@"<s>"];
513
+ }
514
+ }
515
+ if ([underline integerValue] > 0 && !linkAttr) {
516
+ if (styles.underlineColor && ![styles.underlineColor isEqualToString:@"inherit"]) {
517
+ [html appendFormat:@"<u style=\"text-decoration-color: %@;\">", styles.underlineColor];
518
+ } else {
519
+ [html appendString:@"<u>"];
520
+ }
521
+ }
508
522
 
509
523
  [html appendString:escapeHTML(text)];
510
524
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  #import "MarkdownExtractor.h"
2
2
  #import "BlockquoteBorder.h"
3
- #import "ImageAttachment.h"
3
+ #import "EnrichedMarkdownImageAttachment.h"
4
4
  #import "LastElementUtils.h"
5
5
  #import "RuntimeKeys.h"
6
6
  #import "ThematicBreakAttachment.h"
@@ -67,7 +67,8 @@ static void extractFontTraits(NSDictionary *attrs, BOOL *isBold, BOOL *isItalic,
67
67
  }
68
68
  }
69
69
 
70
- static NSString *applyInlineFormatting(NSString *text, BOOL isBold, BOOL isItalic, BOOL isMonospace, NSString *linkURL)
70
+ static NSString *applyInlineFormatting(NSString *text, BOOL isBold, BOOL isItalic, BOOL isMonospace,
71
+ BOOL isStrikethrough, BOOL isUnderline, NSString *linkURL)
71
72
  {
72
73
  NSMutableString *result = [NSMutableString stringWithString:text];
73
74
 
@@ -75,6 +76,12 @@ static NSString *applyInlineFormatting(NSString *text, BOOL isBold, BOOL isItali
75
76
  if (isMonospace && !linkURL) {
76
77
  result = [NSMutableString stringWithFormat:@"`%@`", result];
77
78
  }
79
+ if (isStrikethrough) {
80
+ result = [NSMutableString stringWithFormat:@"~~%@~~", result];
81
+ }
82
+ if (isUnderline && !linkURL) {
83
+ result = [NSMutableString stringWithFormat:@"<u>%@</u>", result];
84
+ }
78
85
  if (isItalic) {
79
86
  result = [NSMutableString stringWithFormat:@"*%@*", result];
80
87
  }
@@ -128,8 +135,8 @@ NSString *_Nullable extractMarkdownFromAttributedString(NSAttributedString *attr
128
135
 
129
136
  // Images and Thematic Breaks
130
137
  NSTextAttachment *attachment = attrs[NSAttachmentAttributeName];
131
- if ([attachment isKindOfClass:[ImageAttachment class]]) {
132
- ImageAttachment *img = (ImageAttachment *)attachment;
138
+ if ([attachment isKindOfClass:[EnrichedMarkdownImageAttachment class]]) {
139
+ EnrichedMarkdownImageAttachment *img = (EnrichedMarkdownImageAttachment *)attachment;
133
140
  if (!img.imageURL)
134
141
  return;
135
142
 
@@ -255,8 +262,14 @@ NSString *_Nullable extractMarkdownFromAttributedString(NSAttributedString *attr
255
262
  BOOL isBold, isItalic, isMonospace;
256
263
  extractFontTraits(attrs, &isBold, &isItalic, &isMonospace);
257
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
+
258
270
  NSString *linkURL = attrs[NSLinkAttributeName];
259
- NSString *segment = applyInlineFormatting(text, isBold, isItalic, isMonospace, linkURL);
271
+ NSString *segment = applyInlineFormatting(text, isBold, isItalic, isMonospace, isStrikethrough,
272
+ isUnderline, linkURL);
260
273
 
261
274
  // Add block prefixes at line start
262
275
  if (isAtLineStart(result)) {
@@ -3,11 +3,19 @@
3
3
 
4
4
  NS_ASSUME_NONNULL_BEGIN
5
5
 
6
+ __BEGIN_DECLS
7
+
6
8
  extern NSAttributedString *kNewlineAttributedString;
7
9
 
8
10
  NSMutableParagraphStyle *getOrCreateParagraphStyle(NSMutableAttributedString *output, NSUInteger index);
9
- void applyParagraphSpacing(NSMutableAttributedString *output, NSUInteger start, CGFloat marginBottom);
10
- void applyBlockSpacing(NSMutableAttributedString *output, CGFloat marginBottom);
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);
11
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
12
20
 
13
21
  NS_ASSUME_NONNULL_END