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.
- package/README.md +80 -8
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +17 -2
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +6 -1
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +9 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +6 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +28 -3
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +225 -1
- package/android/src/main/cpp/jni-adapter.cpp +28 -11
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +132 -15
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +1 -16
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +67 -13
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +241 -21
- package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/MarkdownAccessibilityHelper.kt +279 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLongPressEvent.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +2 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +17 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +13 -18
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +23 -24
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +18 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +22 -6
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +3 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +16 -9
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +23 -9
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +24 -10
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/UnderlineRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +8 -17
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +19 -5
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrikethroughSpan.kt +12 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +3 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrikethroughStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +32 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +22 -5
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/TextAlignment.kt +32 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/UnderlineStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +23 -5
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/LinkLongPressMovementMethod.kt +121 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +58 -56
- package/android/src/main/jni/CMakeLists.txt +1 -13
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +0 -13
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +2 -14
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +3 -0
- package/cpp/parser/MD4CParser.cpp +21 -8
- package/cpp/parser/MD4CParser.hpp +5 -1
- package/cpp/parser/MarkdownASTNode.hpp +2 -0
- package/ios/EnrichedMarkdownText.mm +356 -29
- package/ios/attachments/{ImageAttachment.h → EnrichedMarkdownImageAttachment.h} +1 -1
- package/ios/attachments/{ImageAttachment.m → EnrichedMarkdownImageAttachment.m} +4 -4
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +9 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +6 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +28 -3
- package/ios/generated/EnrichedMarkdownTextSpec/Props.h +225 -1
- package/ios/parser/MarkdownASTNode.h +2 -0
- package/ios/parser/MarkdownParser.h +9 -0
- package/ios/parser/MarkdownParser.mm +31 -2
- package/ios/parser/MarkdownParserBridge.mm +13 -3
- package/ios/renderer/AttributedRenderer.h +2 -0
- package/ios/renderer/AttributedRenderer.m +52 -19
- package/ios/renderer/BlockquoteRenderer.m +7 -6
- package/ios/renderer/CodeBlockRenderer.m +9 -8
- package/ios/renderer/HeadingRenderer.m +31 -24
- package/ios/renderer/ImageRenderer.m +31 -10
- package/ios/renderer/ListItemRenderer.m +51 -39
- package/ios/renderer/ListRenderer.m +21 -18
- package/ios/renderer/ParagraphRenderer.m +27 -16
- package/ios/renderer/RenderContext.h +17 -0
- package/ios/renderer/RenderContext.m +66 -2
- package/ios/renderer/RendererFactory.m +6 -0
- package/ios/renderer/StrikethroughRenderer.h +6 -0
- package/ios/renderer/StrikethroughRenderer.m +40 -0
- package/ios/renderer/UnderlineRenderer.h +6 -0
- package/ios/renderer/UnderlineRenderer.m +39 -0
- package/ios/styles/StyleConfig.h +46 -0
- package/ios/styles/StyleConfig.mm +351 -12
- package/ios/utils/AccessibilityInfo.h +35 -0
- package/ios/utils/AccessibilityInfo.m +24 -0
- package/ios/utils/CodeBlockBackground.m +4 -9
- package/ios/utils/FontUtils.h +5 -0
- package/ios/utils/FontUtils.m +14 -0
- package/ios/utils/HTMLGenerator.m +21 -7
- package/ios/utils/MarkdownAccessibilityElementBuilder.h +45 -0
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +323 -0
- package/ios/utils/MarkdownExtractor.m +18 -5
- package/ios/utils/ParagraphStyleUtils.h +10 -2
- package/ios/utils/ParagraphStyleUtils.m +57 -2
- package/ios/utils/PasteboardUtils.h +1 -1
- package/ios/utils/PasteboardUtils.m +3 -3
- package/lib/module/EnrichedMarkdownText.js +33 -2
- package/lib/module/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +83 -3
- package/lib/module/index.js +0 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.js +58 -14
- package/lib/module/normalizeMarkdownStyle.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownText.d.ts +85 -3
- package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +75 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedMarkdownText.tsx +133 -5
- package/src/EnrichedMarkdownTextNativeComponent.ts +83 -3
- package/src/index.tsx +5 -2
- package/src/normalizeMarkdownStyle.ts +46 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +0 -9
- 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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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:&
|
|
101
|
+
effectiveRange:&styleRange];
|
|
102
|
+
|
|
82
103
|
if (style) {
|
|
83
|
-
NSMutableParagraphStyle *
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
104
|
+
NSMutableParagraphStyle *mutableStyle = [style mutableCopy];
|
|
105
|
+
mutableStyle.paragraphSpacing = 0;
|
|
106
|
+
mutableStyle.paragraphSpacingBefore = 0;
|
|
107
|
+
|
|
87
108
|
if (isLastElementImage(output)) {
|
|
88
|
-
|
|
109
|
+
mutableStyle.lineSpacing = 0;
|
|
89
110
|
}
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
84
|
+
// External Margin: Applied outside the background range
|
|
84
85
|
if (marginBottom > 0) {
|
|
85
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
76
|
-
break;
|
|
89
|
+
return (HeadingStyle){c.h1Font, c.h1Color, c.h1MarginTop, c.h1MarginBottom, c.h1LineHeight, c.h1TextAlign};
|
|
77
90
|
case 2:
|
|
78
|
-
|
|
79
|
-
break;
|
|
91
|
+
return (HeadingStyle){c.h2Font, c.h2Color, c.h2MarginTop, c.h2MarginBottom, c.h2LineHeight, c.h2TextAlign};
|
|
80
92
|
case 3:
|
|
81
|
-
|
|
82
|
-
break;
|
|
93
|
+
return (HeadingStyle){c.h3Font, c.h3Color, c.h3MarginTop, c.h3MarginBottom, c.h3LineHeight, c.h3TextAlign};
|
|
83
94
|
case 4:
|
|
84
|
-
|
|
85
|
-
break;
|
|
95
|
+
return (HeadingStyle){c.h4Font, c.h4Color, c.h4MarginTop, c.h4MarginBottom, c.h4LineHeight, c.h4TextAlign};
|
|
86
96
|
case 5:
|
|
87
|
-
|
|
88
|
-
break;
|
|
97
|
+
return (HeadingStyle){c.h5Font, c.h5Color, c.h5MarginTop, c.h5MarginBottom, c.h5LineHeight, c.h5TextAlign};
|
|
89
98
|
case 6:
|
|
90
|
-
|
|
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 "
|
|
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
|
-
|
|
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" 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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
+
// Render the actual content of the list item (text, bolding, etc.)
|
|
37
38
|
[_rendererFactory renderChildrenOfNode:node into:output context:context];
|
|
38
39
|
|
|
39
|
-
//
|
|
40
|
-
if (output.length >
|
|
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(
|
|
45
|
+
const NSRange itemRange = NSMakeRange(startLocation, output.length - startLocation);
|
|
45
46
|
if (itemRange.length == 0)
|
|
46
47
|
return;
|
|
47
48
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
67
|
-
//
|
|
68
|
-
|
|
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 =
|
|
73
|
-
style.headIndent =
|
|
85
|
+
style.firstLineHeadIndent = totalIndent;
|
|
86
|
+
style.headIndent = totalIndent;
|
|
74
87
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
[
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
context.listType = _isOrdered ? ListTypeOrdered : ListTypeUnordered;
|
|
38
|
-
context.listItemNumber = 0;
|
|
34
|
+
const NSUInteger startLocation = output.length;
|
|
35
|
+
NSUInteger contentStart = startLocation;
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
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:
|
|
47
|
-
color:
|
|
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
|
-
|
|
60
|
+
|
|
61
|
+
if (prevDepth == 0) {
|
|
58
62
|
[context clearBlockStyle];
|
|
59
|
-
}
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
// Apply bottom margin for root-level list
|
|
65
|
+
applyBlockSpacingAfter(output, _config.listStyleMarginBottom);
|
|
66
|
+
}
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
|