react-native-jet-markdown 0.2.2 → 0.2.3-beta.1
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/JetMarkdown.podspec +1 -1
- package/README.md +28 -27
- package/android/src/main/java/com/jetmarkdown/JetMarkdownView.kt +11 -11
- package/android/src/main/java/com/jetmarkdown/views/BlockStackView.kt +49 -22
- package/android/src/main/java/com/jetmarkdown/views/BlockTextView.kt +2 -0
- package/android/src/main/java/com/jetmarkdown/views/TableBlockView.kt +11 -3
- package/cpp/core/InlineExtensions.cpp +2 -2
- package/cpp/core/Parser.cpp +7 -1
- package/cpp/md4c/md4c.c +14 -1
- package/ios/JetMarkdownView.mm +9 -16
- package/ios/measure/JMDMarkdownMeasurer.mm +1 -1
- package/ios/render/JMDBlock.h +7 -0
- package/ios/render/JMDBlock.m +2 -0
- package/ios/render/JMDBlockRenderer.mm +27 -1
- package/ios/render/JMDRenderedContent.mm +36 -11
- package/ios/views/JMDBlockStackView.m +36 -14
- package/ios/views/JMDBlockTextView.h +6 -1
- package/ios/views/JMDBlockTextView.m +90 -127
- package/ios/views/JMDBoxViews.m +14 -11
- package/ios/views/JMDTableView.m +15 -8
- package/package.json +2 -1
- package/cpp/tests/parser_tests.cpp +0 -260
- package/cpp/tests/run_tests.sh +0 -22
|
@@ -10,58 +10,81 @@
|
|
|
10
10
|
CGFloat _gap;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// Rebinds in place: a subview of the right kind at the same index is
|
|
14
|
+
// reused, anything else is replaced. List recycling rebinds whole trees, so
|
|
15
|
+
// the common case (same block kinds, different content) allocates nothing.
|
|
13
16
|
- (void)setBlocks:(NSArray<JMDMeasuredBlock *> *)blocks gap:(CGFloat)gap {
|
|
14
17
|
_measured = blocks;
|
|
15
18
|
_gap = gap;
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
NSArray<UIView *> *existing = self.subviews;
|
|
21
|
+
for (NSUInteger i = 0; i < blocks.count; i++) {
|
|
22
|
+
UIView *old = i < existing.count ? existing[i] : nil;
|
|
23
|
+
UIView *view = [self bindView:old to:blocks[i]];
|
|
24
|
+
if (view != old) {
|
|
25
|
+
[old removeFromSuperview];
|
|
26
|
+
[self insertSubview:view atIndex:i];
|
|
27
|
+
}
|
|
19
28
|
}
|
|
20
|
-
for (
|
|
21
|
-
[
|
|
29
|
+
for (NSUInteger i = blocks.count; i < existing.count; i++) {
|
|
30
|
+
[existing[i] removeFromSuperview];
|
|
22
31
|
}
|
|
23
32
|
[self setNeedsLayout];
|
|
24
33
|
}
|
|
25
34
|
|
|
26
|
-
- (UIView *)
|
|
35
|
+
- (UIView *)bindView:(nullable UIView *)old to:(JMDMeasuredBlock *)measured {
|
|
27
36
|
switch (measured.block.kind) {
|
|
28
37
|
case JMDBlockKindText: {
|
|
29
|
-
JMDBlockTextView *view = [
|
|
38
|
+
JMDBlockTextView *view = [old isKindOfClass:JMDBlockTextView.class]
|
|
39
|
+
? (JMDBlockTextView *)old
|
|
40
|
+
: [[JMDBlockTextView alloc] initWithFrame:CGRectZero];
|
|
30
41
|
view.host = self.host;
|
|
31
42
|
view.spoilerColor = measured.block.spoilerColor;
|
|
32
43
|
view.spoilerRadius = measured.block.spoilerRadius;
|
|
33
44
|
view.spoilerContinuous = measured.block.spoilerContinuous;
|
|
34
|
-
view
|
|
45
|
+
[view bindTextStorage:measured.textStorage];
|
|
35
46
|
return view;
|
|
36
47
|
}
|
|
37
48
|
case JMDBlockKindCode: {
|
|
38
|
-
JMDCodeBlockView *view = [
|
|
49
|
+
JMDCodeBlockView *view = [old isKindOfClass:JMDCodeBlockView.class]
|
|
50
|
+
? (JMDCodeBlockView *)old
|
|
51
|
+
: [[JMDCodeBlockView alloc] initWithFrame:CGRectZero];
|
|
39
52
|
[view bind:measured];
|
|
40
53
|
return view;
|
|
41
54
|
}
|
|
42
55
|
case JMDBlockKindQuote: {
|
|
43
|
-
JMDQuoteView *view = [
|
|
56
|
+
JMDQuoteView *view = [old isKindOfClass:JMDQuoteView.class]
|
|
57
|
+
? (JMDQuoteView *)old
|
|
58
|
+
: [[JMDQuoteView alloc] initWithFrame:CGRectZero];
|
|
44
59
|
[view bind:measured gap:_gap host:self.host];
|
|
45
60
|
return view;
|
|
46
61
|
}
|
|
47
62
|
case JMDBlockKindList: {
|
|
48
|
-
JMDListBlockView *view = [
|
|
63
|
+
JMDListBlockView *view = [old isKindOfClass:JMDListBlockView.class]
|
|
64
|
+
? (JMDListBlockView *)old
|
|
65
|
+
: [[JMDListBlockView alloc] initWithFrame:CGRectZero];
|
|
49
66
|
[view bind:measured gap:_gap host:self.host];
|
|
50
67
|
return view;
|
|
51
68
|
}
|
|
52
69
|
case JMDBlockKindDivider: {
|
|
53
|
-
UIView *view =
|
|
70
|
+
UIView *view = (old != nil && old.class == UIView.class)
|
|
71
|
+
? old
|
|
72
|
+
: [[UIView alloc] initWithFrame:CGRectZero];
|
|
54
73
|
view.backgroundColor = measured.block.dividerColor;
|
|
55
74
|
return view;
|
|
56
75
|
}
|
|
57
76
|
case JMDBlockKindImage: {
|
|
58
|
-
JMDImageView *view = [
|
|
77
|
+
JMDImageView *view = [old isKindOfClass:JMDImageView.class]
|
|
78
|
+
? (JMDImageView *)old
|
|
79
|
+
: [[JMDImageView alloc] initWithFrame:CGRectZero];
|
|
59
80
|
view.host = self.host;
|
|
60
81
|
[view bind:measured.block];
|
|
61
82
|
return view;
|
|
62
83
|
}
|
|
63
84
|
case JMDBlockKindTable: {
|
|
64
|
-
JMDTableView *view = [
|
|
85
|
+
JMDTableView *view = [old isKindOfClass:JMDTableView.class]
|
|
86
|
+
? (JMDTableView *)old
|
|
87
|
+
: [[JMDTableView alloc] initWithFrame:CGRectZero];
|
|
65
88
|
[view bind:measured host:self.host];
|
|
66
89
|
return view;
|
|
67
90
|
}
|
|
@@ -69,7 +92,6 @@
|
|
|
69
92
|
return [[UIView alloc] initWithFrame:CGRectZero];
|
|
70
93
|
}
|
|
71
94
|
|
|
72
|
-
|
|
73
95
|
// Never the hit view itself: markdown touches belong to the host component
|
|
74
96
|
// view; only nested scrollers (code blocks, tables) claim touches.
|
|
75
97
|
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
|
|
@@ -9,12 +9,17 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
9
9
|
/// hit-tests links, mentions, and spoilers by character range.
|
|
10
10
|
@interface JMDBlockTextView : UIView
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/// The bound storage's contents (nil before bind).
|
|
13
|
+
@property (nonatomic, readonly, nullable) NSAttributedString *attributedText;
|
|
13
14
|
@property (nonatomic, weak, nullable) id<JMDMarkdownHost> host;
|
|
14
15
|
@property (nonatomic, strong, nullable) UIColor *spoilerColor;
|
|
15
16
|
@property (nonatomic, assign) CGFloat spoilerRadius;
|
|
16
17
|
@property (nonatomic, assign) BOOL spoilerContinuous;
|
|
17
18
|
|
|
19
|
+
/// Binds a measure-time TextKit stack (see JMDMeasuredBlock); the view
|
|
20
|
+
/// draws with it and never lays text out itself.
|
|
21
|
+
- (void)bindTextStorage:(NSTextStorage *)storage;
|
|
22
|
+
|
|
18
23
|
/// Attributed-string attributes at a point in this view's coordinates;
|
|
19
24
|
/// nil when the point is not on a glyph. Used by the host for hit testing.
|
|
20
25
|
- (nullable NSDictionary *)attributesAtPoint:(CGPoint)point;
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
#import <CoreText/CoreText.h>
|
|
4
4
|
|
|
5
5
|
@implementation JMDBlockTextView {
|
|
6
|
+
// Owned by the measured block and shared with every view bound to the
|
|
7
|
+
// same cached content (and the layout thread that built it), so it is
|
|
8
|
+
// never mutated here: spoiler hiding is a draw-time clip.
|
|
9
|
+
NSTextStorage *_textStorage;
|
|
6
10
|
NSLayoutManager *_layoutManager;
|
|
7
11
|
NSTextContainer *_textContainer;
|
|
8
|
-
NSTextStorage *_textStorage;
|
|
9
|
-
NSMutableSet<NSNumber *> *_hiddenSpoilers;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
- (instancetype)initWithFrame:(CGRect)frame {
|
|
@@ -23,108 +25,82 @@
|
|
|
23
25
|
return nil;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
- (void)
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
_layoutManager =
|
|
30
|
-
|
|
31
|
-
// original colors) alongside the layout manager.
|
|
32
|
-
[_hiddenSpoilers removeAllObjects];
|
|
28
|
+
- (void)bindTextStorage:(NSTextStorage *)storage {
|
|
29
|
+
if (_textStorage != storage) {
|
|
30
|
+
_textStorage = storage;
|
|
31
|
+
_layoutManager = storage.layoutManagers.firstObject;
|
|
32
|
+
_textContainer = _layoutManager.textContainers.firstObject;
|
|
33
33
|
self.isAccessibilityElement = YES;
|
|
34
|
-
self.accessibilityLabel =
|
|
34
|
+
self.accessibilityLabel = storage.string;
|
|
35
35
|
self.accessibilityTraits = UIAccessibilityTraitStaticText;
|
|
36
|
-
[self setNeedsDisplay];
|
|
37
36
|
}
|
|
37
|
+
// Spoiler styling and host may change without the storage changing.
|
|
38
|
+
[self setNeedsDisplay];
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// never disagree about line positions.
|
|
43
|
-
- (NSLayoutManager *)layoutManagerForBounds {
|
|
44
|
-
if (_layoutManager == nil && _attributedText != nil) {
|
|
45
|
-
_textStorage = [[NSTextStorage alloc] initWithAttributedString:_attributedText];
|
|
46
|
-
_layoutManager = [NSLayoutManager new];
|
|
47
|
-
_textContainer =
|
|
48
|
-
[[NSTextContainer alloc] initWithSize:CGSizeMake(self.bounds.size.width, CGFLOAT_MAX)];
|
|
49
|
-
_textContainer.lineFragmentPadding = 0;
|
|
50
|
-
[_layoutManager addTextContainer:_textContainer];
|
|
51
|
-
[_textStorage addLayoutManager:_layoutManager];
|
|
52
|
-
} else if (_textContainer != nil &&
|
|
53
|
-
_textContainer.size.width != self.bounds.size.width) {
|
|
54
|
-
_textContainer.size = CGSizeMake(self.bounds.size.width, CGFLOAT_MAX);
|
|
55
|
-
}
|
|
56
|
-
return _layoutManager;
|
|
41
|
+
- (NSAttributedString *)attributedText {
|
|
42
|
+
return _textStorage;
|
|
57
43
|
}
|
|
58
44
|
|
|
59
45
|
- (void)drawRect:(CGRect)rect {
|
|
60
|
-
|
|
61
|
-
if (layoutManager == nil) {
|
|
46
|
+
if (_layoutManager == nil || _textStorage.length == 0) {
|
|
62
47
|
return;
|
|
63
48
|
}
|
|
64
|
-
// One engine for everything: the
|
|
65
|
-
// overlays
|
|
66
|
-
// disagrees with NSLayoutManager about font
|
|
67
|
-
// cap (fonts with a nonzero line gap drift
|
|
68
|
-
// misaligned overlays on wrapped lines.
|
|
69
|
-
[
|
|
70
|
-
[self drawRunBackgrounds
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
[
|
|
74
|
-
[
|
|
75
|
-
[
|
|
49
|
+
// One engine for everything: the layout manager that measured the block
|
|
50
|
+
// also positions the overlays and draws the glyphs. NSStringDrawing
|
|
51
|
+
// typesets separately and disagrees with NSLayoutManager about font
|
|
52
|
+
// leading under a lineHeight cap (fonts with a nonzero line gap drift
|
|
53
|
+
// ~gap pt per line), which misaligned overlays on wrapped lines.
|
|
54
|
+
const NSRange glyphRange = [_layoutManager glyphRangeForTextContainer:_textContainer];
|
|
55
|
+
[self drawRunBackgrounds];
|
|
56
|
+
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
57
|
+
CGContextSaveGState(context);
|
|
58
|
+
[self clipHiddenSpoilers:context];
|
|
59
|
+
[_layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:CGPointZero];
|
|
60
|
+
[_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:CGPointZero];
|
|
61
|
+
CGContextRestoreGState(context);
|
|
62
|
+
[self drawSpoilerCovers];
|
|
76
63
|
}
|
|
77
64
|
|
|
78
|
-
// Unrevealed spoiler text
|
|
79
|
-
// text, so glyphs would leak around it
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (self.host == nil || _attributedText.length == 0 || _textStorage == nil) {
|
|
65
|
+
// Unrevealed spoiler text is hidden by clipping its line-box slices out of
|
|
66
|
+
// the glyph draw (the cover hugs the text, so glyphs would leak around it).
|
|
67
|
+
// Purely draw-time, per-view state; the shared storage stays untouched.
|
|
68
|
+
- (void)clipHiddenSpoilers:(CGContextRef)context {
|
|
69
|
+
if (self.host == nil) {
|
|
84
70
|
return;
|
|
85
71
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
[_textStorage beginEditing];
|
|
90
|
-
[_attributedText
|
|
72
|
+
CGMutablePathRef hidden = CGPathCreateMutable();
|
|
73
|
+
__block BOOL any = NO;
|
|
74
|
+
[_textStorage
|
|
91
75
|
enumerateAttribute:JMDSpoilerIDAttributeName
|
|
92
|
-
inRange:NSMakeRange(0,
|
|
76
|
+
inRange:NSMakeRange(0, _textStorage.length)
|
|
93
77
|
options:0
|
|
94
78
|
usingBlock:^(NSNumber *spoilerId, NSRange range, BOOL *stop) {
|
|
95
|
-
if (spoilerId == nil
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
const BOOL shouldHide =
|
|
99
|
-
![self.host isSpoilerRevealed:spoilerId.integerValue];
|
|
100
|
-
const BOOL isHidden =
|
|
101
|
-
[self->_hiddenSpoilers containsObject:spoilerId];
|
|
102
|
-
if (shouldHide == isHidden) {
|
|
79
|
+
if (spoilerId == nil ||
|
|
80
|
+
[self.host isSpoilerRevealed:spoilerId.integerValue]) {
|
|
103
81
|
return;
|
|
104
82
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
BOOL *stopInner) {
|
|
120
|
-
[self->_textStorage
|
|
121
|
-
setAttributes:attrs
|
|
122
|
-
range:subRange];
|
|
123
|
-
}];
|
|
124
|
-
[self->_hiddenSpoilers removeObject:spoilerId];
|
|
125
|
-
}
|
|
83
|
+
const NSRange glyphs =
|
|
84
|
+
[self->_layoutManager glyphRangeForCharacterRange:range
|
|
85
|
+
actualCharacterRange:nil];
|
|
86
|
+
[self->_layoutManager
|
|
87
|
+
enumerateEnclosingRectsForGlyphRange:glyphs
|
|
88
|
+
withinSelectedGlyphRange:NSMakeRange(NSNotFound, 0)
|
|
89
|
+
inTextContainer:self->_textContainer
|
|
90
|
+
usingBlock:^(CGRect rect, BOOL *stopInner) {
|
|
91
|
+
// Padded past side bearings
|
|
92
|
+
// and italic overhang.
|
|
93
|
+
CGPathAddRect(hidden, NULL,
|
|
94
|
+
CGRectInset(rect, -2, 0));
|
|
95
|
+
any = YES;
|
|
96
|
+
}];
|
|
126
97
|
}];
|
|
127
|
-
|
|
98
|
+
if (any) {
|
|
99
|
+
CGContextAddRect(context, self.bounds);
|
|
100
|
+
CGContextAddPath(context, hidden);
|
|
101
|
+
CGContextEOClip(context);
|
|
102
|
+
}
|
|
103
|
+
CGPathRelease(hidden);
|
|
128
104
|
}
|
|
129
105
|
|
|
130
106
|
// A chip inside an unrevealed spoiler would peek around the cover.
|
|
@@ -133,7 +109,7 @@
|
|
|
133
109
|
return NO;
|
|
134
110
|
}
|
|
135
111
|
__block BOOL hidden = NO;
|
|
136
|
-
[
|
|
112
|
+
[_textStorage
|
|
137
113
|
enumerateAttribute:JMDSpoilerIDAttributeName
|
|
138
114
|
inRange:range
|
|
139
115
|
options:0
|
|
@@ -200,49 +176,45 @@ static const CGFloat JMDInkPad = 2;
|
|
|
200
176
|
// Per-line overlay rects for a character range. Whitespace-only segments
|
|
201
177
|
// have no ink and produce no rect.
|
|
202
178
|
- (NSArray<NSValue *> *)inkRectsForRange:(NSRange)range
|
|
203
|
-
layoutManager:(NSLayoutManager *)layoutManager
|
|
204
179
|
padLeft:(CGFloat)padLeft
|
|
205
180
|
padRight:(CGFloat)padRight {
|
|
206
181
|
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
207
182
|
// CTLineGetImageBounds reports bounds relative to the context's current
|
|
208
|
-
// text position
|
|
209
|
-
// line ended.
|
|
183
|
+
// text position.
|
|
210
184
|
CGContextSetTextPosition(context, 0, 0);
|
|
211
185
|
const CGFloat maxWidth = self.bounds.size.width;
|
|
212
186
|
const CGFloat maxHeight = self.bounds.size.height;
|
|
213
187
|
const NSRange glyphRange =
|
|
214
|
-
[
|
|
188
|
+
[_layoutManager glyphRangeForCharacterRange:range actualCharacterRange:nil];
|
|
215
189
|
NSMutableArray<NSValue *> *lineRects = [NSMutableArray new];
|
|
216
190
|
NSUInteger glyphIndex = glyphRange.location;
|
|
217
191
|
while (glyphIndex < NSMaxRange(glyphRange)) {
|
|
218
192
|
NSRange lineGlyphRange;
|
|
219
193
|
const CGRect fragment =
|
|
220
|
-
[
|
|
221
|
-
|
|
194
|
+
[_layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex
|
|
195
|
+
effectiveRange:&lineGlyphRange];
|
|
222
196
|
const NSRange lineRange = NSIntersectionRange(lineGlyphRange, glyphRange);
|
|
223
197
|
if (lineRange.length == 0) {
|
|
224
198
|
break;
|
|
225
199
|
}
|
|
226
200
|
const NSRange charRange =
|
|
227
|
-
[
|
|
201
|
+
[_layoutManager characterRangeForGlyphRange:lineRange actualGlyphRange:nil];
|
|
228
202
|
const CGPoint startLocation =
|
|
229
|
-
[
|
|
203
|
+
[_layoutManager locationForGlyphAtIndex:lineRange.location];
|
|
230
204
|
// The typesetter already folds NSBaselineOffset (lineHeight centering,
|
|
231
205
|
// sup/sub shifts) into the glyph location.
|
|
232
206
|
const CGFloat baselineY = fragment.origin.y + startLocation.y;
|
|
233
207
|
const CGFloat penX = fragment.origin.x + startLocation.x;
|
|
234
208
|
|
|
235
209
|
CTLineRef line = CTLineCreateWithAttributedString(
|
|
236
|
-
(__bridge CFAttributedStringRef)[
|
|
237
|
-
attributedSubstringFromRange:charRange]);
|
|
210
|
+
(__bridge CFAttributedStringRef)[_textStorage attributedSubstringFromRange:charRange]);
|
|
238
211
|
const CGRect ink = CTLineGetImageBounds(line, context);
|
|
239
212
|
CFRelease(line);
|
|
240
213
|
if (!CGRectIsNull(ink) && ink.size.width > 0) {
|
|
241
|
-
UIFont *font =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
?: [UIFont systemFontOfSize:UIFont.systemFontSize];
|
|
214
|
+
UIFont *font = [_textStorage attribute:NSFontAttributeName
|
|
215
|
+
atIndex:charRange.location
|
|
216
|
+
effectiveRange:nil]
|
|
217
|
+
?: [UIFont systemFontOfSize:UIFont.systemFontSize];
|
|
246
218
|
const CGFloat top = MAX(baselineY - font.capHeight - JMDInkPad, 0);
|
|
247
219
|
const CGFloat bottom =
|
|
248
220
|
MIN(baselineY - font.descender + JMDInkPad, maxHeight);
|
|
@@ -261,15 +233,11 @@ static const CGFloat JMDInkPad = 2;
|
|
|
261
233
|
}
|
|
262
234
|
|
|
263
235
|
// Run background chips (inlineCode/link/mention and plain highlights),
|
|
264
|
-
// drawn UNDER the text;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (_attributedText.length == 0) {
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
[_attributedText
|
|
236
|
+
// drawn UNDER the text; chips hidden with their spoiler don't draw.
|
|
237
|
+
- (void)drawRunBackgrounds {
|
|
238
|
+
[_textStorage
|
|
271
239
|
enumerateAttribute:JMDRunBackgroundAttributeName
|
|
272
|
-
inRange:NSMakeRange(0,
|
|
240
|
+
inRange:NSMakeRange(0, _textStorage.length)
|
|
273
241
|
options:0
|
|
274
242
|
usingBlock:^(JMDRunBackground *chip, NSRange range, BOOL *stop) {
|
|
275
243
|
if (chip == nil || [self isRangeInsideHiddenSpoiler:range]) {
|
|
@@ -277,7 +245,6 @@ static const CGFloat JMDInkPad = 2;
|
|
|
277
245
|
}
|
|
278
246
|
NSArray<NSValue *> *rects = [self
|
|
279
247
|
inkRectsForRange:range
|
|
280
|
-
layoutManager:layoutManager
|
|
281
248
|
padLeft:chip.padLeft > 0 ? chip.padLeft : JMDInkPad
|
|
282
249
|
padRight:chip.padRight > 0 ? chip.padRight
|
|
283
250
|
: JMDInkPad];
|
|
@@ -289,16 +256,16 @@ static const CGFloat JMDInkPad = 2;
|
|
|
289
256
|
}];
|
|
290
257
|
}
|
|
291
258
|
|
|
292
|
-
// Spoiler cover chips, drawn OVER the (
|
|
259
|
+
// Spoiler cover chips, drawn OVER the (clipped-out) text until revealed.
|
|
293
260
|
// Same ink geometry as the run backgrounds so covers and backgrounds look
|
|
294
261
|
// identical.
|
|
295
|
-
- (void)drawSpoilerCovers
|
|
296
|
-
if (
|
|
262
|
+
- (void)drawSpoilerCovers {
|
|
263
|
+
if (self.host == nil) {
|
|
297
264
|
return;
|
|
298
265
|
}
|
|
299
|
-
[
|
|
266
|
+
[_textStorage
|
|
300
267
|
enumerateAttribute:JMDSpoilerIDAttributeName
|
|
301
|
-
inRange:NSMakeRange(0,
|
|
268
|
+
inRange:NSMakeRange(0, _textStorage.length)
|
|
302
269
|
options:0
|
|
303
270
|
usingBlock:^(NSNumber *spoilerId, NSRange range, BOOL *stop) {
|
|
304
271
|
if (spoilerId == nil ||
|
|
@@ -306,10 +273,7 @@ static const CGFloat JMDInkPad = 2;
|
|
|
306
273
|
return;
|
|
307
274
|
}
|
|
308
275
|
NSArray<NSValue *> *rects =
|
|
309
|
-
[self inkRectsForRange:range
|
|
310
|
-
layoutManager:layoutManager
|
|
311
|
-
padLeft:JMDInkPad
|
|
312
|
-
padRight:JMDInkPad];
|
|
276
|
+
[self inkRectsForRange:range padLeft:JMDInkPad padRight:JMDInkPad];
|
|
313
277
|
[self.spoilerColor ?: UIColor.darkGrayColor setFill];
|
|
314
278
|
for (NSValue *value in rects) {
|
|
315
279
|
[JMDChipPath(value.CGRectValue, self.spoilerRadius,
|
|
@@ -319,22 +283,21 @@ static const CGFloat JMDInkPad = 2;
|
|
|
319
283
|
}
|
|
320
284
|
|
|
321
285
|
- (nullable NSDictionary *)attributesAtPoint:(CGPoint)point {
|
|
322
|
-
|
|
323
|
-
if (layoutManager == nil || _attributedText.length == 0) {
|
|
286
|
+
if (_layoutManager == nil || _textStorage.length == 0) {
|
|
324
287
|
return nil;
|
|
325
288
|
}
|
|
326
|
-
const NSUInteger glyphIndex = [
|
|
327
|
-
|
|
328
|
-
const CGRect glyphRect = [
|
|
329
|
-
|
|
289
|
+
const NSUInteger glyphIndex = [_layoutManager glyphIndexForPoint:point
|
|
290
|
+
inTextContainer:_textContainer];
|
|
291
|
+
const CGRect glyphRect = [_layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1)
|
|
292
|
+
inTextContainer:_textContainer];
|
|
330
293
|
if (!CGRectContainsPoint(CGRectInset(glyphRect, -8, -4), point)) {
|
|
331
294
|
return nil;
|
|
332
295
|
}
|
|
333
|
-
const NSUInteger charIndex = [
|
|
334
|
-
if (charIndex >=
|
|
296
|
+
const NSUInteger charIndex = [_layoutManager characterIndexForGlyphAtIndex:glyphIndex];
|
|
297
|
+
if (charIndex >= _textStorage.length) {
|
|
335
298
|
return nil;
|
|
336
299
|
}
|
|
337
|
-
return [
|
|
300
|
+
return [_textStorage attributesAtIndex:charIndex effectiveRange:nil];
|
|
338
301
|
}
|
|
339
302
|
|
|
340
303
|
@end
|
package/ios/views/JMDBoxViews.m
CHANGED
|
@@ -203,7 +203,7 @@ static void JMDApplyBox(UIView *view, JMDLayoutStyle *style) {
|
|
|
203
203
|
[self addSubview:_scroller];
|
|
204
204
|
}
|
|
205
205
|
JMDApplyBox(self, measured.block.layoutStyle);
|
|
206
|
-
_text
|
|
206
|
+
[_text bindTextStorage:measured.textStorage];
|
|
207
207
|
[self setNeedsLayout];
|
|
208
208
|
}
|
|
209
209
|
|
|
@@ -238,19 +238,22 @@ static void JMDApplyBox(UIView *view, JMDLayoutStyle *style) {
|
|
|
238
238
|
host:(nullable id<JMDMarkdownHost>)host {
|
|
239
239
|
_measured = measured;
|
|
240
240
|
_gap = gap;
|
|
241
|
-
|
|
242
|
-
|
|
241
|
+
// Subviews are (marker, content) pairs; grow/shrink the pair list, then
|
|
242
|
+
// rebind every pair in place.
|
|
243
|
+
const NSUInteger needed = measured.block.rows.count * 2;
|
|
244
|
+
while (self.subviews.count > needed) {
|
|
245
|
+
[self.subviews.lastObject removeFromSuperview];
|
|
243
246
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
247
|
+
while (self.subviews.count < needed) {
|
|
248
|
+
[self addSubview:[[JMDBlockTextView alloc] initWithFrame:CGRectZero]];
|
|
249
|
+
[self addSubview:[[JMDBlockStackView alloc] initWithFrame:CGRectZero]];
|
|
250
|
+
}
|
|
251
|
+
for (NSUInteger i = 0; i < measured.block.rows.count; i++) {
|
|
252
|
+
JMDBlockTextView *marker = (JMDBlockTextView *)self.subviews[i * 2];
|
|
253
|
+
[marker bindTextStorage:measured.markerStorages[i]];
|
|
254
|
+
JMDBlockStackView *content = (JMDBlockStackView *)self.subviews[i * 2 + 1];
|
|
251
255
|
content.host = host;
|
|
252
256
|
[content setBlocks:measured.rowContents[i] gap:gap];
|
|
253
|
-
[self addSubview:content];
|
|
254
257
|
}
|
|
255
258
|
[self setNeedsLayout];
|
|
256
259
|
}
|
package/ios/views/JMDTableView.m
CHANGED
|
@@ -22,16 +22,23 @@
|
|
|
22
22
|
|
|
23
23
|
- (void)bind:(JMDMeasuredBlock *)measured host:(nullable id<JMDMarkdownHost>)host {
|
|
24
24
|
_measured = measured;
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// One text view per cell, row-major; grow/shrink then rebind in place.
|
|
26
|
+
NSUInteger needed = 0;
|
|
27
|
+
for (NSArray<NSTextStorage *> *row in measured.cellStorages) {
|
|
28
|
+
needed += row.count;
|
|
27
29
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
while (self.subviews.count > needed) {
|
|
31
|
+
[self.subviews.lastObject removeFromSuperview];
|
|
32
|
+
}
|
|
33
|
+
while (self.subviews.count < needed) {
|
|
34
|
+
[self addSubview:[[JMDBlockTextView alloc] initWithFrame:CGRectZero]];
|
|
35
|
+
}
|
|
36
|
+
NSUInteger index = 0;
|
|
37
|
+
for (NSArray<NSTextStorage *> *row in measured.cellStorages) {
|
|
38
|
+
for (NSTextStorage *cell in row) {
|
|
39
|
+
JMDBlockTextView *view = (JMDBlockTextView *)self.subviews[index++];
|
|
32
40
|
view.host = host;
|
|
33
|
-
view
|
|
34
|
-
[self addSubview:view];
|
|
41
|
+
[view bindTextStorage:cell];
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
[self setNeedsDisplay];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-jet-markdown",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3-beta.1",
|
|
4
4
|
"description": "High-performance native Markdown renderer for React Native",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"!android/gradlew",
|
|
42
42
|
"!android/gradlew.bat",
|
|
43
43
|
"!android/local.properties",
|
|
44
|
+
"!cpp/tests",
|
|
44
45
|
"!**/__tests__",
|
|
45
46
|
"!**/__fixtures__",
|
|
46
47
|
"!**/__mocks__",
|