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.
@@ -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
- for (UIView *subview in [self.subviews copy]) {
18
- [subview removeFromSuperview];
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 (JMDMeasuredBlock *measured in blocks) {
21
- [self addSubview:[self createViewFor:measured]];
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 *)createViewFor:(JMDMeasuredBlock *)measured {
35
+ - (UIView *)bindView:(nullable UIView *)old to:(JMDMeasuredBlock *)measured {
27
36
  switch (measured.block.kind) {
28
37
  case JMDBlockKindText: {
29
- JMDBlockTextView *view = [[JMDBlockTextView alloc] initWithFrame:CGRectZero];
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.attributedText = measured.block.attributedText;
45
+ [view bindTextStorage:measured.textStorage];
35
46
  return view;
36
47
  }
37
48
  case JMDBlockKindCode: {
38
- JMDCodeBlockView *view = [[JMDCodeBlockView alloc] initWithFrame:CGRectZero];
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 = [[JMDQuoteView alloc] initWithFrame:CGRectZero];
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 = [[JMDListBlockView alloc] initWithFrame:CGRectZero];
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 = [[UIView alloc] initWithFrame:CGRectZero];
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 = [[JMDImageView alloc] initWithFrame:CGRectZero];
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 = [[JMDTableView alloc] initWithFrame:CGRectZero];
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
- @property (nonatomic, copy, nullable) NSAttributedString *attributedText;
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)setAttributedText:(NSAttributedString *)attributedText {
27
- if (![_attributedText isEqualToAttributedString:attributedText]) {
28
- _attributedText = [attributedText copy];
29
- _layoutManager = nil;
30
- // The hiding state lives in the storage, which is rebuilt (with the
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 = attributedText.string;
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
- // Lazy TextKit stack shared by drawing, overlay geometry, and hit-testing
41
- // (and configured identically to the measurer's) — one engine, so they can
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
- NSLayoutManager *layoutManager = [self layoutManagerForBounds];
61
- if (layoutManager == nil) {
46
+ if (_layoutManager == nil || _textStorage.length == 0) {
62
47
  return;
63
48
  }
64
- // One engine for everything: the same layout manager that positions the
65
- // overlays also draws the glyphs. NSStringDrawing typesets separately and
66
- // disagrees with NSLayoutManager about font leading under a lineHeight
67
- // cap (fonts with a nonzero line gap drift ~gap pt per line), which
68
- // misaligned overlays on wrapped lines.
69
- [self syncSpoilerHiding:layoutManager];
70
- [self drawRunBackgrounds:layoutManager];
71
- const NSRange glyphRange =
72
- [layoutManager glyphRangeForTextContainer:self->_textContainer];
73
- [layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:CGPointZero];
74
- [layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:CGPointZero];
75
- [self drawSpoilerCovers:layoutManager];
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 draws fully transparent the cover hugs the
79
- // text, so glyphs would leak around it if drawn. Colors mutate on the
80
- // shared storage (color-only edits don't reflow); originals restore from
81
- // the immutable _attributedText on reveal.
82
- - (void)syncSpoilerHiding:(NSLayoutManager *)layoutManager {
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
- if (_hiddenSpoilers == nil) {
87
- _hiddenSpoilers = [NSMutableSet new];
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, _attributedText.length)
76
+ inRange:NSMakeRange(0, _textStorage.length)
93
77
  options:0
94
78
  usingBlock:^(NSNumber *spoilerId, NSRange range, BOOL *stop) {
95
- if (spoilerId == nil) {
96
- return;
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
- if (shouldHide) {
106
- [self->_textStorage addAttributes:@{
107
- NSForegroundColorAttributeName : UIColor.clearColor,
108
- NSUnderlineColorAttributeName : UIColor.clearColor,
109
- NSStrikethroughColorAttributeName : UIColor.clearColor,
110
- }
111
- range:range];
112
- [self->_hiddenSpoilers addObject:spoilerId];
113
- } else {
114
- [self->_attributedText
115
- enumerateAttributesInRange:range
116
- options:0
117
- usingBlock:^(NSDictionary *attrs,
118
- NSRange subRange,
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
- [_textStorage endEditing];
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
- [_attributedText
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, and NSStringDrawing leaves it wherever the last drawn
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
- [layoutManager glyphRangeForCharacterRange:range actualCharacterRange:nil];
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
- [layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex
221
- effectiveRange:&lineGlyphRange];
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
- [layoutManager characterRangeForGlyphRange:lineRange actualGlyphRange:nil];
201
+ [_layoutManager characterRangeForGlyphRange:lineRange actualGlyphRange:nil];
228
202
  const CGPoint startLocation =
229
- [layoutManager locationForGlyphAtIndex:lineRange.location];
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)[self->_attributedText
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
- [self->_attributedText attribute:NSFontAttributeName
243
- atIndex:charRange.location
244
- effectiveRange:nil]
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; enumerates the display copy so chips hidden with
265
- // their spoiler don't draw.
266
- - (void)drawRunBackgrounds:(NSLayoutManager *)layoutManager {
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, _attributedText.length)
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 (transparent) text until revealed.
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:(NSLayoutManager *)layoutManager {
296
- if (_attributedText.length == 0 || self.host == nil) {
262
+ - (void)drawSpoilerCovers {
263
+ if (self.host == nil) {
297
264
  return;
298
265
  }
299
- [_attributedText
266
+ [_textStorage
300
267
  enumerateAttribute:JMDSpoilerIDAttributeName
301
- inRange:NSMakeRange(0, _attributedText.length)
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
- NSLayoutManager *layoutManager = [self layoutManagerForBounds];
323
- if (layoutManager == nil || _attributedText.length == 0) {
286
+ if (_layoutManager == nil || _textStorage.length == 0) {
324
287
  return nil;
325
288
  }
326
- const NSUInteger glyphIndex = [layoutManager glyphIndexForPoint:point
327
- inTextContainer:_textContainer];
328
- const CGRect glyphRect = [layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1)
329
- inTextContainer:_textContainer];
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 = [layoutManager characterIndexForGlyphAtIndex:glyphIndex];
334
- if (charIndex >= _attributedText.length) {
296
+ const NSUInteger charIndex = [_layoutManager characterIndexForGlyphAtIndex:glyphIndex];
297
+ if (charIndex >= _textStorage.length) {
335
298
  return nil;
336
299
  }
337
- return [_attributedText attributesAtIndex:charIndex effectiveRange:nil];
300
+ return [_textStorage attributesAtIndex:charIndex effectiveRange:nil];
338
301
  }
339
302
 
340
303
  @end
@@ -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.attributedText = measured.block.attributedText;
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
- for (UIView *subview in [self.subviews copy]) {
242
- [subview removeFromSuperview];
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
- JMDBlock *block = measured.block;
245
- for (NSUInteger i = 0; i < block.rows.count; i++) {
246
- JMDBlockTextView *marker = [[JMDBlockTextView alloc] initWithFrame:CGRectZero];
247
- marker.attributedText = block.rows[i].marker;
248
- [self addSubview:marker];
249
-
250
- JMDBlockStackView *content = [[JMDBlockStackView alloc] initWithFrame:CGRectZero];
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
  }
@@ -22,16 +22,23 @@
22
22
 
23
23
  - (void)bind:(JMDMeasuredBlock *)measured host:(nullable id<JMDMarkdownHost>)host {
24
24
  _measured = measured;
25
- for (UIView *subview in [self.subviews copy]) {
26
- [subview removeFromSuperview];
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
- JMDBlock *block = measured.block;
29
- for (JMDTableRow *row in block.tableRows) {
30
- for (NSAttributedString *cell in row.cells) {
31
- JMDBlockTextView *view = [[JMDBlockTextView alloc] initWithFrame:CGRectZero];
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.attributedText = cell;
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.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__",