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
@@ -1,13 +1,42 @@
1
1
  #import "MarkdownParser.h"
2
2
  #import "MarkdownASTNode.h"
3
3
 
4
- extern MarkdownASTNode *parseMarkdownWithCppParser(NSString *markdown);
4
+ extern MarkdownASTNode *parseMarkdownWithCppParser(NSString *markdown, Md4cFlags *flags);
5
+
6
+ @implementation Md4cFlags
7
+
8
+ - (instancetype)init
9
+ {
10
+ if (self = [super init]) {
11
+ _underline = NO;
12
+ }
13
+ return self;
14
+ }
15
+
16
+ + (instancetype)defaultFlags
17
+ {
18
+ return [[Md4cFlags alloc] init];
19
+ }
20
+
21
+ - (id)copyWithZone:(NSZone *)zone
22
+ {
23
+ Md4cFlags *copy = [[Md4cFlags allocWithZone:zone] init];
24
+ copy.underline = self.underline;
25
+ return copy;
26
+ }
27
+
28
+ @end
5
29
 
6
30
  @implementation MarkdownParser
7
31
 
8
32
  - (MarkdownASTNode *)parseMarkdown:(NSString *)markdown
9
33
  {
10
- return parseMarkdownWithCppParser(markdown);
34
+ return [self parseMarkdown:markdown flags:[Md4cFlags defaultFlags]];
35
+ }
36
+
37
+ - (MarkdownASTNode *)parseMarkdown:(NSString *)markdown flags:(Md4cFlags *)flags
38
+ {
39
+ return parseMarkdownWithCppParser(markdown, flags);
11
40
  }
12
41
 
13
42
  @end
@@ -1,6 +1,7 @@
1
1
  #include "MD4CParser.hpp"
2
2
  #import "MarkdownASTNode.h"
3
3
  #include "MarkdownASTNode.hpp"
4
+ #import "MarkdownParser.h"
4
5
 
5
6
  // Convert C++ AST node to Objective-C AST node
6
7
  static MarkdownASTNode *convertCppASTToObjC(std::shared_ptr<Markdown::MarkdownASTNode> cppNode)
