react-native-jet-markdown 0.2.1 → 0.2.3-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/JetMarkdown.podspec +1 -1
  2. package/README.md +31 -82
  3. package/android/src/main/cpp/JniBindings.cpp +0 -106
  4. package/android/src/main/java/com/jetmarkdown/JetMarkdownNative.kt +0 -79
  5. package/android/src/main/java/com/jetmarkdown/JetMarkdownPackage.kt +1 -2
  6. package/android/src/main/java/com/jetmarkdown/JetMarkdownView.kt +11 -11
  7. package/android/src/main/java/com/jetmarkdown/render/spans/MarkdownLineHeightSpan.kt +2 -13
  8. package/android/src/main/java/com/jetmarkdown/views/BlockStackView.kt +49 -22
  9. package/android/src/main/java/com/jetmarkdown/views/BlockTextView.kt +2 -0
  10. package/android/src/main/java/com/jetmarkdown/views/TableBlockView.kt +11 -3
  11. package/cpp/react/override/react/renderer/components/JetMarkdownViewSpec/ComponentDescriptors.h +0 -4
  12. package/ios/JetMarkdownView.mm +9 -16
  13. package/ios/measure/JMDMarkdownMeasurer.mm +1 -1
  14. package/ios/render/JMDBlock.h +7 -0
  15. package/ios/render/JMDBlock.m +2 -0
  16. package/ios/render/JMDBlockRenderer.mm +27 -1
  17. package/ios/render/JMDRenderedContent.mm +36 -11
  18. package/ios/views/JMDBlockStackView.m +36 -14
  19. package/ios/views/JMDBlockTextView.h +6 -1
  20. package/ios/views/JMDBlockTextView.m +90 -127
  21. package/ios/views/JMDBoxViews.m +14 -11
  22. package/ios/views/JMDTableView.m +15 -8
  23. package/lib/module/index.js +0 -2
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/typescript/src/index.d.ts +1 -3
  26. package/lib/typescript/src/index.d.ts.map +1 -1
  27. package/lib/typescript/src/types.d.ts +0 -127
  28. package/lib/typescript/src/types.d.ts.map +1 -1
  29. package/package.json +3 -5
  30. package/src/index.tsx +0 -9
  31. package/src/types.ts +0 -134
  32. package/android/src/main/java/com/jetmarkdown/editor/EditorSpans.kt +0 -217
  33. package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorManager.kt +0 -224
  34. package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorView.kt +0 -1736
  35. package/cpp/core/AstToMarkdown.cpp +0 -549
  36. package/cpp/core/AstToMarkdown.h +0 -16
  37. package/cpp/core/EditorRuns.cpp +0 -640
  38. package/cpp/core/EditorRuns.h +0 -94
  39. package/cpp/core/EditorText.cpp +0 -15
  40. package/cpp/core/EditorText.h +0 -18
  41. package/cpp/react/JetMarkdownEditorShadowNode.cpp +0 -54
  42. package/cpp/react/JetMarkdownEditorShadowNode.h +0 -55
  43. package/cpp/react/JetMarkdownEditorState.h +0 -35
  44. package/cpp/tests/parser_tests.cpp +0 -662
  45. package/cpp/tests/run_tests.sh +0 -25
  46. package/ios/editor/JetMarkdownEditor.h +0 -12
  47. package/ios/editor/JetMarkdownEditor.mm +0 -2002
  48. package/lib/module/JetMarkdownEditor.js +0 -85
  49. package/lib/module/JetMarkdownEditor.js.map +0 -1
  50. package/lib/module/JetMarkdownEditor.native.js +0 -244
  51. package/lib/module/JetMarkdownEditor.native.js.map +0 -1
  52. package/lib/module/JetMarkdownEditorNativeComponent.ts +0 -173
  53. package/lib/module/useJetMarkdownEditor.js +0 -41
  54. package/lib/module/useJetMarkdownEditor.js.map +0 -1
  55. package/lib/typescript/src/JetMarkdownEditor.d.ts +0 -6
  56. package/lib/typescript/src/JetMarkdownEditor.d.ts.map +0 -1
  57. package/lib/typescript/src/JetMarkdownEditor.native.d.ts +0 -6
  58. package/lib/typescript/src/JetMarkdownEditor.native.d.ts.map +0 -1
  59. package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts +0 -105
  60. package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts.map +0 -1
  61. package/lib/typescript/src/useJetMarkdownEditor.d.ts +0 -36
  62. package/lib/typescript/src/useJetMarkdownEditor.d.ts.map +0 -1
  63. package/src/JetMarkdownEditor.native.tsx +0 -301
  64. package/src/JetMarkdownEditor.tsx +0 -90
  65. package/src/JetMarkdownEditorNativeComponent.ts +0 -173
  66. package/src/useJetMarkdownEditor.ts +0 -50
