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 +2 -1
- package/docs/BREAKING_CHANGES.md +5 -0
- package/ios/EnrichedMarkdown.mm +62 -18
- package/ios/EnrichedMarkdownText.mm +61 -33
- package/package.json +1 -1
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.
|
|
265
|
+
| **1.1.0** | ⛔ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
266
|
+
| **1.0.x** | ⛔ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
266
267
|
| **0.7.0** | ⛔ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
267
268
|
| **0.6.0** | ⛔ | ✅ | ✅ | ✅ | ⛔ | ⛔ |
|
|
268
269
|
| **0.5.0** | ⛔ | ✅ | ✅ | ✅ | ⛔ | ⛔ |
|
package/docs/BREAKING_CHANGES.md
CHANGED
|
@@ -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
|
package/ios/EnrichedMarkdown.mm
CHANGED
|
@@ -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 =
|
|
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
|
-
///
|
|
584
|
-
///
|
|
585
|
-
///
|
|
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
|
-
|
|
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
|
-
|
|
688
|
+
CGRect contentBounds = [self contentBounds];
|
|
689
|
+
if (contentBounds.size.width > 0) {
|
|
653
690
|
[self setNeedsLayout];
|
|
654
|
-
CGSize measured = [self measureSize:
|
|
655
|
-
if (needsHeightUpdate(measured,
|
|
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
|
-
|
|
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:
|
|
874
|
+
[self computeSegmentLayoutForWidth:contentBounds.size.width applyFrames:YES];
|
|
837
875
|
[self layoutIfNeeded];
|
|
838
876
|
[self requestHeightUpdate];
|
|
839
877
|
} else {
|
|
840
|
-
CGSize measured = [self measureSize:
|
|
841
|
-
if (needsHeightUpdate(measured,
|
|
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
|
|
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
|
-
|
|
1234
|
-
if (
|
|
1235
|
-
[self
|
|
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
|
-
///
|
|
190
|
-
///
|
|
191
|
-
///
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
448
|
-
//
|
|
449
|
-
//
|
|
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
|
-
|
|
510
|
+
CGRect contentBounds = [self contentBounds];
|
|
511
|
+
CGFloat containerWidth = contentBounds.size.width;
|
|
481
512
|
if (containerWidth <= 0) {
|
|
482
|
-
containerWidth =
|
|
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 (
|
|
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:
|
|
517
|
-
if (forceHeightUpdate || needsHeightUpdate(measured,
|
|
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
|
-
|
|
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
|
-
|
|
750
|
-
if (
|
|
751
|
-
[self
|
|
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