react-native-enriched-markdown 1.1.0-nightly-20260930-7bcab4e4a → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -262,7 +262,8 @@ We're actively working on expanding the capabilities of `react-native-enriched-m
262
262
  | | 0.82 | 0.83 | 0.84 | 0.85 | 0.86 | 0.87 |
263
263
  | ----------- | :--: | :--: | :--: | :--: | :--: | :--: |
264
264
  | **nightly** | ⛔ | ✅ | ✅ | ✅ | ✅ | ✅ |
265
- | **1.0.0** | ⛔ | ✅ | ✅ | ✅ | ✅ | ✅ |
265
+ | **1.1.0** | ⛔ | ✅ | ✅ | ✅ | ✅ | ✅ |
266
+ | **1.0.x** | ⛔ | ✅ | ✅ | ✅ | ✅ | ✅ |
266
267
  | **0.7.0** | ⛔ | ✅ | ✅ | ✅ | ✅ | ✅ |
267
268
  | **0.6.0** | ⛔ | ✅ | ✅ | ✅ | ⛔ | ⛔ |
268
269
  | **0.5.0** | ⛔ | ✅ | ✅ | ✅ | ⛔ | ⛔ |
@@ -5,6 +5,11 @@ This file tracks breaking changes and how to migrate for each. Entries are newes
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## v1.1.0
9
+
10
+ No breaking changes. v1.1.0 is additive over v1.0.2 - every public prop, method, event and style key
11
+ from v1.0.2 keeps its name and behaviour, and no deprecation was introduced or removed.
12
+
8
13
  ## v1.0.2
9
14
 
10
15
  ### The Expo config plugin was removed
@@ -404,6 +404,32 @@ static char kENRMSegmentFadeAnimatorKey;
404
404
  return view;
405
405
  }
406
406
 
407
+ /// The box the segments actually live in: the component frame inset by the
408
+ /// `containerStyle` border and padding (`LayoutMetrics::getContentFrame`).
409
+ ///
410
+ /// Yoga hands `measureContent` an available size that already excludes those
411
+ /// insets and then adds them back to the frame it commits, so laying the
412
+ /// segments out against `self.bounds` would stack them over the padding and
413
+ /// across the border while the space Yoga reserved for the content stays blank
414
+ /// at the bottom. Unlike `EnrichedMarkdownText`, the segments are attached to
415
+ /// the component view itself rather than to `contentView`, so nothing applies
416
+ /// the insets for us.
417
+ ///
418
+ /// Taken from `_layoutMetrics` rather than recomputed off `self.bounds`:
419
+ /// `prepareForRecycle` resets the metrics but leaves the bounds behind, so the
420
+ /// two do not always agree. The box is empty both before the first layout
421
+ /// metrics arrive and when the insets consume the whole component width; neither
422
+ /// may fall back to `self.bounds`, which is the padded border box the segments
423
+ /// must not be drawn across, so an over-constrained box clamps to zero and
424
+ /// callers skip laying out at all.
425
+ - (CGRect)contentBounds
426
+ {
427
+ CGRect box = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
428
+ box.size.width = MAX(box.size.width, 0);
429
+ box.size.height = MAX(box.size.height, 0);
430
+ return box;
431
+ }
432
+
407
433
  - (CGSize)computeSegmentLayoutForWidth:(CGFloat)width applyFrames:(BOOL)applyFrames