@@ -36,6 +37,12 @@ static MarkdownASTNode *convertCppASTToObjC(std::shared_ptr<Markdown::MarkdownAS
36
37
  case Markdown::NodeType::Emphasis:
37
38
  objcType = MarkdownNodeTypeEmphasis;
38
39
  break;
40
+ case Markdown::NodeType::Strikethrough:
41
+ objcType = MarkdownNodeTypeStrikethrough;
42
+ break;
43
+ case Markdown::NodeType::Underline:
44
+ objcType = MarkdownNodeTypeUnderline;
45
+ break;
39
46
  case Markdown::NodeType::Code:
40
47
  objcType = MarkdownNodeTypeCode;
41
48
  break;
@@ -86,7 +93,7 @@ static MarkdownASTNode *convertCppASTToObjC(std::shared_ptr<Markdown::MarkdownAS
86
93
  }
87
94
 
88
95
  // Public function to parse markdown using C++ parser and convert to Objective-C AST
89
- MarkdownASTNode *parseMarkdownWithCppParser(NSString *markdown)
96
+ MarkdownASTNode *parseMarkdownWithCppParser(NSString *markdown, Md4cFlags *flags)
90
97
  {
91
98
  if (markdown.length == 0) {
92
99
  return [[MarkdownASTNode alloc] initWithType:MarkdownNodeTypeDocument];
@@ -101,9 +108,12 @@ MarkdownASTNode *parseMarkdownWithCppParser(NSString *markdown)
101
108
 
102
109
  std::string cppMarkdown(utf8String);
103
110
 
104
- // Parse using C++ parser
111
+ // Convert Objective-C flags to C++ flags
112
+ Markdown::Md4cFlags cppFlags;
113
+ cppFlags.underline = flags.underline;
114
+
105
115
  Markdown::MD4CParser parser;
106
- auto cppAST = parser.parse(cppMarkdown);
116
+ auto cppAST = parser.parse(cppMarkdown, cppFlags);
107
117
 
108
118
  // Convert C++ AST to Objective-C AST
109
119
  return convertCppASTToObjC(cppAST);
@@ -6,4 +6,6 @@
6
6
  @interface AttributedRenderer : NSObject
7
7
  - (instancetype)initWithConfig:(id)config;
8
8
  - (NSMutableAttributedString *)renderRoot:(MarkdownASTNode *)root context:(RenderContext *)context;
9
+ - (CGFloat)getLastElementMarginBottom;
10
+ - (void)setAllowTrailingMargin:(BOOL)allow;
9
11
  @end
@@ -10,6 +10,8 @@
10
10
  @implementation AttributedRenderer {
11
11
  StyleConfig *_config;
12
12
  RendererFactory *_rendererFactory;
13
+ CGFloat _lastElementMarginBottom;
14
+ BOOL _allowTrailingMargin;
13
15
  }
14
16
 
15
17
  - (instancetype)initWithConfig:(StyleConfig *)config
@@ -18,6 +20,8 @@
18
20
  if (self) {
19
21
  _config = config;
20
22
  _rendererFactory = [[RendererFactory alloc] initWithConfig:config];
23
+ _lastElementMarginBottom = 0.0;
24
+ _allowTrailingMargin = NO;
21
25
  }
22
26
  return self;
23
27
  }
@@ -44,6 +48,8 @@
44
48
  }
45
49
 
46
50
  // 3. Remove trailing paragraph spacing from last block element
51
+ // Reset lastElementMarginBottom before processing
52
+ _lastElementMarginBottom = 0.0;
47
53
  [self removeTrailingSpacing:output];
48
54
 
49
55
  // 4. Cleanup global state to prevent side effects in subsequent renders.
@@ -58,40 +64,67 @@
58
64
  if (output.length == 0)
59
65
  return;
60
66
 
61
- NSRange lastContent = [output.string rangeOfCharacterFromSet:[[NSCharacterSet newlineCharacterSet] invertedSet]
67
+ // Find the last non-newline character
68
+ NSRange lastContent = [output.string rangeOfCharacterFromSet:[NSCharacterSet.newlineCharacterSet invertedSet]
62
69
  options:NSBackwardsSearch];
63
70
  if (lastContent.location == NSNotFound)
64
71
  return;
65
72
 
66
- if (isLastElementCodeBlock(output)) {
67
- // Code block: preserve bottom padding, only trim external margin
68
- NSRange codeBlockRange;
69
- [output attribute:CodeBlockAttributeName atIndex:lastContent.location effectiveRange:&codeBlockRange];
70
- NSUInteger codeBlockEnd = NSMaxRange(codeBlockRange);
71
- if (codeBlockEnd < output.length) {
72
- [output deleteCharactersInRange:NSMakeRange(codeBlockEnd, output.length - codeBlockEnd)];
73
+ // 1. Capture the Margin Bottom (Scanning from last content to end)
74
+ _lastElementMarginBottom = 0.0;
75
+ for (NSUInteger i = lastContent.location; i < output.length;) {
76
+ NSRange attrRange;
77
+ NSParagraphStyle *style = [output attribute:NSParagraphStyleAttributeName atIndex:i effectiveRange:&attrRange];
78
+ if (style) {
79
+ _lastElementMarginBottom = MAX(_lastElementMarginBottom, style.paragraphSpacing);
73
80
  }
74
- } else {
75
- // Other elements: trim trailing newlines and zero all spacing
76
- [output deleteCharactersInRange:NSMakeRange(NSMaxRange(lastContent), output.length - NSMaxRange(lastContent))];
81
+ i = NSMaxRange(attrRange);
82
+ }
83
+
84
+ // 2. Trim trailing characters
85
+ NSUInteger logicalEnd = NSMaxRange(lastContent);
86
+ if (isLastElementCodeBlock(output)) {
87
+ NSRange codeRange;
88
+ [output attribute:CodeBlockAttributeName atIndex:lastContent.location effectiveRange:&codeRange];
89
+ logicalEnd = NSMaxRange(codeRange);
90
+ }
91
+
92
+ if (logicalEnd < output.length) {
93
+ [output deleteCharactersInRange:NSMakeRange(logicalEnd, output.length - logicalEnd)];
94
+ }
77
95
 
78
- NSRange range;
96
+ // 3. Zero out internal spacing for the last element (if not a code block)
97
+ if (!isLastElementCodeBlock(output)) {
98
+ NSRange styleRange;
79
99
  NSParagraphStyle *style = [output attribute:NSParagraphStyleAttributeName
80
100
  atIndex:lastContent.location
81
- effectiveRange:&range];
101
+ effectiveRange:&styleRange];
102
+
82
103
  if (style) {
83
- NSMutableParagraphStyle *fixed = [style mutableCopy];
84
- fixed.paragraphSpacing = 0;
85
- fixed.paragraphSpacingBefore = 0;
86
- // For images: zero line spacing to eliminate baseline gaps
104
+ NSMutableParagraphStyle *mutableStyle = [style mutableCopy];
105
+ mutableStyle.paragraphSpacing = 0;
106
+ mutableStyle.paragraphSpacingBefore = 0;
107
+
87
108
  if (isLastElementImage(output)) {
88
- fixed.lineSpacing = 0;
109
+ mutableStyle.lineSpacing = 0;
89
110
  }
90
- [output addAttribute:NSParagraphStyleAttributeName value:fixed range:range];
111
+
112
+ NSRange safeRange = NSIntersectionRange(styleRange, NSMakeRange(0, output.length));
113
+ [output addAttribute:NSParagraphStyleAttributeName value:mutableStyle range:safeRange];
91
114
  }
92
115
  }
93
116
  }
94
117
 
118
+ - (void)setAllowTrailingMargin:(BOOL)allow
119
+ {
120
+ _allowTrailingMargin = allow;
121
+ }
122
+
123
+ - (CGFloat)getLastElementMarginBottom
124
+ {
125
+ return _lastElementMarginBottom;
126
+ }
127
+
95
128
  /**
96
129
  * Orchestrates the recursive traversal of the AST.
97
130
  * If a specialized renderer exists for a node type, it takes full control.
@@ -56,7 +56,12 @@ static NSString *const kNestedInfoRangeKey = @"range";
56
56
  end:(NSUInteger)end
57
57
  currentDepth:(NSInteger)currentDepth
58
58
  {
59
- NSRange blockquoteRange = NSMakeRange(start, end - start);
59
+ NSUInteger contentStart = start;
60
+ if (currentDepth == 0) {
61
+ contentStart += applyBlockSpacingBefore(output, start, [_config blockquoteMarginTop]);
62
+ }
63
+
64
+ NSRange blockquoteRange = NSMakeRange(contentStart, end - start);
60
65
  CGFloat levelSpacing = [_config blockquoteBorderWidth] + [_config blockquoteGapWidth];
61
66
  NSArray<NSDictionary *> *nestedInfo = [self collectNestedBlockquotes:output range:blockquoteRange depth:currentDepth];
62
67
 
@@ -72,12 +77,8 @@ static NSString *const kNestedInfoRangeKey = @"range";
72
77
  // (applyBaseBlockquoteStyle overwrites nested indents with the parent's indent)
73
78
  [self reapplyNestedStyles:output nestedInfo:nestedInfo levelSpacing:levelSpacing];
74
79
 
75
- // Apply bottom margin for top-level blockquotes only
76
80
  if (currentDepth == 0) {
77
- CGFloat marginBottom = [_config blockquoteMarginBottom];
78
- if (marginBottom > 0) {
79
- applyBlockSpacing(output, marginBottom);
80
- }
81
+ applyBlockSpacingAfter(output, [_config blockquoteMarginBottom]);
81
82
  }
82
83
  }
83
84
 
@@ -27,15 +27,17 @@
27
27
 
28
28
  CGFloat padding = [_config codeBlockPadding];
29
29
  CGFloat lineHeight = [_config codeBlockLineHeight];
30
+ CGFloat marginTop = [_config codeBlockMarginTop];
30
31
  CGFloat marginBottom = [_config codeBlockMarginBottom];
32
+
31
33
  NSUInteger blockStart = output.length;
34
+ blockStart += applyBlockSpacingBefore(output, blockStart, marginTop);
32
35
 
33
- // 1. TOP PADDING: Inside the background
36
+ // Top Padding: Inserted as a spacer character inside the background area
34
37
  [output appendAttributedString:kNewlineAttributedString];
35
38
  NSMutableParagraphStyle *topSpacerStyle = [context spacerStyleWithHeight:padding spacing:0];
36
39
  [output addAttribute:NSParagraphStyleAttributeName value:topSpacerStyle range:NSMakeRange(blockStart, 1)];
37
40
 
38
- // 2. RENDER CONTENT
39
41
  NSUInteger contentStart = output.length;
40
42
  @try {
41
43
  [_rendererFactory renderChildrenOfNode:node into:output context:context];
@@ -49,7 +51,6 @@
49
51
 
50
52
  NSRange contentRange = NSMakeRange(contentStart, contentEnd - contentStart);
51
53
 
52
- // 3. CONTENT STYLING
53
54
  UIFont *codeFont = [_config codeBlockFont];
54
55
  UIColor *codeColor = [_config codeBlockColor];
55
56
  if (codeColor) {
@@ -63,26 +64,26 @@
63
64
  applyLineHeight(output, contentRange, lineHeight);
64
65
  }
65
66
 
66
- // HORIZONTAL INDENTS
67
+ // Apply horizontal indents to the content
67
68
  NSMutableParagraphStyle *baseStyle = [getOrCreateParagraphStyle(output, contentStart) mutableCopy];
68
69
  baseStyle.firstLineHeadIndent = padding;
69
70
  baseStyle.headIndent = padding;
70
71
  baseStyle.tailIndent = -padding;
71
72
  [output addAttribute:NSParagraphStyleAttributeName value:baseStyle range:contentRange];
72
73
 
73
- // 4. BOTTOM PADDING: Inside the background
74
+ // Bottom Padding: Inserted as a spacer character inside the background area
74
75
  NSUInteger bottomPaddingStart = output.length;
75
76
  [output appendAttributedString:kNewlineAttributedString];
76
77
  NSMutableParagraphStyle *bottomPaddingStyle = [context spacerStyleWithHeight:padding spacing:0];
77
78
  [output addAttribute:NSParagraphStyleAttributeName value:bottomPaddingStyle range:NSMakeRange(bottomPaddingStart, 1)];
78
79
 
79
- // MARK BACKGROUND: Ends here to exclude the margin
80
+ // Define the range for background rendering (includes padding, excludes margins)
80
81
  NSRange backgroundRange = NSMakeRange(blockStart, output.length - blockStart);
81
82
  [output addAttribute:CodeBlockAttributeName value:@YES range:backgroundRange];
82
83
 
83
- // 5. EXTERNAL MARGIN: Outside the background
84
+ // External Margin: Applied outside the background range
84
85
  if (marginBottom > 0) {
85
- applyBlockSpacing(output, marginBottom);
86
+ applyBlockSpacingAfter(output, marginBottom);
86
87
  }
87
88
  }
88
89
 
@@ -10,11 +10,12 @@
10
10
  typedef struct {
11
11
  __unsafe_unretained UIFont *font;
12
12
  __unsafe_unretained UIColor *color;
13
+ CGFloat marginTop;
13
14
  CGFloat marginBottom;
14
15
  CGFloat lineHeight;
16
+ NSTextAlignment textAlign;
15
17
  } HeadingStyle;
16
18
 
17
- // Static heading type strings (index 0 unused, 1-6 for h1-h6)
18
19
  static NSString *const kHeadingTypes[] = {nil, @"heading-1", @"heading-2", @"heading-3",
19
20
  @"heading-4", @"heading-5", @"heading-6"};
20
21
 
@@ -32,20 +33,25 @@ static NSString *const kHeadingTypes[] = {nil, @"heading-1", @"heading-
32
33
  return self;
33
34
  }
34
35
 
35
- #pragma mark - Rendering
36
-
37
36
  - (void)renderNode:(MarkdownASTNode *)node into:(NSMutableAttributedString *)output context:(RenderContext *)context
38
37
  {
39
38
  NSInteger level = [node.attributes[@"level"] integerValue];
40
39
  if (level < 1 || level > 6)
41
40
  level = 1;
42
41
 
43
- // Fetch style struct with pre-cached font from StyleConfig
44
42
  HeadingStyle style = [self styleForLevel:level];
45
-
46
43
  [context setBlockStyle:BlockTypeHeading font:style.font color:style.color headingLevel:level];
47
44
 
48
45
  NSUInteger start = output.length;
46
+ NSUInteger contentStart = start;
47
+
48
+ // Spacing at the very start of the document requires a spacer character (index 0 check)
49
+ if (start == 0) {
50
+ NSUInteger offset = applyBlockSpacingBefore(output, 0, style.marginTop);
51
+ contentStart += offset;
52
+ start += offset;
53
+ }
54
+
49
55
  @try {
50
56
  [_rendererFactory renderChildrenOfNode:node into:output context:context];
51
57
  } @finally {
@@ -56,43 +62,44 @@ static NSString *const kHeadingTypes[] = {nil, @"heading-1", @"heading-
56
62
  if (range.length == 0)
57
63
  return;
58
64
 
59
- // Mark as heading for HTML generation and Copy as Markdown
65
+ // Register heading for accessibility
66
+ NSString *headingText = [[output attributedSubstringFromRange:range] string];
67
+ [context registerHeadingRange:range level:level text:headingText];
68
+
69
+ // Metadata attribute used for post-processing (e.g., Export to Markdown/HTML)
60
70
  [output addAttribute:MarkdownTypeAttributeName value:kHeadingTypes[level] range:range];
61
71
 
62
72
  applyLineHeight(output, range, style.lineHeight);
63
- applyParagraphSpacing(output, start, style.marginBottom);
73
+ applyTextAlignment(output, range, style.textAlign);
74
+
75
+ // Use paragraphSpacingBefore for internal elements; applyBlockSpacingBefore handles index 0
76
+ if (contentStart != 1) {
77
+ applyParagraphSpacingBefore(output, range, style.marginTop);
78
+ }
79
+ applyParagraphSpacingAfter(output, start, style.marginBottom);
64
80
  }
65
81
 
66
- #pragma mark - Optimized Style Provider
82
+ #pragma mark - Style Mapping
67
83
 
68
84
  - (HeadingStyle)styleForLevel:(NSInteger)level
69
85
  {
70
86
  StyleConfig *c = _config;
71
- HeadingStyle s;
72
-
73
87
  switch (level) {
74
88
  case 1:
75
- s = (HeadingStyle){c.h1Font, c.h1Color, c.h1MarginBottom, c.h1LineHeight};
76
- break;
89
+ return (HeadingStyle){c.h1Font, c.h1Color, c.h1MarginTop, c.h1MarginBottom, c.h1LineHeight, c.h1TextAlign};
77
90
  case 2:
78
- s = (HeadingStyle){c.h2Font, c.h2Color, c.h2MarginBottom, c.h2LineHeight};
79
- break;
91
+ return (HeadingStyle){c.h2Font, c.h2Color, c.h2MarginTop, c.h2MarginBottom, c.h2LineHeight, c.h2TextAlign};
80
92
  case 3:
81
- s = (HeadingStyle){c.h3Font, c.h3Color, c.h3MarginBottom, c.h3LineHeight};
82
- break;
93
+ return (HeadingStyle){c.h3Font, c.h3Color, c.h3MarginTop, c.h3MarginBottom, c.h3LineHeight, c.h3TextAlign};
83
94
  case 4:
84
- s = (HeadingStyle){c.h4Font, c.h4Color, c.h4MarginBottom, c.h4LineHeight};
85
- break;
95
+ return (HeadingStyle){c.h4Font, c.h4Color, c.h4MarginTop, c.h4MarginBottom, c.h4LineHeight, c.h4TextAlign};
86
96
  case 5:
87
- s = (HeadingStyle){c.h5Font, c.h5Color, c.h5MarginBottom, c.h5LineHeight};
88
- break;
97
+ return (HeadingStyle){c.h5Font, c.h5Color, c.h5MarginTop, c.h5MarginBottom, c.h5LineHeight, c.h5TextAlign};
89
98
  case 6:
90
- s = (HeadingStyle){c.h6Font, c.h6Color, c.h6MarginBottom, c.h6LineHeight};
91
- break;
99
+ return (HeadingStyle){c.h6Font, c.h6Color, c.h6MarginTop, c.h6MarginBottom, c.h6LineHeight, c.h6TextAlign};
92
100
  default:
93
101
  return [self styleForLevel:1];
94
102
  }
95
- return s;
96
103
  }
97
104
 
98
- @end
105
+ @end
@@ -1,5 +1,5 @@
1
1
  #import "ImageRenderer.h"
2
- #import "ImageAttachment.h"
2
+ #import "EnrichedMarkdownImageAttachment.h"
3
3
  #import "MarkdownASTNode.h"
4
4
  #import "RenderContext.h"
5
5
  #import "RendererFactory.h"
@@ -27,35 +27,56 @@ static const unichar kZeroWidthSpace = 0x200B;
27
27
  - (void)renderNode:(MarkdownASTNode *)node into:(NSMutableAttributedString *)output context:(RenderContext *)context
28
28
  {
29
29
  NSString *imageURL = node.attributes[@"url"];
30
- // Safety check for URL presence and length
31
30
  if (!imageURL || imageURL.length == 0) {
32
31
  return;
33
32
  }
34
33
 
35
- // Determine if this image is being placed inside an existing line of text
36
34
  BOOL isInline = [self isInlineImageInOutput:output];
35
+ EnrichedMarkdownImageAttachment *attachment = [[EnrichedMarkdownImageAttachment alloc] initWithImageURL:imageURL
36
+ config:_config
37
+ isInline:isInline];
37
38
 
38
- // Create the attachment using the shared config
39
- ImageAttachment *attachment = [[ImageAttachment alloc] initWithImageURL:imageURL config:_config isInline:isInline];
39
+ NSUInteger startIndex = output.length;
40
40
 
41
- // Append the attachment character to the output
42
41
  NSAttributedString *imageString = [NSAttributedString attributedStringWithAttachment:attachment];
43
42
  [output appendAttributedString:imageString];
43
+
44
+ // Extract alt text from children (![alt text](url) - "alt text" is in children)
45
+ NSString *altText = [self extractTextFromNode:node];
46
+ NSRange imageRange = NSMakeRange(startIndex, output.length - startIndex);
47
+ [context registerImageRange:imageRange altText:altText url:imageURL];
44
48
  }
45
49
 
46
50
  #pragma mark - Private Helpers
47
51
 
52
+ - (NSString *)extractTextFromNode:(MarkdownASTNode *)node
53
+ {
54
+ if (!node)
55
+ return @"";
56
+
57
+ NSMutableString *buffer = [NSMutableString string];
58
+ [self _appendChildTextFromNode:node toBuffer:buffer];
59
+ return [buffer copy];
60
+ }
61
+
62
+ - (void)_appendChildTextFromNode:(MarkdownASTNode *)node toBuffer:(NSMutableString *)buffer
63
+ {
64
+ if (node.content.length > 0) {
65
+ [buffer appendString:node.content];
66
+ }
67
+
68
+ for (MarkdownASTNode *child in node.children) {
69
+ [self _appendChildTextFromNode:child toBuffer:buffer];
70
+ }
71
+ }
72
+
48
73
  - (BOOL)isInlineImageInOutput:(NSAttributedString *)output
49
74
  {
50
75
  if (output.length == 0) {
51
76
  return NO;
52
77
  }
53
78
 
54
- // Check the last character to see if we are currently mid-paragraph
55
79
  unichar lastChar = [output.string characterAtIndex:output.length - 1];
56
-
57
- // If the last character is a newline or a zero-width space (often used as block separators),
58
- // we consider the next image to be a "block" image.
59
80
  return (lastChar != kLineBreak && lastChar != kZeroWidthSpace);
60
81
  }
61
82
 
@@ -28,63 +28,75 @@ NSString *const ListItemNumberAttribute = @"ListItemNumber";
28
28
  if (!context)
29
29
  return;
30
30
 
31
- // 1. Maintain the ordered list counter
32
- if (context.listType == ListTypeOrdered) {
33
- context.listItemNumber++;
34
- }
31
+ context.listItemNumber++;
32
+ const NSInteger currentPosition = context.listItemNumber;
33
+ const NSInteger currentDepth = context.listDepth; // 1-based (1 = top level)
34
+
35
+ const NSUInteger startLocation = output.length;
35
36
 
36
- NSUInteger start = output.length;
37
+ // Render the actual content of the list item (text, bolding, etc.)
37
38
  [_rendererFactory renderChildrenOfNode:node into:output context:context];
38
39
 
39
- // 2. Structural Fix: Ensure paragraph isolation to prevent merged lines
40
- if (output.length > start && ![output.string hasSuffix:@"\n"]) {
40
+ // Ensure every list item ends with a newline to prevent paragraph merging
41
+ if (output.length > startLocation && ![output.string hasSuffix:@"\n"]) {
41
42
  [output appendAttributedString:kNewlineAttributedString];
42
43
  }
43
44
 
44
- NSRange itemRange = NSMakeRange(start, output.length - start);
45
+ const NSRange itemRange = NSMakeRange(startLocation, output.length - startLocation);
45
46
  if (itemRange.length == 0)
46
47
  return;
47
48
 
48
- // 3. Pre-calculate invariant metadata for this item
49
- NSInteger currentDepth = context.listDepth - 1;
50
- // Root items: just marker width + gap (flush to left edge)
51
- // Nested items: add marginLeft for each nesting level
52
- CGFloat minMarkerWidth = (context.listType == ListTypeOrdered) ? [_config effectiveListMarginLeftForNumber]
53
- : [_config effectiveListMarginLeftForBullet];
54
- CGFloat indent = minMarkerWidth + [_config effectiveListGapWidth] + (currentDepth * [_config listStyleMarginLeft]);
55
- CGFloat configLineHeight = [_config listStyleLineHeight];
56
-
57
- // Pre-wrap numbers to avoid repeated allocations in the block
58
- NSNumber *depthVal = @(currentDepth);
59
- NSNumber *typeVal = @(context.listType);
60
- NSNumber *numVal = @(context.listItemNumber);
61
-
62
- // 4. Protected Styling: Use enumerateAttribute to avoid flattening children
49
+ // Informs MarkdownAccessibilityElementBuilder about the specific boundaries of this list item
50
+ [context registerListItemRange:itemRange
51
+ position:currentPosition
52
+ depth:currentDepth
53
+ isOrdered:(context.listType == ListTypeOrdered)];
54
+
55
+ // currentDepth - 1 handles the horizontal offset for nested lists
56
+ const NSInteger nestingLevel = currentDepth - 1;
57
+ const CGFloat baseMarkerWidth = (context.listType == ListTypeOrdered) ? [_config effectiveListMarginLeftForNumber]
58
+ : [_config effectiveListMarginLeftForBullet];
59
+
60
+ const CGFloat totalIndent =
61
+ baseMarkerWidth + [_config effectiveListGapWidth] + (nestingLevel * [_config listStyleMarginLeft]);
62
+
63
+ const CGFloat lineHeightConfig = [_config listStyleLineHeight];
64
+
65
+ // Boxing metadata for attributed string storage
66
+ NSDictionary *metadata = @{
67
+ ListDepthAttribute : @(nestingLevel),
68
+ ListTypeAttribute : @(context.listType),
69
+ ListItemNumberAttribute : @(currentPosition)
70
+ };
71
+
72
+ // We enumerate to ensure we don't overwrite styles of nested sub-lists
73
+ // that may have already been rendered inside this item.
63
74
  [output enumerateAttribute:ListDepthAttribute
64
75
  inRange:itemRange
65
76
  options:0
66
- usingBlock:^(id depth, NSRange range, BOOL *stop) {
67
- // Skip if this segment belongs to a deeper nested list
68
- if (depth && [depth integerValue] > currentDepth)
77
+ usingBlock:^(id depthAttr, NSRange range, BOOL *stop) {
78
+ // If a segment already has a Depth attribute higher than our current level,
79
+ // it belongs to a nested list and we should skip it to preserve its styling.
80
+ if (depthAttr && [depthAttr integerValue] > nestingLevel) {
69
81
  return;
82
+ }
70
83
 
71
84
  NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
72
- style.firstLineHeadIndent = indent;
73
- style.headIndent = indent;
85
+ style.firstLineHeadIndent = totalIndent;
86
+ style.headIndent = totalIndent;
74
87
 
75
- UIFont *font = [output attribute:NSFontAttributeName atIndex:range.location effectiveRange:NULL];
76
- if (configLineHeight > 0 && font) {
77
- style.lineHeightMultiple = configLineHeight / font.pointSize;
88
+ // Apply line height if configured
89
+ UIFont *currentFont = [output attribute:NSFontAttributeName
90
+ atIndex:range.location
91
+ effectiveRange:NULL];
92
+ if (lineHeightConfig > 0 && currentFont) {
93
+ style.lineHeightMultiple = lineHeightConfig / currentFont.pointSize;
78
94
  }
79
95
 
80
- // Final attribute application
81
- [output addAttributes:@{
82
- NSParagraphStyleAttributeName : style,
83
- ListDepthAttribute : depthVal,
84
- ListTypeAttribute : typeVal,
85
- ListItemNumberAttribute : numVal
86
- }
87
- range:range];
96
+ NSMutableDictionary *attributesToApply = [metadata mutableCopy];
97
+ attributesToApply[NSParagraphStyleAttributeName] = style;
98
+
99
+ [output addAttributes:attributesToApply range:range];
88
100
  }];
89
101
  }
90
102
 
@@ -27,40 +27,43 @@
27
27
  if (!context)
28
28
  return;
29
29
 
30
- // Snapshot parent state
31
- NSInteger prevDepth = context.listDepth;
32
- ListType prevType = context.listType;
33
- NSInteger prevNum = context.listItemNumber;
30
+ const NSInteger prevDepth = context.listDepth;
31
+ const ListType prevType = context.listType;
32
+ const NSInteger prevNum = context.listItemNumber;
34
33
 
35
- // Configure depth and type
36
- context.listDepth = prevDepth + 1;
37
- context.listType = _isOrdered ? ListTypeOrdered : ListTypeUnordered;
38
- context.listItemNumber = 0;
34
+ const NSUInteger startLocation = output.length;
35
+ NSUInteger contentStart = startLocation;
39
36
 
40
- // Ensure isolation for nested lists
41
- if (prevDepth > 0 && output.length > 0 && ![output.string hasSuffix:@"\n"]) {
37
+ if (prevDepth == 0) {
38
+ // Apply top margin for root-level list
39
+ contentStart += applyBlockSpacingBefore(output, startLocation, _config.listStyleMarginTop);
40
+ } else if (output.length > 0 && ![output.string hasSuffix:@"\n"]) {
41
+ // Ensure nested lists start on a new line
42
42
  [output appendAttributedString:kNewlineAttributedString];
43
43
  }
44
44
 
45
+ context.listDepth = prevDepth + 1;
46
+ context.listType = _isOrdered ? ListTypeOrdered : ListTypeUnordered;
47
+ context.listItemNumber = 0; // Reset counter for this specific list level
48
+
45
49
  [context setBlockStyle:_isOrdered ? BlockTypeOrderedList : BlockTypeUnorderedList
46
- font:[_config listStyleFont]
47
- color:[_config listStyleColor]
50
+ font:_config.listStyleFont
51
+ color:_config.listStyleColor
48
52
  headingLevel:0];
49
53
 
50
54
  @try {
51
55
  [_rendererFactory renderChildrenOfNode:node into:output context:context];
52
56
  } @finally {
53
- // Restore parent state
54
57
  context.listDepth = prevDepth;
55
58
  context.listType = prevType;
56
59
  context.listItemNumber = prevNum;
57
- if (prevDepth == 0)
60
+
61
+ if (prevDepth == 0) {
58
62
  [context clearBlockStyle];
59
- }
60
63
 
61
- // Final spacing for root container
62
- if (prevDepth == 0 && [_config listStyleMarginBottom] > 0) {
63
- applyBlockSpacing(output, [_config listStyleMarginBottom]);
64
+ // Apply bottom margin for root-level list
65
+ applyBlockSpacingAfter(output, _config.listStyleMarginBottom);
66
+ }
64
67
  }
65
68
  }
66
69