react-native-enriched-markdown 0.3.0 → 0.4.0-nightly-20260228-6747e87e4
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/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +45 -54
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +3 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt +7 -23
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +1 -1
- package/ios/EnrichedMarkdownText.mm +1 -1
- package/ios/attachments/{EnrichedMarkdownImageAttachment.h → ENRMImageAttachment.h} +1 -1
- package/ios/attachments/ENRMImageAttachment.m +271 -0
- package/ios/renderer/{ImageRenderer.h → ENRMImageRenderer.h} +1 -1
- package/ios/renderer/{ImageRenderer.m → ENRMImageRenderer.m} +6 -10
- package/ios/renderer/RendererFactory.m +2 -2
- package/ios/utils/HTMLGenerator.m +3 -3
- package/ios/utils/MarkdownExtractor.m +3 -3
- package/ios/utils/PasteboardUtils.h +1 -1
- package/ios/utils/PasteboardUtils.m +3 -3
- package/package.json +1 -1
- package/ios/attachments/EnrichedMarkdownImageAttachment.m +0 -185
|
@@ -24,35 +24,18 @@ data class BlockStyle(
|
|
|
24
24
|
val color: Int,
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
private class
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
var isDirty: Boolean = false
|
|
33
|
-
|
|
34
|
-
fun updateFrom(style: BaseBlockStyle) {
|
|
35
|
-
fontSize = style.fontSize
|
|
36
|
-
fontFamily = style.fontFamily
|
|
37
|
-
fontWeight = style.fontWeight
|
|
38
|
-
color = style.color
|
|
39
|
-
isDirty = true
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
fun toImmutable(): BlockStyle = BlockStyle(fontSize, fontFamily, fontWeight, color)
|
|
43
|
-
|
|
44
|
-
fun clear() {
|
|
45
|
-
isDirty = false
|
|
46
|
-
}
|
|
47
|
-
}
|
|
27
|
+
private data class BlockStyleEntry(
|
|
28
|
+
val blockType: BlockType,
|
|
29
|
+
val blockStyle: BlockStyle,
|
|
30
|
+
val headingLevel: Int,
|
|
31
|
+
)
|
|
48
32
|
|
|
49
33
|
class BlockStyleContext {
|
|
50
34
|
var currentBlockType = BlockType.NONE
|
|
51
35
|
private set
|
|
52
36
|
|
|
53
|
-
private val mutableBlockStyle = MutableBlockStyle()
|
|
54
|
-
private var cachedBlockStyle: BlockStyle? = null
|
|
55
37
|
private var currentHeadingLevel = 0
|
|
38
|
+
private val blockStyleStack = ArrayDeque<BlockStyleEntry>()
|
|
56
39
|
|
|
57
40
|
var blockquoteDepth = 0
|
|
58
41
|
var listDepth = 0
|
|
@@ -64,37 +47,58 @@ class BlockStyleContext {
|
|
|
64
47
|
|
|
65
48
|
enum class ListType { UNORDERED, ORDERED }
|
|
66
49
|
|
|
67
|
-
private fun
|
|
50
|
+
private fun pushBlockStyle(
|
|
68
51
|
type: BlockType,
|
|
69
52
|
style: BaseBlockStyle,
|
|
70
53
|
headingLevel: Int = 0,
|
|
71
54
|
) {
|
|
55
|
+
val entry =
|
|
56
|
+
BlockStyleEntry(
|
|
57
|
+
blockType = type,
|
|
58
|
+
blockStyle = BlockStyle(style.fontSize, style.fontFamily, style.fontWeight, style.color),
|
|
59
|
+
headingLevel = headingLevel,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
blockStyleStack.addLast(entry)
|
|
72
63
|
currentBlockType = type
|
|
73
64
|
currentHeadingLevel = headingLevel
|
|
74
|
-
mutableBlockStyle.updateFrom(style)
|
|
75
|
-
cachedBlockStyle = null
|
|
76
65
|
}
|
|
77
66
|
|
|
78
|
-
fun
|
|
67
|
+
fun popBlockStyle() {
|
|
68
|
+
if (blockStyleStack.isNotEmpty()) {
|
|
69
|
+
blockStyleStack.removeLast()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
val parentStyle = blockStyleStack.lastOrNull()
|
|
73
|
+
if (parentStyle != null) {
|
|
74
|
+
currentBlockType = parentStyle.blockType
|
|
75
|
+
currentHeadingLevel = parentStyle.headingLevel
|
|
76
|
+
} else {
|
|
77
|
+
currentBlockType = BlockType.NONE
|
|
78
|
+
currentHeadingLevel = 0
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fun setParagraphStyle(style: ParagraphStyle) = pushBlockStyle(BlockType.PARAGRAPH, style)
|
|
79
83
|
|
|
80
84
|
fun setHeadingStyle(
|
|
81
85
|
style: HeadingStyle,
|
|
82
86
|
level: Int,
|
|
83
|
-
) =
|
|
87
|
+
) = pushBlockStyle(BlockType.HEADING, style, level)
|
|
84
88
|
|
|
85
|
-
fun setBlockquoteStyle(style: BlockquoteStyle) =
|
|
89
|
+
fun setBlockquoteStyle(style: BlockquoteStyle) = pushBlockStyle(BlockType.BLOCKQUOTE, style)
|
|
86
90
|
|
|
87
91
|
fun setUnorderedListStyle(style: ListStyle) {
|
|
88
92
|
listType = ListType.UNORDERED
|
|
89
|
-
|
|
93
|
+
pushBlockStyle(BlockType.UNORDERED_LIST, style)
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
fun setOrderedListStyle(style: ListStyle) {
|
|
93
97
|
listType = ListType.ORDERED
|
|
94
|
-
|
|
98
|
+
pushBlockStyle(BlockType.ORDERED_LIST, style)
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
fun setCodeBlockStyle(style: CodeBlockStyle) =
|
|
101
|
+
fun setCodeBlockStyle(style: CodeBlockStyle) = pushBlockStyle(BlockType.CODE_BLOCK, style)
|
|
98
102
|
|
|
99
103
|
fun isInsideBlockElement(): Boolean = blockquoteDepth > 0 || listDepth > 0
|
|
100
104
|
|
|
@@ -117,39 +121,26 @@ class BlockStyleContext {
|
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
fun clearListStyle() {
|
|
124
|
+
popBlockStyle()
|
|
125
|
+
|
|
120
126
|
if (listDepth == 0) {
|
|
121
|
-
|
|
127
|
+
listType = null
|
|
128
|
+
listItemNumber = 0
|
|
129
|
+
orderedListItemNumbers.clear()
|
|
122
130
|
}
|
|
123
131
|
}
|
|
124
132
|
|
|
125
|
-
private fun reset() {
|
|
126
|
-
clearBlockStyle()
|
|
127
|
-
listType = null
|
|
128
|
-
listItemNumber = 0
|
|
129
|
-
orderedListItemNumbers.clear()
|
|
130
|
-
}
|
|
131
|
-
|
|
132
133
|
fun requireBlockStyle(): BlockStyle {
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
val entry = blockStyleStack.lastOrNull()
|
|
135
|
+
return entry?.blockStyle
|
|
136
|
+
?: throw IllegalStateException(
|
|
135
137
|
"BlockStyle is null. Inline renderers must be used within a block context.",
|
|
136
138
|
)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return cachedBlockStyle ?: mutableBlockStyle.toImmutable().also { cachedBlockStyle = it }
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
fun clearBlockStyle() {
|
|
143
|
-
currentBlockType = BlockType.NONE
|
|
144
|
-
mutableBlockStyle.clear()
|
|
145
|
-
cachedBlockStyle = null
|
|
146
|
-
currentHeadingLevel = 0
|
|
147
139
|
}
|
|
148
140
|
|
|
149
141
|
fun resetForNewRender() {
|
|
142
|
+
blockStyleStack.clear()
|
|
150
143
|
currentBlockType = BlockType.NONE
|
|
151
|
-
mutableBlockStyle.clear()
|
|
152
|
-
cachedBlockStyle = null
|
|
153
144
|
currentHeadingLevel = 0
|
|
154
145
|
blockquoteDepth = 0
|
|
155
146
|
listDepth = 0
|
|
@@ -23,12 +23,13 @@ class HeadingRenderer(
|
|
|
23
23
|
val start = builder.length
|
|
24
24
|
|
|
25
25
|
val headingStyle = config.style.headingStyles[level]!!
|
|
26
|
-
factory.blockStyleContext
|
|
26
|
+
val blockStyleContext = factory.blockStyleContext
|
|
27
|
+
blockStyleContext.setHeadingStyle(headingStyle, level)
|
|
27
28
|
|
|
28
29
|
try {
|
|
29
30
|
factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
|
|
30
31
|
} finally {
|
|
31
|
-
|
|
32
|
+
blockStyleContext.popBlockStyle()
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
val end = builder.length
|
package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package com.swmansion.enriched.markdown.renderer
|
|
2
2
|
|
|
3
3
|
import com.swmansion.enriched.markdown.styles.ListStyle
|
|
4
|
-
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Manages list context transitions (entering/exiting lists, handling nesting, etc.).
|
|
@@ -16,7 +15,6 @@ import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
|
16
15
|
*/
|
|
17
16
|
class ListContextManager(
|
|
18
17
|
private val context: BlockStyleContext,
|
|
19
|
-
private val styleConfig: StyleConfig,
|
|
20
18
|
) {
|
|
21
19
|
/**
|
|
22
20
|
* Captures the state when entering a list, needed for proper restoration when exiting.
|
|
@@ -37,7 +35,7 @@ class ListContextManager(
|
|
|
37
35
|
*/
|
|
38
36
|
fun enterList(
|
|
39
37
|
listType: BlockStyleContext.ListType,
|
|
40
|
-
style:
|
|
38
|
+
style: ListStyle,
|
|
41
39
|
): ListEntryState {
|
|
42
40
|
val previousDepth = context.listDepth
|
|
43
41
|
val isNested = previousDepth > 0
|
|
@@ -53,11 +51,11 @@ class ListContextManager(
|
|
|
53
51
|
context.listDepth = previousDepth + 1
|
|
54
52
|
when (listType) {
|
|
55
53
|
BlockStyleContext.ListType.ORDERED -> {
|
|
56
|
-
context.setOrderedListStyle(style
|
|
54
|
+
context.setOrderedListStyle(style)
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
BlockStyleContext.ListType.UNORDERED -> {
|
|
60
|
-
context.setUnorderedListStyle(style
|
|
58
|
+
context.setUnorderedListStyle(style)
|
|
61
59
|
}
|
|
62
60
|
}
|
|
63
61
|
context.resetListItemNumber()
|
|
@@ -71,35 +69,21 @@ class ListContextManager(
|
|
|
71
69
|
|
|
72
70
|
/**
|
|
73
71
|
* Exits a list context. Handles:
|
|
74
|
-
* -
|
|
72
|
+
* - Popping current list block style from stack
|
|
75
73
|
* - Decrementing list depth back to previousDepth
|
|
76
74
|
* - Restoring parent list item numbers from stack (if applicable)
|
|
77
|
-
* - Restoring parent list
|
|
75
|
+
* - Restoring parent list metadata (if nested) so subsequent parent items render correctly
|
|
78
76
|
*/
|
|
79
77
|
fun exitList(entryState: ListEntryState) {
|
|
80
|
-
context.clearListStyle()
|
|
81
78
|
context.listDepth = entryState.previousDepth
|
|
79
|
+
context.clearListStyle()
|
|
82
80
|
|
|
83
81
|
if (entryState.wasNestedInOrderedList) {
|
|
84
82
|
context.popOrderedListItemNumber()
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
if (entryState.previousDepth > 0) {
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private fun restoreParentListContext(parentListType: BlockStyleContext.ListType?) {
|
|
93
|
-
when (parentListType) {
|
|
94
|
-
BlockStyleContext.ListType.UNORDERED -> {
|
|
95
|
-
context.setUnorderedListStyle(styleConfig.listStyle)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
BlockStyleContext.ListType.ORDERED -> {
|
|
99
|
-
context.setOrderedListStyle(styleConfig.listStyle)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
null -> {}
|
|
86
|
+
context.listType = entryState.parentListType
|
|
103
87
|
}
|
|
104
88
|
}
|
|
105
89
|
}
|
|
@@ -22,7 +22,7 @@ class ListRenderer(
|
|
|
22
22
|
val listStyle = config.style.listStyle
|
|
23
23
|
val listType = if (isOrdered) BlockStyleContext.ListType.ORDERED else BlockStyleContext.ListType.UNORDERED
|
|
24
24
|
|
|
25
|
-
val contextManager = ListContextManager(factory.blockStyleContext
|
|
25
|
+
val contextManager = ListContextManager(factory.blockStyleContext)
|
|
26
26
|
val entryState = contextManager.enterList(listType, listStyle)
|
|
27
27
|
|
|
28
28
|
// For top-level lists, insert marginTop spacer before rendering content
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
#import "AccessibilityInfo.h"
|
|
3
3
|
#import "AttributedRenderer.h"
|
|
4
4
|
#import "CodeBlockBackground.h"
|
|
5
|
+
#import "ENRMImageAttachment.h"
|
|
5
6
|
#import "ENRMMarkdownParser.h"
|
|
6
7
|
#import "EditMenuUtils.h"
|
|
7
|
-
#import "EnrichedMarkdownImageAttachment.h"
|
|
8
8
|
#import "FontScaleObserver.h"
|
|
9
9
|
#import "FontUtils.h"
|
|
10
10
|
#import "LastElementUtils.h"
|
|
@@ -11,7 +11,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
11
11
|
* Images are loaded asynchronously and scaled dynamically based on text container width.
|
|
12
12
|
* Supports inline and block images with custom height and border radius from config.
|
|
13
13
|
*/
|
|
14
|
-
@interface
|
|
14
|
+
@interface ENRMImageAttachment : NSTextAttachment
|
|
15
15
|
|
|
16
16
|
@property (nonatomic, readonly) NSString *imageURL;
|
|
17
17
|
@property (nonatomic, readonly) BOOL isInline;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#import "ENRMImageAttachment.h"
|
|
2
|
+
#import "RuntimeKeys.h"
|
|
3
|
+
#import "StyleConfig.h"
|
|
4
|
+
#import <React/RCTLog.h>
|
|
5
|
+
#import <objc/runtime.h>
|
|
6
|
+
|
|
7
|
+
@interface ENRMImageAttachment ()
|
|
8
|
+
|
|
9
|
+
@property (nonatomic, readwrite) NSString *imageURL;
|
|
10
|
+
@property (nonatomic, weak) StyleConfig *styleConfiguration;
|
|
11
|
+
@property (nonatomic, assign) BOOL isInline;
|
|
12
|
+
@property (nonatomic, assign) CGFloat cachedHeight;
|
|
13
|
+
@property (nonatomic, assign) CGFloat cachedBorderRadius;
|
|
14
|
+
@property (nonatomic, weak) NSTextContainer *textContainer;
|
|
15
|
+
@property (nonatomic, weak) UITextView *textView;
|
|
16
|
+
@property (nonatomic, strong) UIImage *originalImage;
|
|
17
|
+
@property (nonatomic, strong) UIImage *loadedImage;
|
|
18
|
+
@property (nonatomic, strong) NSURLSessionDataTask *loadingTask;
|
|
19
|
+
|
|
20
|
+
@end
|
|
21
|
+
|
|
22
|
+
@implementation ENRMImageAttachment
|
|
23
|
+
|
|
24
|
+
- (instancetype)initWithImageURL:(NSString *)imageURL config:(StyleConfig *)config isInline:(BOOL)isInline
|
|
25
|
+
{
|
|
26
|
+
self = [super init];
|
|
27
|
+
if (self) {
|
|
28
|
+
_imageURL = imageURL;
|
|
29
|
+
_styleConfiguration = config;
|
|
30
|
+
_isInline = isInline;
|
|
31
|
+
|
|
32
|
+
_cachedHeight = isInline ? [config inlineImageSize] : [config imageHeight];
|
|
33
|
+
_cachedBorderRadius = [config imageBorderRadius];
|
|
34
|
+
|
|
35
|
+
[self setupPlaceholder];
|
|
36
|
+
[self startDownloadingImage];
|
|
37
|
+
}
|
|
38
|
+
return self;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
- (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer
|
|
42
|
+
proposedLineFragment:(CGRect)lineFragment
|
|
43
|
+
glyphPosition:(CGPoint)position
|
|
44
|
+
characterIndex:(NSUInteger)characterIndex
|
|
45
|
+
{
|
|
46
|
+
CGFloat height = self.cachedHeight;
|
|
47
|
+
CGFloat width = self.isInline ? height : (lineFragment.size.width > 0 ? lineFragment.size.width : height);
|
|
48
|
+
|
|
49
|
+
if (self.isInline) {
|
|
50
|
+
UIFont *appliedFont = nil;
|
|
51
|
+
NSLayoutManager *layoutManager = textContainer.layoutManager;
|
|
52
|
+
NSTextStorage *textStorage = layoutManager.textStorage;
|
|
53
|
+
|
|
54
|
+
if (textStorage && characterIndex < textStorage.length) {
|
|
55
|
+
appliedFont = [textStorage attribute:NSFontAttributeName atIndex:characterIndex effectiveRange:NULL];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Determine the vertical alignment:
|
|
59
|
+
// Center against the font's Capital Height if available,
|
|
60
|
+
// otherwise center within the line fragment.
|
|
61
|
+
CGFloat verticalOffset;
|
|
62
|
+
if (appliedFont) {
|
|
63
|
+
verticalOffset = (appliedFont.capHeight - height) / 2.0;
|
|
64
|
+
} else {
|
|
65
|
+
verticalOffset = (lineFragment.size.height - height) / 2.0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return CGRectMake(0, verticalOffset, width, height);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return CGRectMake(0, 0, width, height);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
- (UIImage *)imageForBounds:(CGRect)imageBounds
|
|
75
|
+
textContainer:(NSTextContainer *)textContainer
|
|
76
|
+
characterIndex:(NSUInteger)characterIndex
|
|
77
|
+
{
|
|
78
|
+
self.textContainer = textContainer;
|
|
79
|
+
|
|
80
|
+
if (self.originalImage && imageBounds.size.width > 0) {
|
|
81
|
+
CGFloat currentWidth = imageBounds.size.width;
|
|
82
|
+
|
|
83
|
+
BOOL isFirstLoad = (self.loadedImage == nil);
|
|
84
|
+
BOOL hasWidthChanged = !self.isInline && self.loadedImage && fabs(self.loadedImage.size.width - currentWidth) > 1.0;
|
|
85
|
+
|
|
86
|
+
if (isFirstLoad || hasWidthChanged) {
|
|
87
|
+
self.bounds = imageBounds;
|
|
88
|
+
[self processAndApplyImage:self.originalImage withTargetWidth:currentWidth];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return self.loadedImage ?: self.image;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
- (void)handleLoadedImage:(UIImage *)image
|
|
96
|
+
{
|
|
97
|
+
if (!image) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
self.originalImage = image;
|
|
102
|
+
CGFloat targetWidth = self.isInline ? self.cachedHeight : self.bounds.size.width;
|
|
103
|
+
|
|
104
|
+
// If bounds width isn't known yet (image loaded before layout), defer scaling
|
|
105
|
+
// until imageForBounds: provides the real width via the text system.
|
|
106
|
+
if (!self.isInline && targetWidth <= self.cachedHeight) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
[self processAndApplyImage:image withTargetWidth:targetWidth];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
- (void)processAndApplyImage:(UIImage *)image withTargetWidth:(CGFloat)targetWidth
|
|
114
|
+
{
|
|
115
|
+
if (targetWidth <= 0) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
__weak typeof(self) weakSelf = self;
|
|
120
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
121
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
122
|
+
if (!strongSelf)
|
|
123
|
+
return;
|
|
124
|
+
|
|
125
|
+
UIImage *processedImage = [strongSelf createScaledImage:image
|
|
126
|
+
toWidth:targetWidth
|
|
127
|
+
height:strongSelf.cachedHeight
|
|
128
|
+
borderRadius:strongSelf.cachedBorderRadius];
|
|
129
|
+
|
|
130
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
131
|
+
strongSelf.loadedImage = processedImage;
|
|
132
|
+
|
|
133
|
+
if (strongSelf.isInline) {
|
|
134
|
+
strongSelf.image = processedImage;
|
|
135
|
+
strongSelf.bounds = CGRectMake(0, 0, strongSelf.cachedHeight, strongSelf.cachedHeight);
|
|
136
|
+
} else {
|
|
137
|
+
strongSelf.image = image;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
[strongSelf refreshDisplay];
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
- (UIImage *)createScaledImage:(UIImage *)image
|
|
146
|
+
toWidth:(CGFloat)targetWidth
|
|
147
|
+
height:(CGFloat)targetHeight
|
|
148
|
+
borderRadius:(CGFloat)radius
|
|
149
|
+
{
|
|
150
|
+
CGFloat sourceWidth = image.size.width;
|
|
151
|
+
CGFloat sourceHeight = image.size.height;
|
|
152
|
+
|
|
153
|
+
CGFloat drawingWidth, drawingHeight;
|
|
154
|
+
|
|
155
|
+
if (!self.isInline && sourceWidth > 0 && sourceHeight > 0) {
|
|
156
|
+
CGFloat aspectRatioScale = targetWidth / sourceWidth;
|
|
157
|
+
drawingWidth = targetWidth;
|
|
158
|
+
drawingHeight = sourceHeight * aspectRatioScale;
|
|
159
|
+
} else {
|
|
160
|
+
drawingWidth = targetWidth;
|
|
161
|
+
drawingHeight = targetHeight;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
CGFloat xOffset = (targetWidth - drawingWidth) / 2.0;
|
|
165
|
+
CGFloat yOffset = (targetHeight - drawingHeight) / 2.0;
|
|
166
|
+
CGRect drawingRect = CGRectMake(xOffset, yOffset, drawingWidth, drawingHeight);
|
|
167
|
+
|
|
168
|
+
UIGraphicsImageRenderer *renderer =
|
|
169
|
+
[[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(targetWidth, targetHeight)];
|
|
170
|
+
|
|
171
|
+
return [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) {
|
|
172
|
+
if (radius > 0) {
|
|
173
|
+
CGRect clippingRect = CGRectIntersection(CGRectMake(0, 0, targetWidth, targetHeight), drawingRect);
|
|
174
|
+
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:clippingRect cornerRadius:radius];
|
|
175
|
+
[path addClip];
|
|
176
|
+
}
|
|
177
|
+
[image drawInRect:drawingRect];
|
|
178
|
+
}];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
- (void)refreshDisplay
|
|
182
|
+
{
|
|
183
|
+
UITextView *textView = [self fetchAssociatedTextView];
|
|
184
|
+
if (!textView) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
NSRange attachmentRange = [self findAttachmentRangeInText:textView.attributedText];
|
|
189
|
+
if (attachmentRange.location == NSNotFound) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
[textView.layoutManager invalidateDisplayForCharacterRange:attachmentRange];
|
|
194
|
+
if (!self.isInline) {
|
|
195
|
+
[textView.layoutManager invalidateLayoutForCharacterRange:attachmentRange actualCharacterRange:NULL];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
- (UITextView *)fetchAssociatedTextView
|
|
200
|
+
{
|
|
201
|
+
if (self.textView) {
|
|
202
|
+
return self.textView;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!self.textContainer) {
|
|
206
|
+
return nil;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Look up the text view via the associated object key stored on the container
|
|
210
|
+
self.textView = objc_getAssociatedObject(self.textContainer, kTextViewKey);
|
|
211
|
+
return self.textView;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
- (void)setupPlaceholder
|
|
215
|
+
{
|
|
216
|
+
CGFloat size = self.cachedHeight;
|
|
217
|
+
self.bounds = CGRectMake(0, 0, size, size);
|
|
218
|
+
|
|
219
|
+
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(size, size)];
|
|
220
|
+
self.image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context){
|
|
221
|
+
// Generates an empty transparent placeholder
|
|
222
|
+
}];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
- (void)startDownloadingImage
|
|
226
|
+
{
|
|
227
|
+
if (self.imageURL.length == 0) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
NSURL *url = [NSURL URLWithString:self.imageURL];
|
|
232
|
+
if (!url) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
__weak typeof(self) weakSelf = self;
|
|
237
|
+
self.loadingTask = [[NSURLSession sharedSession]
|
|
238
|
+
dataTaskWithURL:url
|
|
239
|
+
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
240
|
+
if (data && !error) {
|
|
241
|
+
UIImage *downloadedImage = [UIImage imageWithData:data];
|
|
242
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf handleLoadedImage:downloadedImage]; });
|
|
243
|
+
}
|
|
244
|
+
}];
|
|
245
|
+
|
|
246
|
+
[self.loadingTask resume];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
- (NSRange)findAttachmentRangeInText:(NSAttributedString *)attributedString
|
|
250
|
+
{
|
|
251
|
+
__block NSRange foundRange = NSMakeRange(NSNotFound, 0);
|
|
252
|
+
|
|
253
|
+
[attributedString enumerateAttribute:NSAttachmentAttributeName
|
|
254
|
+
inRange:NSMakeRange(0, attributedString.length)
|
|
255
|
+
options:0
|
|
256
|
+
usingBlock:^(id value, NSRange range, BOOL *stop) {
|
|
257
|
+
if (value == self) {
|
|
258
|
+
foundRange = range;
|
|
259
|
+
*stop = YES;
|
|
260
|
+
}
|
|
261
|
+
}];
|
|
262
|
+
|
|
263
|
+
return foundRange;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
- (void)dealloc
|
|
267
|
+
{
|
|
268
|
+
[_loadingTask cancel];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
#import "
|
|
2
|
-
#import "
|
|
1
|
+
#import "ENRMImageRenderer.h"
|
|
2
|
+
#import "ENRMImageAttachment.h"
|
|
3
3
|
#import "MarkdownASTNode.h"
|
|
4
4
|
#import "RenderContext.h"
|
|
5
5
|
#import "RendererFactory.h"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
static const unichar kLineBreak = '\n';
|
|
9
9
|
static const unichar kZeroWidthSpace = 0x200B;
|
|
10
10
|
|
|
11
|
-
@implementation
|
|
11
|
+
@implementation ENRMImageRenderer {
|
|
12
12
|
RendererFactory *_rendererFactory;
|
|
13
13
|
StyleConfig *_config;
|
|
14
14
|
}
|
|
@@ -22,8 +22,6 @@ static const unichar kZeroWidthSpace = 0x200B;
|
|
|
22
22
|
return self;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
#pragma mark - Rendering
|
|
26
|
-
|
|
27
25
|
- (void)renderNode:(MarkdownASTNode *)node into:(NSMutableAttributedString *)output context:(RenderContext *)context
|
|
28
26
|
{
|
|
29
27
|
NSString *imageURL = node.attributes[@"url"];
|
|
@@ -32,9 +30,9 @@ static const unichar kZeroWidthSpace = 0x200B;
|
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
BOOL isInline = [self isInlineImageInOutput:output];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
ENRMImageAttachment *attachment = [[ENRMImageAttachment alloc] initWithImageURL:imageURL
|
|
34
|
+
config:_config
|
|
35
|
+
isInline:isInline];
|
|
38
36
|
|
|
39
37
|
NSUInteger startIndex = output.length;
|
|
40
38
|
|
|
@@ -47,8 +45,6 @@ static const unichar kZeroWidthSpace = 0x200B;
|
|
|
47
45
|
[context registerImageRange:imageRange altText:altText url:imageURL];
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
#pragma mark - Private Helpers
|
|
51
|
-
|
|
52
48
|
- (NSString *)extractTextFromNode:(MarkdownASTNode *)node
|
|
53
49
|
{
|
|
54
50
|
if (!node)
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
#import "BlockquoteRenderer.h"
|
|
3
3
|
#import "CodeBlockRenderer.h"
|
|
4
4
|
#import "CodeRenderer.h"
|
|
5
|
+
#import "ENRMImageRenderer.h"
|
|
5
6
|
#import "EmphasisRenderer.h"
|
|
6
7
|
#import "HeadingRenderer.h"
|
|
7
|
-
#import "ImageRenderer.h"
|
|
8
8
|
#import "LinkRenderer.h"
|
|
9
9
|
#import "ListItemRenderer.h"
|
|
10
10
|
#import "ListRenderer.h"
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
case MarkdownNodeTypeCode:
|
|
81
81
|
return [[CodeRenderer alloc] initWithRendererFactory:self config:_config];
|
|
82
82
|
case MarkdownNodeTypeImage:
|
|
83
|
-
return [[
|
|
83
|
+
return [[ENRMImageRenderer alloc] initWithRendererFactory:self config:_config];
|
|
84
84
|
case MarkdownNodeTypeBlockquote:
|
|
85
85
|
return [[BlockquoteRenderer alloc] initWithRendererFactory:self config:_config];
|
|
86
86
|
case MarkdownNodeTypeListItem:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#import "HTMLGenerator.h"
|
|
2
2
|
#import "BlockquoteBorder.h"
|
|
3
3
|
#import "CodeBackground.h"
|
|
4
|
-
#import "
|
|
4
|
+
#import "ENRMImageAttachment.h"
|
|
5
5
|
#import "LastElementUtils.h"
|
|
6
6
|
#import "ListItemRenderer.h"
|
|
7
7
|
#import "ParagraphStyleUtils.h"
|
|
@@ -456,8 +456,8 @@ static void generateInlineHTML(NSMutableString *html, NSAttributedString *attrib
|
|
|
456
456
|
|
|
457
457
|
if ([text containsString:kObjectReplacementChar]) {
|
|
458
458
|
id attachment = attrs[NSAttachmentAttributeName];
|
|
459
|
-
if ([attachment isKindOfClass:[
|
|
460
|
-
|
|
459
|
+
if ([attachment isKindOfClass:[ENRMImageAttachment class]]) {
|
|
460
|
+
ENRMImageAttachment *img = (ENRMImageAttachment *)attachment;
|
|
461
461
|
if (img.imageURL) {
|
|
462
462
|
if (img.isInline) {
|
|
463
463
|
[html appendFormat:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#import "MarkdownExtractor.h"
|
|
2
2
|
#import "BlockquoteBorder.h"
|
|
3
|
-
#import "
|
|
3
|
+
#import "ENRMImageAttachment.h"
|
|
4
4
|
#import "LastElementUtils.h"
|
|
5
5
|
#import "ListItemRenderer.h"
|
|
6
6
|
#import "RuntimeKeys.h"
|
|
@@ -136,8 +136,8 @@ NSString *_Nullable extractMarkdownFromAttributedString(NSAttributedString *attr
|
|
|
136
136
|
|
|
137
137
|
// Images and Thematic Breaks
|
|
138
138
|
NSTextAttachment *attachment = attrs[NSAttachmentAttributeName];
|
|
139
|
-
if ([attachment isKindOfClass:[
|
|
140
|
-
|
|
139
|
+
if ([attachment isKindOfClass:[ENRMImageAttachment class]]) {
|
|
140
|
+
ENRMImageAttachment *img = (ENRMImageAttachment *)attachment;
|
|
141
141
|
if (!img.imageURL)
|
|
142
142
|
return;
|
|
143
143
|
|
|
@@ -25,7 +25,7 @@ NSString *_Nullable markdownForRange(NSAttributedString *attributedText, NSRange
|
|
|
25
25
|
NSString *_Nullable cachedMarkdown);
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Returns remote image URLs (http/https only) from
|
|
28
|
+
* Returns remote image URLs (http/https only) from ENRMImageAttachments in the given range.
|
|
29
29
|
*/
|
|
30
30
|
NSArray<NSString *> *imageURLsInRange(NSAttributedString *attributedText, NSRange range);
|
|
31
31
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#import "PasteboardUtils.h"
|
|
2
|
-
#import "
|
|
2
|
+
#import "ENRMImageAttachment.h"
|
|
3
3
|
#import "HTMLGenerator.h"
|
|
4
4
|
#import "MarkdownExtractor.h"
|
|
5
5
|
#import "RTFExportUtils.h"
|
|
@@ -121,10 +121,10 @@ NSArray<NSString *> *imageURLsInRange(NSAttributedString *attributedText, NSRang
|
|
|
121
121
|
inRange:range
|
|
122
122
|
options:0
|
|
123
123
|
usingBlock:^(id value, NSRange r, BOOL *stop) {
|
|
124
|
-
if (![value isKindOfClass:[
|
|
124
|
+
if (![value isKindOfClass:[ENRMImageAttachment class]])
|
|
125
125
|
return;
|
|
126
126
|
|
|
127
|
-
NSString *url = ((
|
|
127
|
+
NSString *url = ((ENRMImageAttachment *)value).imageURL;
|
|
128
128
|
if ([url hasPrefix:@"http://"] || [url hasPrefix:@"https://"]) {
|
|
129
129
|
[urls addObject:url];
|
|
130
130
|
}
|
package/package.json
CHANGED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
#import "EnrichedMarkdownImageAttachment.h"
|
|
2
|
-
#import "RuntimeKeys.h"
|
|
3
|
-
#import "StyleConfig.h"
|
|
4
|
-
#import <React/RCTLog.h>
|
|
5
|
-
#import <objc/runtime.h>
|
|
6
|
-
|
|
7
|
-
@interface EnrichedMarkdownImageAttachment ()
|
|
8
|
-
@property (nonatomic, readwrite) NSString *imageURL;
|
|
9
|
-
@property (nonatomic, weak) StyleConfig *config;
|
|
10
|
-
@property (nonatomic, assign) BOOL isInline;
|
|
11
|
-
@property (nonatomic, assign) CGFloat cachedHeight;
|
|
12
|
-
@property (nonatomic, assign) CGFloat cachedBorderRadius;
|
|
13
|
-
@property (nonatomic, weak) NSTextContainer *textContainer;
|
|
14
|
-
@property (nonatomic, weak) UITextView *textView;
|
|
15
|
-
@property (nonatomic, strong) UIImage *originalImage;
|
|
16
|
-
@property (nonatomic, strong) UIImage *loadedImage;
|
|
17
|
-
@property (nonatomic, strong) NSURLSessionDataTask *loadingTask;
|
|
18
|
-
@end
|
|
19
|
-
|
|
20
|
-
@implementation EnrichedMarkdownImageAttachment
|
|
21
|
-
|
|
22
|
-
- (instancetype)initWithImageURL:(NSString *)imageURL config:(StyleConfig *)config isInline:(BOOL)isInline
|
|
23
|
-
{
|
|
24
|
-
if (self = [super init]) {
|
|
25
|
-
_imageURL = imageURL;
|
|
26
|
-
_config = config;
|
|
27
|
-
_isInline = isInline;
|
|
28
|
-
_cachedHeight = isInline ? [config inlineImageSize] : [config imageHeight];
|
|
29
|
-
_cachedBorderRadius = [config imageBorderRadius];
|
|
30
|
-
|
|
31
|
-
[self setupPlaceholder];
|
|
32
|
-
[self loadImage];
|
|
33
|
-
}
|
|
34
|
-
return self;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
- (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer
|
|
38
|
-
proposedLineFragment:(CGRect)lineFrag
|
|
39
|
-
glyphPosition:(CGPoint)position
|
|
40
|
-
characterIndex:(NSUInteger)charIndex
|
|
41
|
-
{
|
|
42
|
-
CGFloat height = self.cachedHeight;
|
|
43
|
-
CGFloat width = self.isInline ? height : (lineFrag.size.width > 0 ? lineFrag.size.width : height);
|
|
44
|
-
return CGRectMake(0, 0, width, height);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
- (UIImage *)imageForBounds:(CGRect)imageBounds
|
|
48
|
-
textContainer:(NSTextContainer *)textContainer
|
|
49
|
-
characterIndex:(NSUInteger)charIndex
|
|
50
|
-
{
|
|
51
|
-
self.textContainer = textContainer;
|
|
52
|
-
|
|
53
|
-
// If width is now known but we haven't scaled yet, trigger it
|
|
54
|
-
if (self.originalImage && !self.loadedImage && imageBounds.size.width > 0) {
|
|
55
|
-
self.bounds = imageBounds;
|
|
56
|
-
[self handleLoadedImage:self.originalImage];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return self.loadedImage ?: self.image;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
- (void)handleLoadedImage:(UIImage *)image
|
|
63
|
-
{
|
|
64
|
-
if (!image)
|
|
65
|
-
return;
|
|
66
|
-
self.originalImage = image;
|
|
67
|
-
|
|
68
|
-
__weak typeof(self) weakSelf = self;
|
|
69
|
-
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
70
|
-
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
71
|
-
if (!strongSelf)
|
|
72
|
-
return;
|
|
73
|
-
|
|
74
|
-
CGFloat targetWidth = strongSelf.isInline ? strongSelf.cachedHeight : strongSelf.bounds.size.width;
|
|
75
|
-
if (targetWidth <= 0)
|
|
76
|
-
return;
|
|
77
|
-
|
|
78
|
-
UIImage *scaled = [strongSelf scaleImage:image
|
|
79
|
-
toWidth:targetWidth
|
|
80
|
-
height:strongSelf.cachedHeight
|
|
81
|
-
borderRadius:strongSelf.cachedBorderRadius];
|
|
82
|
-
|
|
83
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
84
|
-
strongSelf.loadedImage = scaled;
|
|
85
|
-
|
|
86
|
-
if (strongSelf.isInline) {
|
|
87
|
-
// Use scaled image for RTF export (RTF uses self.image dimensions)
|
|
88
|
-
strongSelf.image = scaled;
|
|
89
|
-
strongSelf.bounds = CGRectMake(0, 0, strongSelf.cachedHeight, strongSelf.cachedHeight);
|
|
90
|
-
} else {
|
|
91
|
-
// Use original for "Save to Camera Roll" (no baked-in rounded corners)
|
|
92
|
-
strongSelf.image = image;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
[strongSelf updateUI];
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
- (void)updateUI
|
|
101
|
-
{
|
|
102
|
-
UITextView *textView = [self getTextView];
|
|
103
|
-
if (!textView)
|
|
104
|
-
return;
|
|
105
|
-
|
|
106
|
-
NSRange range = [self findAttachmentRangeInText:textView.attributedText];
|
|
107
|
-
if (range.location == NSNotFound)
|
|
108
|
-
return;
|
|
109
|
-
|
|
110
|
-
[textView.layoutManager invalidateDisplayForCharacterRange:range];
|
|
111
|
-
if (!self.isInline) {
|
|
112
|
-
[textView.layoutManager invalidateLayoutForCharacterRange:range actualCharacterRange:NULL];
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
- (UITextView *)getTextView
|
|
117
|
-
{
|
|
118
|
-
if (self.textView)
|
|
119
|
-
return self.textView;
|
|
120
|
-
if (!self.textContainer)
|
|
121
|
-
return nil;
|
|
122
|
-
self.textView = objc_getAssociatedObject(self.textContainer, kTextViewKey);
|
|
123
|
-
return self.textView;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
- (void)setupPlaceholder
|
|
127
|
-
{
|
|
128
|
-
CGFloat size = self.cachedHeight;
|
|
129
|
-
self.bounds = CGRectMake(0, 0, size, size);
|
|
130
|
-
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(size, size)];
|
|
131
|
-
self.image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *ctx){}];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
- (void)loadImage
|
|
135
|
-
{
|
|
136
|
-
if (self.imageURL.length == 0)
|
|
137
|
-
return;
|
|
138
|
-
NSURL *url = [NSURL URLWithString:self.imageURL];
|
|
139
|
-
if (!url)
|
|
140
|
-
return;
|
|
141
|
-
|
|
142
|
-
__weak typeof(self) weakSelf = self;
|
|
143
|
-
self.loadingTask =
|
|
144
|
-
[[NSURLSession sharedSession] dataTaskWithURL:url
|
|
145
|
-
completionHandler:^(NSData *data, NSURLResponse *res, NSError *err) {
|
|
146
|
-
if (data) {
|
|
147
|
-
UIImage *img = [UIImage imageWithData:data];
|
|
148
|
-
dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf handleLoadedImage:img]; });
|
|
149
|
-
}
|
|
150
|
-
}];
|
|
151
|
-
[self.loadingTask resume];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
- (UIImage *)scaleImage:(UIImage *)image toWidth:(CGFloat)w height:(CGFloat)h borderRadius:(CGFloat)r
|
|
155
|
-
{
|
|
156
|
-
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(w, h)];
|
|
157
|
-
return [renderer imageWithActions:^(UIGraphicsImageRendererContext *ctx) {
|
|
158
|
-
if (r > 0) {
|
|
159
|
-
[[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, w, h) cornerRadius:r] addClip];
|
|
160
|
-
}
|
|
161
|
-
[image drawInRect:CGRectMake(0, 0, w, h)];
|
|
162
|
-
}];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
- (NSRange)findAttachmentRangeInText:(NSAttributedString *)text
|
|
166
|
-
{
|
|
167
|
-
__block NSRange found = NSMakeRange(NSNotFound, 0);
|
|
168
|
-
[text enumerateAttribute:NSAttachmentAttributeName
|
|
169
|
-
inRange:NSMakeRange(0, text.length)
|
|
170
|
-
options:0
|
|
171
|
-
usingBlock:^(id val, NSRange range, BOOL *stop) {
|
|
172
|
-
if (val == self) {
|
|
173
|
-
found = range;
|
|
174
|
-
*stop = YES;
|
|
175
|
-
}
|
|
176
|
-
}];
|
|
177
|
-
return found;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
- (void)dealloc
|
|
181
|
-
{
|
|
182
|
-
[_loadingTask cancel];
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
@end
|