408
434
  {
409
435
  if (_segmentViews.count == 0)
@@ -435,6 +461,10 @@ static char kENRMSegmentFadeAnimatorKey;
435
461
  __block CGFloat yOffset = 0.0;
436
462
  __block CGFloat maxContentWidth = 0.0;
437
463
  const NSUInteger lastIndex = _segmentViews.count - 1;
464
+ // Frames are stacked in the component's own coordinate space, so the content
465
+ // origin (containerStyle border + padding) shifts every segment. yOffset stays
466
+ // content-relative because it doubles as the measured content height.
467
+ const CGPoint contentOrigin = applyFrames ? [self contentBounds].origin : CGPointZero;
438
468
 
439
469
  [_segmentViews enumerateObjectsUsingBlock:^(RCTUIView *segment, NSUInteger i, BOOL *stop) {
440
470
  const BOOL isLast = (i == lastIndex);
@@ -480,16 +510,16 @@ static char kENRMSegmentFadeAnimatorKey;
480
510
  #endif
481
511
 
482
512
  if (applyFrames) {
483
- CGFloat segmentX = 0;
513
+ CGFloat segmentX = contentOrigin.x;
484
514
  CGFloat segmentWidth = width;
485
515
  if (isTable) {
486
516
  CGFloat overhang = MAX(_config.tableHorizontalOverflow, 0);
487
517
  if (overhang > 0) {
488
- segmentX = -overhang;
518
+ segmentX = contentOrigin.x - overhang;
489
519
  segmentWidth = width + overhang * 2;
490
520
  }
491
521
  }
492
- CGRect segmentFrame = CGRectMake(segmentX, yOffset, segmentWidth, segmentHeight);
522
+ CGRect segmentFrame = CGRectMake(segmentX, contentOrigin.y + yOffset, segmentWidth, segmentHeight);
493
523
  segment.frame = segmentFrame;
494
524
  #if TARGET_OS_OSX
495
525
  if ([segment isKindOfClass:[EnrichedMarkdownInternalText class]]) {
@@ -580,15 +610,21 @@ static char kENRMSegmentFadeAnimatorKey;
580
610
  }
581
611
  }
582
612
 
583
- /// Yoga-resolved direction inherited from any ancestor `direction` style.
584
- /// In FirstStrong mode this feeds the neutral-paragraph fallback, so a change
585
- /// requires segment recreation.
613
+ /// Publishes the committed size to the streaming fast path's mailbox, and picks
614
+ /// up the Yoga-resolved direction inherited from any ancestor `direction` style.
615
+ /// In FirstStrong mode the direction feeds the neutral-paragraph fallback, so a
616
+ /// change requires segment recreation.
617
+ ///
618
+ /// The mailbox holds a content size, not the border box: `measureContent`
619
+ /// returns it straight to Yoga, which adds the `containerStyle` insets back on
620
+ /// top of whatever it gets, so storing `frame.size` would double-count them.
586
621
  - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
587
622
  oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
588
623
  {
589
624
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
590
625
 
591
- _lastCommittedSize.store(CGSizeMake(layoutMetrics.frame.size.width, layoutMetrics.frame.size.height));
626
+ const auto contentSize = layoutMetrics.getContentFrame().size;
627
+ _lastCommittedSize.store(CGSizeMake(contentSize.width, contentSize.height));
592
628
 
593
629
  NSWritingDirection resolved = _resolvedLayoutDirection;
594
630
  if (layoutMetrics.layoutDirection == LayoutDirection::RightToLeft) {
@@ -649,10 +685,11 @@ static char kENRMSegmentFadeAnimatorKey;
649
685
  MeasurementCache::shared().removeMatchingMarkdown(std::string(_renderedMarkdown.UTF8String));
650
686
  }
651
687
 
652
- if (self.bounds.size.width > 0) {
688
+ CGRect contentBounds = [self contentBounds];
689
+ if (contentBounds.size.width > 0) {
653
690
  [self setNeedsLayout];
654
- CGSize measured = [self measureSize:self.bounds.size.width];
655
- if (needsHeightUpdate(measured, self.bounds)) {
691
+ CGSize measured = [self measureSize:contentBounds.size.width];
692
+ if (needsHeightUpdate(measured, contentBounds)) {
656
693
  [self requestHeightUpdate];
657
694
  }
658
695
  }
@@ -829,16 +866,17 @@ static char kENRMSegmentFadeAnimatorKey;
829
866
  }
830
867
  }];
831
868
 
832
- if (self.bounds.size.width > 0) {
869
+ CGRect contentBounds = [self contentBounds];
870
+ if (contentBounds.size.width > 0) {
833
871
  [self setNeedsLayout];
834
872
 
835
873
  if (forceHeightUpdate || segmentTopologyChanged) {
836
- [self computeSegmentLayoutForWidth:self.bounds.size.width applyFrames:YES];
874
+ [self computeSegmentLayoutForWidth:contentBounds.size.width applyFrames:YES];
837
875
  [self layoutIfNeeded];
838
876
  [self requestHeightUpdate];
839
877
  } else {
840
- CGSize measured = [self measureSize:self.bounds.size.width];
841
- if (needsHeightUpdate(measured, self.bounds)) {
878
+ CGSize measured = [self measureSize:contentBounds.size.width];
879
+ if (needsHeightUpdate(measured, contentBounds)) {
842
880
  [self requestHeightUpdate];
843
881
  }
844
882
  }
@@ -1012,7 +1050,10 @@ static char kENRMSegmentFadeAnimatorKey;
1012
1050
  - (void)layoutSubviews
1013
1051
  {
1014
1052
  [super layoutSubviews];
1015
- [self computeSegmentLayoutForWidth:self.bounds.size.width applyFrames:YES];
1053
+ CGFloat contentWidth = [self contentBounds].size.width;
1054
+ if (contentWidth > 0) {
1055
+ [self computeSegmentLayoutForWidth:contentWidth applyFrames:YES];
1056
+ }
1016
1057
  }
1017
1058
 
1018
1059
  - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
@@ -1230,9 +1271,12 @@ static char kENRMSegmentFadeAnimatorKey;
1230
1271
  }
1231
1272
  }
1232
1273
 
1233
- CGSize measured = [self measureSize:self.bounds.size.width];
1234
- if (needsHeightUpdate(measured, self.bounds)) {
1235
- [self requestHeightUpdate];
1274
+ CGRect contentBounds = [self contentBounds];
1275
+ if (contentBounds.size.width > 0) {
1276
+ CGSize measured = [self measureSize:contentBounds.size.width];
1277
+ if (needsHeightUpdate(measured, contentBounds)) {
1278
+ [self requestHeightUpdate];
1279
+ }
1236
1280
  }
1237
1281
  }
1238
1282
  }
@@ -151,6 +151,34 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
151
151
 
152
152
  #pragma mark - Measuring and State
153
153
 
154
+ /// The box the text actually lives in: `self.bounds` inset by the
155
+ /// `containerStyle` border and padding.
156
+ ///
157
+ /// Yoga hands `measureContent` an available size that already excludes those
158
+ /// insets, and `RCTViewComponentView` sizes `contentView` (our text view) to
159
+ /// `layoutMetrics.getContentFrame()`, so the shadow node's height and the text
160
+ /// view's frame are both content-box quantities. Measuring at `self.bounds`
161
+ /// instead overshoots by the horizontal insets, and because
162
+ /// `ENRMMeasureTextLayout` measures by resizing the live display container and
163
+ /// leaves it there (UIKit never re-syncs it - `widthTracksTextView` does not
164
+ /// apply with `scrollEnabled = NO`), the overshoot becomes the layout the user
165
+ /// sees: glyphs spill past the text view and get clipped, while the taller
166
+ /// content-width height Yoga committed shows up as blank space at the bottom.
167
+ ///
168
+ /// The text view's frame is authoritative, so this is zero-sized both before the
169
+ /// first layout metrics arrive and when the insets consume the whole component
170
+ /// width. Neither may fall back to `self.bounds`: that is the padded border box,
171
+ /// and in the second case it is a real layout the text would then be drawn
172
+ /// across. Callers skip measuring and laying out at a zero width instead.
173
+ ///
174
+ /// The frame, not the bounds: a `UITextView` is a scroll view, so its
175
+ /// `bounds.origin` is the content offset rather than the inset the segments of
176
+ /// `EnrichedMarkdown` get from the matching method.
177
+ - (CGRect)contentBounds
178
+ {
179
+ return _textView.frame;
180
+ }
181
+
154
182
  - (CGSize)measureSize:(CGFloat)maxWidth
155
183
  {
156
184
  CGSize size = ENRMMeasureMarkdownText(_textView, maxWidth, _config, _allowTrailingMargin, _lastElementMarginBottom);
@@ -186,15 +214,21 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
186
214
  }
187
215
  }
188
216
 
189
- /// Yoga-resolved direction inherited from any ancestor `direction` style.
190
- /// In FirstStrong mode this feeds the neutral-paragraph fallback, so a change
191
- /// requires a re-render of the cached markdown.
217
+ /// Publishes the committed size to the streaming fast path's mailbox, and picks
218
+ /// up the Yoga-resolved direction inherited from any ancestor `direction` style.
219
+ /// In FirstStrong mode the direction feeds the neutral-paragraph fallback, so a
220
+ /// change requires a re-render of the cached markdown.
221
+ ///
222
+ /// The mailbox holds a content size, not the border box: `measureContent`
223
+ /// returns it straight to Yoga, which adds the `containerStyle` insets back on
224
+ /// top of whatever it gets, so storing `frame.size` would double-count them.
192
225
  - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
193
226
  oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
194
227
  {
195
228
  [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
196
229
 
197
- _lastCommittedSize.store(CGSizeMake(layoutMetrics.frame.size.width, layoutMetrics.frame.size.height));
230
+ const auto contentSize = layoutMetrics.getContentFrame().size;
231
+ _lastCommittedSize.store(CGSizeMake(contentSize.width, contentSize.height));
198
232
 
199
233
  NSWritingDirection resolved = _resolvedLayoutDirection;
200
234
  if (layoutMetrics.layoutDirection == LayoutDirection::RightToLeft) {
@@ -225,9 +259,10 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
225
259
  facebook::react::MeasurementCache::shared().removeMatchingMarkdown(std::string(_renderedMarkdown.UTF8String));
226
260
  }
227
261
 
228
- if (self.bounds.size.width > 0) {
229
- CGSize measured = [self measureSize:self.bounds.size.width];
230
- if (needsHeightUpdate(measured, self.bounds)) {
262
+ CGRect contentBounds = [self contentBounds];
263
+ if (contentBounds.size.width > 0) {
264
+ CGSize measured = [self measureSize:contentBounds.size.width];
265
+ if (needsHeightUpdate(measured, contentBounds)) {
231
266
  [self requestHeightUpdate];
232
267
  }
233
268
  }
@@ -444,17 +479,12 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
444
479
  }
445
480
 
446
481
  // Kept in sync with the view-free measurement so the rendered line count matches
447
- // the measured height. The clamp must be computed at the padding-inset content
448
- // width (the text view's own width) rather than the full component bounds, or a
449
- // full-width measurement pass leaves the truncation laid out too wide and fewer
450
- // lines render than were measured. numberOfLines == 0 restores the unlimited default.
482
+ // the measured height: both clamp a layout performed at the content width (see
483
+ // contentBounds), so the truncation falls on the same word.
484
+ // numberOfLines == 0 restores the unlimited default.
451
485
  - (void)applyLineClampToTextContainer
452
486
  {
453
487
  if (_numberOfLines > 0) {
454
- CGFloat contentWidth = _textView.bounds.size.width;
455
- if (contentWidth > 0) {
456
- _textView.textContainer.size = CGSizeMake(contentWidth, CGFLOAT_MAX);
457
- }
458
488
  _textView.textContainer.maximumNumberOfLines = _numberOfLines;
459
489
  _textView.textContainer.lineBreakMode = _ellipsizeLineBreakMode;
460
490
  } else {
@@ -477,9 +507,10 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
477
507
  // Ensure the text container has unlimited height before setting content.
478
508
  // updateLayoutMetrics may have shrunk the frame (and thus the text container)
479
509
  // from a previous layout pass, which would clip the new attributed text.
480
- CGFloat containerWidth = _textView.textContainer.size.width;
510
+ CGRect contentBounds = [self contentBounds];
511
+ CGFloat containerWidth = contentBounds.size.width;
481
512
  if (containerWidth <= 0) {
482
- containerWidth = self.bounds.size.width;
513
+ containerWidth = _textView.textContainer.size.width;
483
514
  }
484
515
  _textView.textContainer.size = CGSizeMake(containerWidth, CGFLOAT_MAX);
485
516
  [self applyLineClampToTextContainer];
@@ -499,7 +530,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
499
530
  // that corrupts the height sent to Yoga.
500
531
  [_spoilerManager setNeedsUpdate];
501
532
 
502
- if (self.bounds.size.width > 0) {
533
+ if (contentBounds.size.width > 0) {
503
534
  // Font/style changes can produce the same measured size before UIKit has
504
535
  // fully refreshed layout, so force one Yoga update after those renders.
505
536
  BOOL forceHeightUpdate = _forceHeightUpdateOnNextRender;
@@ -513,19 +544,10 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
513
544
 
514
545
  [_spoilerManager updateIfNeeded];
515
546
 
516
- CGSize measured = [self measureSize:self.bounds.size.width];
517
- if (forceHeightUpdate || needsHeightUpdate(measured, self.bounds)) {
547
+ CGSize measured = [self measureSize:contentBounds.size.width];
548
+ if (forceHeightUpdate || needsHeightUpdate(measured, contentBounds)) {
518
549
  [self requestHeightUpdate];
519
550
  }
520
-
521
- // measureSize lays the shared display container out at the full bounds width;
522
- // re-pin the clamp to the content width so the visible truncation matches the
523
- // content-width line count the shadow node measured.
524
- if (_numberOfLines > 0) {
525
- [self applyLineClampToTextContainer];
526
- [_textView.layoutManager ensureLayoutForTextContainer:_textView.textContainer];
527
- ENRMSetNeedsDisplay(_textView);
528
- }
529
551
  }
530
552
 
531
553
  if (_textView.hidden) {
@@ -741,14 +763,20 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
741
763
 
742
764
  if (self.window && _renderedMarkdown != nil) {
743
765
  _textView.hidden = NO;
744
- ENRMRefreshTextViewAfterWindowAttach(_textView, self.bounds);
766
+ // Refresh in place: RCTViewComponentView owns the text view's frame and has
767
+ // already set it to the content frame, so re-assigning self.bounds here would
768
+ // drop the containerStyle insets (see contentBounds).
769
+ ENRMRefreshTextViewLayout(_textView);
745
770
 
746
771
  [_spoilerManager setNeedsUpdate];
747
772
  [_spoilerManager updateIfNeeded];
748
773
 
749
- CGSize measured = [self measureSize:self.bounds.size.width];
750
- if (needsHeightUpdate(measured, self.bounds)) {
751
- [self requestHeightUpdate];
774
+ CGRect contentBounds = [self contentBounds];
775
+ if (contentBounds.size.width > 0) {
776
+ CGSize measured = [self measureSize:contentBounds.size.width];
777
+ if (needsHeightUpdate(measured, contentBounds)) {
778
+ [self requestHeightUpdate];
779
+ }
752
780
  }
753
781
  }
754
782
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-enriched-markdown",
3
- "version": "1.1.0-nightly-20260930-7bcab4e4a",
3
+ "version": "1.1.0",
4
4
  "description": "Markdown-based Rich Text solution for React Native",
5
5
  "main": "./lib/module/index",
6
6
  "module": "./lib/module/index",