@@ -7,7 +7,6 @@
7
7
 
8
8
  #include <react/renderer/core/ConcreteComponentDescriptor.h>
9
9
 
10
- #include "../../../../../JetMarkdownEditorShadowNode.h"
11
10
  #include "../../../../../JetMarkdownShadowNode.h"
12
11
 
13
12
  namespace facebook::react {
@@ -15,7 +14,4 @@ namespace facebook::react {
15
14
  using JetMarkdownViewComponentDescriptor =
16
15
  ConcreteComponentDescriptor<JetMarkdownShadowNode>;
17
16
 
18
- using JetMarkdownEditorComponentDescriptor =
19
- ConcreteComponentDescriptor<JetMarkdownEditorShadowNode>;
20
-
21
17
  } // namespace facebook::react
@@ -41,8 +41,9 @@ static BOOL JMDIsScrollInProgress(UIView *view);
41
41
  NSString *_stylesJson;
42
42
  BOOL _allowFontScaling;
43
43
  JMDBlockStackView *_stack;
44
- NSString *_boundKey;
45
- CGFloat _boundWidth;
44
+ // Layout objects are cached per (content, width, image sizes, font
45
+ // scale), so identity says whether the bound view tree is still current.
46
+ JMDWidthLayout *_boundLayout;
46
47
  // url -> @[w, h] points: from the images prop (wins) and loaded bitmaps.
47
48
  NSMutableDictionary<NSString *, NSArray<NSNumber *> *> *_propImageSizes;
48
49
  NSMutableDictionary<NSString *, NSArray<NSNumber *> *> *_loadedImageSizes;
@@ -346,7 +347,7 @@ static void JMDSetNeedsDisplayDeep(UIView *view) {
346
347
  // Dynamic (platform) colors resolve at draw time for text, but border
347
348
  // and background colors snapshot into CGColors when blocks bind; rebind
348
349
  // so they re-resolve under the new appearance.
349
- _boundKey = nil;
350
+ _boundLayout = nil;
350
351
  [self setNeedsLayout];
351
352
  JMDSetNeedsDisplayDeep(self);
352
353
  }
@@ -449,7 +450,7 @@ static void JMDSetNeedsDisplayDeep(UIView *view) {
449
450
  [super prepareForRecycle];
450
451
  _markdown = @"";
451
452
  _stylesJson = @"";
452
- _boundKey = nil;
453
+ _boundLayout = nil;
453
454
  _state = nullptr;
454
455
  [_propImageSizes removeAllObjects];
455
456
  [_loadedImageSizes removeAllObjects];
@@ -463,9 +464,9 @@ static void JMDSetNeedsDisplayDeep(UIView *view) {
463
464
 
464
465
  const CGFloat contentWidth =
465
466
  self.bounds.size.width - styles.paddingLeft - styles.paddingRight;
466
- if (contentWidth <= 0 || _markdown.length == 0) {
467
+ if (contentWidth <= 0) {
467
468
  [_stack setBlocks:@[] gap:0];
468
- _boundKey = nil;
469
+ _boundLayout = nil;
469
470
  return;
470
471
  }
471
472
 
@@ -477,16 +478,8 @@ static void JMDSetNeedsDisplayDeep(UIView *view) {
477
478
  NSDictionary *imageSizes = [self mergedImageSizes];
478
479
  JMDWidthLayout *layout = [content layoutForWidth:contentWidth imageSizes:imageSizes];
479
480
 
480
- // Full contents, not hashes: CFString samples long strings and
481
- // NSDictionary.hash is the entry count — both collide.
482
- NSString *key = [NSString stringWithFormat:@"%@\x1f%@\x1f%@\x1f%.3f",
483
- _markdown,
484
- _stylesJson,
485
- [JMDRenderedContent keyForImageSizes:imageSizes],
486
- fontScale];
487
- if (![key isEqualToString:_boundKey] || _boundWidth != contentWidth) {
488
- _boundKey = key;
489
- _boundWidth = contentWidth;
481
+ if (layout != _boundLayout) {
482
+ _boundLayout = layout;
490
483
  [_stack setBlocks:layout.measured gap:content.gap];
491
484
  }
492
485
 
@@ -12,7 +12,7 @@
12
12
  fontScale:(CGFloat)fontScale {
13
13
  JMDStyleConfig *styles = [JMDStyleConfig configWithJson:stylesJson];
14
14
  const CGFloat contentWidth = maxWidth - styles.paddingLeft - styles.paddingRight;
15
- if (contentWidth <= 0 || markdown.length == 0) {
15
+ if (contentWidth <= 0) {
16
16
  return 0;
17
17
  }
18
18
 
@@ -92,6 +92,13 @@ FOUNDATION_EXPORT NSAttributedStringKey const JMDRunBackgroundAttributeName;
92
92
  /// Tables: resolved column widths and per-row heights.
93
93
  @property (nonatomic, strong) NSArray<NSNumber *> *columnWidths;
94
94
  @property (nonatomic, strong) NSArray<NSNumber *> *rowHeights;
95
+ /// TextKit stacks built at measure time (storage owns the layout manager
96
+ /// and container); the views draw with these, so measure and draw never
97
+ /// disagree and text is laid out once. Text/Code: textStorage. Lists:
98
+ /// one per row. Tables: row-major cells.
99
+ @property (nonatomic, strong, nullable) NSTextStorage *textStorage;
100
+ @property (nonatomic, strong) NSArray<NSTextStorage *> *markerStorages;
101
+ @property (nonatomic, strong) NSArray<NSArray<NSTextStorage *> *> *cellStorages;
95
102
  @end
96
103
 
97
104
  NS_ASSUME_NONNULL_END
@@ -21,6 +21,8 @@
21
21
  _rowContents = @[];
22
22
  _columnWidths = @[];
23
23
  _rowHeights = @[];
24
+ _markerStorages = @[];
25
+ _cellStorages = @[];
24
26
  }
25
27
  return self;
26
28
  }
@@ -186,7 +186,7 @@ NSArray<NSDictionary *> *featureSettings(NSArray<NSString *> *variants) {
186
186
  return features;
187
187
  }
188
188
 
189
- UIFont *buildFont(const ResolvedAttrs &attrs) {
189
+ UIFont *buildFontUncached(const ResolvedAttrs &attrs) {
190
190
  UIFont *base;
191
191
  if (attrs.family.length > 0) {
192
192
  UIFontDescriptor *descriptor = [UIFontDescriptor fontDescriptorWithFontAttributes:@{
@@ -228,6 +228,32 @@ UIFont *buildFont(const ResolvedAttrs &attrs) {
228
228
  return font ?: base;
229
229
  }
230
230
 
231
+ // Descriptor matching (family + weight trait, especially for custom
232
+ // families) costs real time and runs once per text run otherwise. UIFont is
233
+ // immutable, NSCache is thread-safe: the layout thread and main thread share
234
+ // one cache.
235
+ UIFont *buildFont(const ResolvedAttrs &attrs) {
236
+ static NSCache<NSString *, UIFont *> *cache;
237
+ static dispatch_once_t onceToken;
238
+ dispatch_once(&onceToken, ^{
239
+ cache = [NSCache new];
240
+ cache.countLimit = 256;
241
+ });
242
+ NSString *key = [NSString stringWithFormat:@"%@\x1f%.2f\x1f%ld\x1f%d\x1f%@",
243
+ attrs.family ?: @"",
244
+ attrs.fontSize,
245
+ (long)attrs.weight,
246
+ attrs.italic ? 1 : 0,
247
+ [attrs.variants componentsJoinedByString:@","] ?: @""];
248
+ UIFont *cached = [cache objectForKey:key];
249
+ if (cached != nil) {
250
+ return cached;
251
+ }
252
+ UIFont *font = buildFontUncached(attrs);
253
+ [cache setObject:font forKey:key];
254
+ return font;
255
+ }
256
+
231
257
  NSUnderlineStyle decorationMask(NSString *style) {
232
258
  if ([style isEqualToString:@"double"]) {
233
259
  return NSUnderlineStyleDouble;
@@ -84,12 +84,13 @@
84
84
  return layout;
85
85
  }
86
86
 
87
- - (CGSize)textSize:(NSAttributedString *)text width:(CGFloat)width {
88
- // Measured with TextKit, not NSStringDrawing: boundingRect stops at the
89
- // last line's drawn descent, dropping the bottom of the final line box
90
- // under a custom lineHeight. The full box keeps overlays (spoiler covers,
91
- // run background chips) from being clipped at the view's bottom edge and
92
- // matches how RN's <Text> and the Android renderer measure.
87
+ // Lays text out with TextKit, not NSStringDrawing: boundingRect stops at
88
+ // the last line's drawn descent, dropping the bottom of the final line box
89
+ // under a custom lineHeight. The full box keeps overlays (spoiler covers,
90
+ // run background chips) from being clipped at the view's bottom edge and
91
+ // matches how RN's <Text> and the Android renderer measure. The returned
92
+ // storage owns the layout manager; views draw with the same stack.
93
+ - (NSTextStorage *)layoutText:(NSAttributedString *)text width:(CGFloat)width {
93
94
  NSTextStorage *storage = [[NSTextStorage alloc] initWithAttributedString:text];
94
95
  NSLayoutManager *layoutManager = [NSLayoutManager new];
95
96
  NSTextContainer *container = [[NSTextContainer alloc]
@@ -98,10 +99,20 @@
98
99
  [layoutManager addTextContainer:container];
99
100
  [storage addLayoutManager:layoutManager];
100
101
  [layoutManager ensureLayoutForTextContainer:container];
101
- const CGRect used = [layoutManager usedRectForTextContainer:container];
102
+ return storage;
103
+ }
104
+
105
+ static CGSize JMDUsedSize(NSTextStorage *storage) {
106
+ NSLayoutManager *layoutManager = storage.layoutManagers.firstObject;
107
+ const CGRect used =
108
+ [layoutManager usedRectForTextContainer:layoutManager.textContainers.firstObject];
102
109
  return CGSizeMake(ceil(used.size.width), ceil(used.size.height));
103
110
  }
104
111
 
112
+ - (CGSize)textSize:(NSAttributedString *)text width:(CGFloat)width {
113
+ return JMDUsedSize([self layoutText:text width:width]);
114
+ }
115
+
105
116
  - (JMDMeasuredBlock *)measureBlock:(JMDBlock *)block
106
117
  width:(CGFloat)width
107
118
  imageSizes:(nullable NSDictionary<NSString *, NSArray<NSNumber *> *> *)imageSizes {
@@ -111,13 +122,17 @@
111
122
 
112
123
  switch (block.kind) {
113
124
  case JMDBlockKindText: {
114
- const CGSize size = [self textSize:block.attributedText width:width];
125
+ NSTextStorage *storage = [self layoutText:block.attributedText width:width];
126
+ const CGSize size = JMDUsedSize(storage);
127
+ measured.textStorage = storage;
115
128
  measured.height = size.height;
116
129
  measured.textHeight = size.height;
117
130
  break;
118
131
  }
119
132
  case JMDBlockKindCode: {
120
- const CGSize size = [self textSize:block.attributedText width:CGFLOAT_MAX];
133
+ NSTextStorage *storage = [self layoutText:block.attributedText width:CGFLOAT_MAX];
134
+ const CGSize size = JMDUsedSize(storage);
135
+ measured.textStorage = storage;
121
136
  measured.contentWidth = size.width;
122
137
  measured.textHeight = size.height;
123
138
  measured.height =
@@ -142,11 +157,14 @@
142
157
  const CGFloat contentWidth = MAX(width - contentX, 1);
143
158
  measured.contentWidth = contentWidth;
144
159
  NSMutableArray<NSNumber *> *markerHeights = [NSMutableArray new];
160
+ NSMutableArray<NSTextStorage *> *markerStorages = [NSMutableArray new];
145
161
  NSMutableArray<NSArray<JMDMeasuredBlock *> *> *rowContents = [NSMutableArray new];
146
162
  CGFloat height = 0;
147
163
  for (NSUInteger i = 0; i < block.rows.count; i++) {
148
164
  JMDListRow *row = block.rows[i];
149
- const CGSize markerSize = [self textSize:row.marker width:block.markerWidth];
165
+ NSTextStorage *markerStorage = [self layoutText:row.marker width:block.markerWidth];
166
+ [markerStorages addObject:markerStorage];
167
+ const CGSize markerSize = JMDUsedSize(markerStorage);
150
168
  NSMutableArray<JMDMeasuredBlock *> *content = [NSMutableArray new];
151
169
  for (JMDBlock *child in row.content) {
152
170
  [content addObject:[self measureBlock:child width:contentWidth imageSizes:imageSizes]];
@@ -159,6 +177,7 @@
159
177
  }
160
178
  }
161
179
  measured.markerHeights = markerHeights;
180
+ measured.markerStorages = markerStorages;
162
181
  measured.rowContents = rowContents;
163
182
  measured.height = height;
164
183
  break;
@@ -209,6 +228,7 @@
209
228
  }
210
229
 
211
230
  NSMutableArray<NSNumber *> *rowHeights = [NSMutableArray new];
231
+ NSMutableArray<NSArray<NSTextStorage *> *> *cellStorages = [NSMutableArray new];
212
232
  CGFloat contentHeight = 0;
213
233
  for (JMDTableRow *row in block.tableRows) {
214
234
  JMDLayoutStyle *rowStyle = row.isHeader ? block.headerRowStyle : block.bodyRowStyle;
@@ -217,10 +237,14 @@
217
237
  const CGFloat cellPadV = pad.top + pad.bottom;
218
238
  const CGFloat rowExtra = rowStyle.borderTopWidth + rowStyle.borderBottomWidth;
219
239
  CGFloat rowHeight = 0;
240
+ NSMutableArray<NSTextStorage *> *rowStorages = [NSMutableArray new];
220
241
  for (NSUInteger column = 0; column < row.cells.count; column++) {
221
242
  const CGFloat cellWidth = MAX(columnWidths[column] - cellPadH, 1);
222
- rowHeight = MAX(rowHeight, [self textSize:row.cells[column] width:cellWidth].height);
243
+ NSTextStorage *storage = [self layoutText:row.cells[column] width:cellWidth];
244
+ [rowStorages addObject:storage];
245
+ rowHeight = MAX(rowHeight, JMDUsedSize(storage).height);
223
246
  }
247
+ [cellStorages addObject:rowStorages];
224
248
  rowHeight += cellPadV + rowExtra;
225
249
  [rowHeights addObject:@(rowHeight)];
226
250
  contentHeight += rowHeight;
@@ -235,6 +259,7 @@
235
259
 
236
260
  measured.columnWidths = widths;
237
261
  measured.rowHeights = rowHeights;
262
+ measured.cellStorages = cellStorages;
238
263
  measured.contentWidth = contentWidth;
239
264
  measured.height = contentHeight + block.layoutStyle.verticalInset;
240
265
  break;
@@ -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;