From 7d496822d2acb04257934cb89d268616ab8fe414 Mon Sep 17 00:00:00 2001 From: Ernest Date: Wed, 3 Jun 2026 10:58:25 +0200 Subject: [PATCH 1/5] fix(android): unify line break strategy across measuring and drawing --- .../enriched/markdown/MeasurementStore.kt | 17 ++++++++++------- .../markdown/utils/common/BreakStrategyUtils.kt | 17 +++++++++++++++++ .../markdown/utils/text/view/TextViewSetup.kt | 4 ++++ 3 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 android/src/main/java/com/swmansion/enriched/markdown/utils/common/BreakStrategyUtils.kt diff --git a/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt b/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt index a6490d12..ae83ec7c 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt @@ -2,7 +2,6 @@ package com.swmansion.enriched.markdown import android.content.Context import android.graphics.Typeface -import android.graphics.text.LineBreaker import android.os.Build import android.text.SpannableString import android.text.StaticLayout @@ -27,6 +26,7 @@ import com.swmansion.enriched.markdown.utils.common.TableStreamingMode import com.swmansion.enriched.markdown.utils.common.getBooleanOrDefault import com.swmansion.enriched.markdown.utils.common.getMapOrNull import com.swmansion.enriched.markdown.utils.common.getStringOrDefault +import com.swmansion.enriched.markdown.utils.common.resolveBreakStrategy import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments import com.swmansion.enriched.markdown.utils.text.extensions.replaceMathSpansWithPlaceholders import com.swmansion.enriched.markdown.views.TableContainerView @@ -461,8 +461,9 @@ object MeasurementStore { .setIncludePad(false) .setLineSpacing(0f, 1f) .apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + @Suppress("WrongConstant") + setBreakStrategy(resolveBreakStrategy()) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { setUseLineSpacingFromFallbacks(true) @@ -539,8 +540,9 @@ object MeasurementStore { .setIncludePad(false) .setLineSpacing(0f, 1f) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - builder.setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + @Suppress("WrongConstant") + builder.setBreakStrategy(resolveBreakStrategy()) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -576,8 +578,9 @@ object MeasurementStore { .setIncludePad(false) .setLineSpacing(0f, 1f) .apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + @Suppress("WrongConstant") + setBreakStrategy(resolveBreakStrategy()) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { setUseLineSpacingFromFallbacks(true) diff --git a/android/src/main/java/com/swmansion/enriched/markdown/utils/common/BreakStrategyUtils.kt b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/BreakStrategyUtils.kt new file mode 100644 index 00000000..11978f0c --- /dev/null +++ b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/BreakStrategyUtils.kt @@ -0,0 +1,17 @@ +package com.swmansion.enriched.markdown.utils.common + +import android.text.Layout + +/** + * Resolves the break strategy for StaticLayout and TextView. + * + * Both MeasurementStore and the rendered TextView must use the same value — + * a mismatch causes the measured line count to differ from the rendered line + * count, which results in the view being sized incorrectly and ScrollingMovementMethod + * (inherited via LinkMovementMethod) silently scrolling the overflow. + * + * When adding a lineBreakStrategy prop: read it here from props/style and apply + * the result in both MeasurementStore (StaticLayout.Builder) and TextViewSetup + * (TextView.breakStrategy). Both call this function, so updating it here is enough. + */ +fun resolveBreakStrategy(): Int = Layout.BREAK_STRATEGY_SIMPLE diff --git a/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/TextViewSetup.kt b/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/TextViewSetup.kt index 1feab30e..a1843dfc 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/TextViewSetup.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/TextViewSetup.kt @@ -6,10 +6,14 @@ import android.view.textclassifier.TextClassifier import androidx.appcompat.widget.AppCompatTextView import androidx.core.view.ViewCompat import com.swmansion.enriched.markdown.accessibility.AccessibleMarkdownTextView +import com.swmansion.enriched.markdown.utils.common.resolveBreakStrategy fun AccessibleMarkdownTextView.setupAsMarkdownTextView() { setBackgroundColor(Color.TRANSPARENT) includeFontPadding = false + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + breakStrategy = resolveBreakStrategy() + } movementMethod = LinkLongPressMovementMethod.createInstance() setTextIsSelectable(true) customSelectionActionModeCallback = createSelectionActionModeCallback(this) From b4954465950501e280b153b51e281029ce2dd910 Mon Sep 17 00:00:00 2001 From: Ernest Date: Wed, 3 Jun 2026 11:37:54 +0200 Subject: [PATCH 2/5] docs: updated function comment --- .../markdown/utils/common/BreakStrategyUtils.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/markdown/utils/common/BreakStrategyUtils.kt b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/BreakStrategyUtils.kt index 11978f0c..7ed06ab1 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/utils/common/BreakStrategyUtils.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/BreakStrategyUtils.kt @@ -5,13 +5,21 @@ import android.text.Layout /** * Resolves the break strategy for StaticLayout and TextView. * - * Both MeasurementStore and the rendered TextView must use the same value — - * a mismatch causes the measured line count to differ from the rendered line - * count, which results in the view being sized incorrectly and ScrollingMovementMethod - * (inherited via LinkMovementMethod) silently scrolling the overflow. + * Both Measurement (via MeasurementStore) and the render (via rendered TextView) + * must use the same value - a mismatch causes the measured line count to differ + * from the rendered line count, which results in the view being sized incorrectly + * and ScrollingMovementMethod (inherited via LinkMovementMethod) silently + * scrolling the overflow. * * When adding a lineBreakStrategy prop: read it here from props/style and apply * the result in both MeasurementStore (StaticLayout.Builder) and TextViewSetup * (TextView.breakStrategy). Both call this function, so updating it here is enough. + * + * Note: call sites in StaticLayout.Builder suppress "WrongConstant" lint. This is + * intentional - Layout.BREAK_STRATEGY_SIMPLE and LineBreaker.BREAK_STRATEGY_SIMPLE + * are the same integer (0), but the @IntDef annotation on StaticLayout.Builder + * .setBreakStrategy() was changed from Layout.* to LineBreaker.* in API 29. + * The suppression is safe; if this function is updated to return a LineBreaker.* + * constant (requires API 29 guard), the suppression can be removed. */ fun resolveBreakStrategy(): Int = Layout.BREAK_STRATEGY_SIMPLE From 57b21fef1a54231dafcb9c5a427fc0c1af81056a Mon Sep 17 00:00:00 2001 From: Ernest Date: Mon, 8 Jun 2026 16:22:58 +0200 Subject: [PATCH 3/5] feat: wip - add line break stategy props --- .../enriched/markdown/EnrichedMarkdown.kt | 12 ++++++++ .../markdown/EnrichedMarkdownManager.kt | 16 +++++++++++ .../enriched/markdown/EnrichedMarkdownText.kt | 10 +++++++ .../markdown/EnrichedMarkdownTextManager.kt | 16 +++++++++++ .../enriched/markdown/MeasurementStore.kt | 8 +++--- .../utils/common/BreakStrategyUtils.kt | 28 ++++++++++++++----- .../markdown/utils/text/view/TextViewSetup.kt | 4 +-- ios/EnrichedMarkdown.mm | 10 ++++++- ios/EnrichedMarkdownText.mm | 10 ++++++- ios/utils/ParagraphStyleUtils.h | 2 ++ ios/utils/ParagraphStyleUtils.m | 15 ++++++++++ src/EnrichedMarkdownNativeComponent.ts | 12 ++++++++ src/EnrichedMarkdownTextNativeComponent.ts | 12 ++++++++ src/native/EnrichedMarkdownText.tsx | 4 +++ src/types/MarkdownTextProps.ts | 22 +++++++++++++++ 15 files changed, 166 insertions(+), 15 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt index aabf99e8..3a1eed77 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt @@ -14,6 +14,7 @@ import com.swmansion.enriched.markdown.parser.Md4cFlags import com.swmansion.enriched.markdown.parser.Parser import com.swmansion.enriched.markdown.spoiler.SpoilerOverlay import com.swmansion.enriched.markdown.styles.StyleConfig +import com.swmansion.enriched.markdown.utils.common.BreakStrategyUtils import com.swmansion.enriched.markdown.utils.common.FeatureFlags import com.swmansion.enriched.markdown.utils.common.MarkdownSegmentRenderer import com.swmansion.enriched.markdown.utils.common.RenderedSegment @@ -185,6 +186,17 @@ class EnrichedMarkdown applySelectionColorsToSegments() } + fun setTextBreakStrategy(strategy: String) { + BreakStrategyUtils.setStrategy(strategy) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + segmentViews.filterIsInstance().forEach { + it.breakStrategy = BreakStrategyUtils.resolveBreakStrategy() + } + } + dirtyFlags += DirtyFlag.FORCE_HEIGHT + renderPending = true + } + private fun applySelectionColorsToSegments() { segmentViews.filterIsInstance().forEach { it.applySelectionColors(selectionColor, selectionHandleColor) diff --git a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt index 457d5e02..8408babb 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt @@ -148,6 +148,14 @@ class EnrichedMarkdownManager : // No-op on Android — only used on iOS } + @ReactProp(name = "lineBreakStrategyIOS") + override fun setLineBreakStrategyIOS( + view: EnrichedMarkdown?, + strategy: String?, + ) { + // No-op on Android — only used on iOS + } + @ReactProp(name = "streamingAnimation", defaultBoolean = false) override fun setStreamingAnimation( view: EnrichedMarkdown?, @@ -178,6 +186,14 @@ class EnrichedMarkdownManager : view?.spoilerOverlay = SpoilerOverlay.fromString(mode) } + @ReactProp(name = "textBreakStrategy") + override fun setTextBreakStrategy( + view: EnrichedMarkdown?, + strategy: String?, + ) { + view?.setTextBreakStrategy(strategy ?: "simple") + } + @ReactProp(name = "contextMenuItems") override fun setContextMenuItems( view: EnrichedMarkdown?, diff --git a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt index 5104ff80..e218b43d 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt @@ -19,6 +19,7 @@ import com.swmansion.enriched.markdown.spoiler.SpoilerCapable import com.swmansion.enriched.markdown.spoiler.SpoilerOverlay import com.swmansion.enriched.markdown.spoiler.SpoilerOverlayDrawer import com.swmansion.enriched.markdown.styles.StyleConfig +import com.swmansion.enriched.markdown.utils.common.BreakStrategyUtils import com.swmansion.enriched.markdown.utils.text.TailFadeInAnimator import com.swmansion.enriched.markdown.utils.text.interaction.CheckboxTouchHelper import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod @@ -290,6 +291,15 @@ class EnrichedMarkdownText applySelectionColors(selectionColor, selectionHandleColor) } + fun setTextBreakStrategy(strategy: String) { + BreakStrategyUtils.setStrategy(strategy) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + breakStrategy = BreakStrategyUtils.resolveBreakStrategy() + } + MeasurementStore.invalidate(id) + scheduleRenderIfNeeded() + } + fun emitOnLinkPress(url: String) { emitLinkPressEvent(url) } diff --git a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt index 037cb17e..27d85aa4 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt @@ -154,6 +154,14 @@ class EnrichedMarkdownTextManager : // No-op on Android — only used on iOS } + @ReactProp(name = "lineBreakStrategyIOS") + override fun setLineBreakStrategyIOS( + view: EnrichedMarkdownText?, + strategy: String?, + ) { + // No-op on Android — only used on iOS + } + @ReactProp(name = "streamingAnimation", defaultBoolean = false) override fun setStreamingAnimation( view: EnrichedMarkdownText?, @@ -178,6 +186,14 @@ class EnrichedMarkdownTextManager : view?.spoilerOverlay = SpoilerOverlay.fromString(mode) } + @ReactProp(name = "textBreakStrategy") + override fun setTextBreakStrategy( + view: EnrichedMarkdownText?, + strategy: String?, + ) { + view?.setTextBreakStrategy(strategy ?: "simple") + } + @ReactProp(name = "contextMenuItems") override fun setContextMenuItems( view: EnrichedMarkdownText?, diff --git a/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt b/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt index ae83ec7c..fe781ffb 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt @@ -18,6 +18,7 @@ import com.swmansion.enriched.markdown.spans.MathMeasureRequest import com.swmansion.enriched.markdown.spans.MathMetrics import com.swmansion.enriched.markdown.spans.MathRenderMode import com.swmansion.enriched.markdown.styles.StyleConfig +import com.swmansion.enriched.markdown.utils.common.BreakStrategyUtils import com.swmansion.enriched.markdown.utils.common.FeatureFlags import com.swmansion.enriched.markdown.utils.common.MarkdownSegmentRenderer import com.swmansion.enriched.markdown.utils.common.RenderedSegment @@ -26,7 +27,6 @@ import com.swmansion.enriched.markdown.utils.common.TableStreamingMode import com.swmansion.enriched.markdown.utils.common.getBooleanOrDefault import com.swmansion.enriched.markdown.utils.common.getMapOrNull import com.swmansion.enriched.markdown.utils.common.getStringOrDefault -import com.swmansion.enriched.markdown.utils.common.resolveBreakStrategy import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments import com.swmansion.enriched.markdown.utils.text.extensions.replaceMathSpansWithPlaceholders import com.swmansion.enriched.markdown.views.TableContainerView @@ -463,7 +463,7 @@ object MeasurementStore { .apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { @Suppress("WrongConstant") - setBreakStrategy(resolveBreakStrategy()) + setBreakStrategy(BreakStrategyUtils.resolveBreakStrategy()) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { setUseLineSpacingFromFallbacks(true) @@ -542,7 +542,7 @@ object MeasurementStore { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { @Suppress("WrongConstant") - builder.setBreakStrategy(resolveBreakStrategy()) + builder.setBreakStrategy(BreakStrategyUtils.resolveBreakStrategy()) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -580,7 +580,7 @@ object MeasurementStore { .apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { @Suppress("WrongConstant") - setBreakStrategy(resolveBreakStrategy()) + setBreakStrategy(BreakStrategyUtils.resolveBreakStrategy()) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { setUseLineSpacingFromFallbacks(true) diff --git a/android/src/main/java/com/swmansion/enriched/markdown/utils/common/BreakStrategyUtils.kt b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/BreakStrategyUtils.kt index 7ed06ab1..108071e4 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/utils/common/BreakStrategyUtils.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/BreakStrategyUtils.kt @@ -3,7 +3,7 @@ package com.swmansion.enriched.markdown.utils.common import android.text.Layout /** - * Resolves the break strategy for StaticLayout and TextView. + * Singleton that resolves the break strategy for StaticLayout and TextView. * * Both Measurement (via MeasurementStore) and the render (via rendered TextView) * must use the same value - a mismatch causes the measured line count to differ @@ -11,15 +11,29 @@ import android.text.Layout * and ScrollingMovementMethod (inherited via LinkMovementMethod) silently * scrolling the overflow. * - * When adding a lineBreakStrategy prop: read it here from props/style and apply - * the result in both MeasurementStore (StaticLayout.Builder) and TextViewSetup - * (TextView.breakStrategy). Both call this function, so updating it here is enough. + * The strategy is set from the `textBreakStrategy` prop via the view's setter, + * which updates this object before invalidating measurement and triggering a + * re-render. Both MeasurementStore (StaticLayout.Builder) and TextViewSetup + * (TextView.breakStrategy) call resolveBreakStrategy(), so updating it here + * is enough for both paths. * * Note: call sites in StaticLayout.Builder suppress "WrongConstant" lint. This is * intentional - Layout.BREAK_STRATEGY_SIMPLE and LineBreaker.BREAK_STRATEGY_SIMPLE * are the same integer (0), but the @IntDef annotation on StaticLayout.Builder * .setBreakStrategy() was changed from Layout.* to LineBreaker.* in API 29. - * The suppression is safe; if this function is updated to return a LineBreaker.* - * constant (requires API 29 guard), the suppression can be removed. + * The suppression is safe; the Layout.* constants share the same integer values. */ -fun resolveBreakStrategy(): Int = Layout.BREAK_STRATEGY_SIMPLE +object BreakStrategyUtils { + private var strategy: String = "simple" + + fun setStrategy(newStrategy: String?) { + strategy = newStrategy ?: "simple" + } + + fun resolveBreakStrategy(): Int = + when (strategy) { + "highQuality" -> Layout.BREAK_STRATEGY_HIGH_QUALITY + "balanced" -> Layout.BREAK_STRATEGY_BALANCED + else -> Layout.BREAK_STRATEGY_SIMPLE + } +} diff --git a/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/TextViewSetup.kt b/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/TextViewSetup.kt index a1843dfc..de379fbf 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/TextViewSetup.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/TextViewSetup.kt @@ -6,13 +6,13 @@ import android.view.textclassifier.TextClassifier import androidx.appcompat.widget.AppCompatTextView import androidx.core.view.ViewCompat import com.swmansion.enriched.markdown.accessibility.AccessibleMarkdownTextView -import com.swmansion.enriched.markdown.utils.common.resolveBreakStrategy +import com.swmansion.enriched.markdown.utils.common.BreakStrategyUtils fun AccessibleMarkdownTextView.setupAsMarkdownTextView() { setBackgroundColor(Color.TRANSPARENT) includeFontPadding = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - breakStrategy = resolveBreakStrategy() + breakStrategy = BreakStrategyUtils.resolveBreakStrategy() } movementMethod = LinkLongPressMovementMethod.createInstance() setTextIsSelectable(true) diff --git a/ios/EnrichedMarkdown.mm b/ios/EnrichedMarkdown.mm index 50540a2c..b57e8d17 100644 --- a/ios/EnrichedMarkdown.mm +++ b/ios/EnrichedMarkdown.mm @@ -28,6 +28,7 @@ #import "MarkdownAccessibilityElementBuilder.h" #import "MarkdownExtractor.h" #import "MeasurementCache.h" +#import "ParagraphStyleUtils.h" #import "RenderedMarkdownSegment.h" #import "RuntimeKeys.h" #import "SegmentReconciler.h" @@ -750,8 +751,15 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & } } + BOOL lineBreakStrategyChanged = newViewProps.lineBreakStrategyIOS != oldViewProps.lineBreakStrategyIOS; + if (lineBreakStrategyChanged) { + NSString *strategy = [[NSString alloc] initWithUTF8String:newViewProps.lineBreakStrategyIOS.c_str()]; + ENRMSetLineBreakStrategy(strategy); + _dirtyFlags |= ENRMDirtyForceHeight; + } + if (markdownChanged || stylePropChanged || md4cFlagsChanged || allowTrailingMarginChanged || - streamingAnimationChanged || streamingConfigChanged) { + streamingAnimationChanged || streamingConfigChanged || lineBreakStrategyChanged) { _pendingStyleFingerprint = computeStyleFingerprint(newViewProps.markdownStyle) ^ std::hash{}(newViewProps.allowTrailingMargin); NSString *markdownString = [[NSString alloc] initWithUTF8String:newViewProps.markdown.c_str()]; diff --git a/ios/EnrichedMarkdownText.mm b/ios/EnrichedMarkdownText.mm index 8acde324..a98eff59 100644 --- a/ios/EnrichedMarkdownText.mm +++ b/ios/EnrichedMarkdownText.mm @@ -507,7 +507,15 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & _spoilerManager.spoilerOverlay = ENRMSpoilerOverlayFromString(modeStr); } - if (markdownChanged || stylePropChanged || md4cFlagsChanged || allowTrailingMarginChanged) { + BOOL lineBreakStrategyChanged = newViewProps.lineBreakStrategyIOS != oldViewProps.lineBreakStrategyIOS; + if (lineBreakStrategyChanged) { + NSString *strategy = [[NSString alloc] initWithUTF8String:newViewProps.lineBreakStrategyIOS.c_str()]; + ENRMSetLineBreakStrategy(strategy); + _forceHeightUpdateOnNextRender = YES; + } + + if (markdownChanged || stylePropChanged || md4cFlagsChanged || allowTrailingMarginChanged || + lineBreakStrategyChanged) { _pendingStyleFingerprint = computeStyleFingerprint(newViewProps.markdownStyle) ^ std::hash{}(newViewProps.allowTrailingMargin); NSString *markdownString = [[NSString alloc] initWithUTF8String:newViewProps.markdown.c_str()]; diff --git a/ios/utils/ParagraphStyleUtils.h b/ios/utils/ParagraphStyleUtils.h index 40570f03..f19248ac 100644 --- a/ios/utils/ParagraphStyleUtils.h +++ b/ios/utils/ParagraphStyleUtils.h @@ -16,6 +16,8 @@ void applyBlockSpacingAfter(NSMutableAttributedString *output, CGFloat marginBot void applyLineHeight(NSMutableAttributedString *output, NSRange range, CGFloat lineHeight); void applyTextAlignment(NSMutableAttributedString *output, NSRange range, NSTextAlignment textAlign); NSTextAlignment textAlignmentFromString(NSString *textAlign); +void ENRMSetLineBreakStrategy(NSString *strategy); +void ENRMSetLineBreakStrategy(NSString *strategy); __END_DECLS diff --git a/ios/utils/ParagraphStyleUtils.m b/ios/utils/ParagraphStyleUtils.m index dfedaf4c..7bf593f0 100644 --- a/ios/utils/ParagraphStyleUtils.m +++ b/ios/utils/ParagraphStyleUtils.m @@ -3,6 +3,20 @@ NSAttributedString *kNewlineAttributedString; static NSParagraphStyle *kBlockSpacerTemplate; +static NSLineBreakStrategy gLineBreakStrategy = NSLineBreakStrategyNone; + +void ENRMSetLineBreakStrategy(NSString *strategy) +{ + if ([strategy isEqualToString:@"standard"]) { + gLineBreakStrategy = NSLineBreakStrategyStandard; + } else if ([strategy isEqualToString:@"hangul-word"]) { + gLineBreakStrategy = NSLineBreakStrategyHangulWordPriority; + } else if ([strategy isEqualToString:@"push-out"]) { + gLineBreakStrategy = NSLineBreakStrategyPushOut; + } else { + gLineBreakStrategy = NSLineBreakStrategyNone; + } +} __attribute__((constructor)) static void initParagraphStyleUtils(void) { @@ -25,6 +39,7 @@ NSWritingDirection currentWritingDirection(void) NSParagraphStyle *existing = [output attribute:NSParagraphStyleAttributeName atIndex:index effectiveRange:NULL]; NSMutableParagraphStyle *style = existing ? [existing mutableCopy] : [[NSMutableParagraphStyle alloc] init]; style.baseWritingDirection = currentWritingDirection(); + style.lineBreakStrategy = gLineBreakStrategy; return style; } diff --git a/src/EnrichedMarkdownNativeComponent.ts b/src/EnrichedMarkdownNativeComponent.ts index 7d83fd4f..b4495220 100644 --- a/src/EnrichedMarkdownNativeComponent.ts +++ b/src/EnrichedMarkdownNativeComponent.ts @@ -388,6 +388,18 @@ export interface NativeProps extends ViewProps { * Receives the item label, the currently selected text, and the selection range. */ onContextMenuItemPress?: CodegenTypes.BubblingEventHandler; + /** + * Sets the text break strategy on Android (API 23+). + * @default 'simple' + * @platform android + */ + textBreakStrategy?: CodegenTypes.WithDefault; + /** + * Sets the line break strategy on iOS 14+. + * @default 'none' + * @platform ios + */ + lineBreakStrategyIOS?: CodegenTypes.WithDefault; } export default codegenNativeComponent('EnrichedMarkdown', { diff --git a/src/EnrichedMarkdownTextNativeComponent.ts b/src/EnrichedMarkdownTextNativeComponent.ts index 4cc5d18d..a2119773 100644 --- a/src/EnrichedMarkdownTextNativeComponent.ts +++ b/src/EnrichedMarkdownTextNativeComponent.ts @@ -387,6 +387,18 @@ export interface NativeProps extends ViewProps { * Receives the item label, the currently selected text, and the selection range. */ onContextMenuItemPress?: CodegenTypes.BubblingEventHandler; + /** + * Sets the text break strategy on Android (API 23+). + * @default 'simple' + * @platform android + */ + textBreakStrategy?: CodegenTypes.WithDefault; + /** + * Sets the line break strategy on iOS 14+. + * @default 'none' + * @platform ios + */ + lineBreakStrategyIOS?: CodegenTypes.WithDefault; } export default codegenNativeComponent('EnrichedMarkdownText', { diff --git a/src/native/EnrichedMarkdownText.tsx b/src/native/EnrichedMarkdownText.tsx index 8458b418..b6867bb2 100644 --- a/src/native/EnrichedMarkdownText.tsx +++ b/src/native/EnrichedMarkdownText.tsx @@ -55,6 +55,8 @@ export const EnrichedMarkdownText = ({ selectionMenuConfig, selectionColor, selectionHandleColor, + textBreakStrategy = 'simple', + lineBreakStrategyIOS, ...rest }: EnrichedMarkdownTextProps) => { const normalizedStyleRef = useRef(null); @@ -166,6 +168,8 @@ export const EnrichedMarkdownText = ({ onContextMenuItemPress: handleContextMenuItemPress, selectionColor, selectionHandleColor, + textBreakStrategy, + lineBreakStrategyIOS, ...rest, }; diff --git a/src/types/MarkdownTextProps.ts b/src/types/MarkdownTextProps.ts index 96a8dba3..0862fa70 100644 --- a/src/types/MarkdownTextProps.ts +++ b/src/types/MarkdownTextProps.ts @@ -213,4 +213,26 @@ export interface EnrichedMarkdownTextProps extends Omit { * @platform web */ dir?: 'ltr' | 'rtl' | 'auto'; + /** + * Sets the text break strategy on Android (API 23+). + * - `'simple'`: no hyphenation, minimal line-break work. + * - `'highQuality'` (default): full paragraph optimization with hyphenation. + * - `'balanced'`: balances line lengths, no hyphenation. + * + * Both the measurement pass and the render pass use this value so that + * measured line counts match rendered line counts. + * @default 'simple' + * @platform android + */ + textBreakStrategy?: 'simple' | 'highQuality' | 'balanced'; + /** + * Sets the line break strategy on iOS (iOS 14+). + * - `'none'` (default): no additional line break strategy. + * - `'standard'`: standard line breaking rules. + * - `'hangul-word'`: Korean word-boundary breaking. + * - `'push-out'`: pushes text out to avoid orphaned words. + * @default 'none' + * @platform ios + */ + lineBreakStrategyIOS?: 'none' | 'standard' | 'hangul-word' | 'push-out'; } From 3fd78eb1f9976f13cb85ebe311b56a08706b0726 Mon Sep 17 00:00:00 2001 From: Ernest Date: Wed, 10 Jun 2026 10:23:25 +0200 Subject: [PATCH 4/5] fix: match android default strategy with the same as in reactnative text component --- .../swmansion/enriched/markdown/EnrichedMarkdownManager.kt | 2 +- .../enriched/markdown/EnrichedMarkdownTextManager.kt | 2 +- src/EnrichedMarkdownNativeComponent.ts | 4 ++-- src/EnrichedMarkdownTextNativeComponent.ts | 4 ++-- src/native/EnrichedMarkdownText.tsx | 2 +- src/types/MarkdownTextProps.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt index 8408babb..87b43e1a 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt @@ -191,7 +191,7 @@ class EnrichedMarkdownManager : view: EnrichedMarkdown?, strategy: String?, ) { - view?.setTextBreakStrategy(strategy ?: "simple") + view?.setTextBreakStrategy(strategy ?: "highQuality") } @ReactProp(name = "contextMenuItems") diff --git a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt index 27d85aa4..c303b8cd 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt @@ -191,7 +191,7 @@ class EnrichedMarkdownTextManager : view: EnrichedMarkdownText?, strategy: String?, ) { - view?.setTextBreakStrategy(strategy ?: "simple") + view?.setTextBreakStrategy(strategy ?: "highQuality") } @ReactProp(name = "contextMenuItems") diff --git a/src/EnrichedMarkdownNativeComponent.ts b/src/EnrichedMarkdownNativeComponent.ts index b4495220..5584ddee 100644 --- a/src/EnrichedMarkdownNativeComponent.ts +++ b/src/EnrichedMarkdownNativeComponent.ts @@ -390,10 +390,10 @@ export interface NativeProps extends ViewProps { onContextMenuItemPress?: CodegenTypes.BubblingEventHandler; /** * Sets the text break strategy on Android (API 23+). - * @default 'simple' + * @default 'highQuality' * @platform android */ - textBreakStrategy?: CodegenTypes.WithDefault; + textBreakStrategy?: CodegenTypes.WithDefault; /** * Sets the line break strategy on iOS 14+. * @default 'none' diff --git a/src/EnrichedMarkdownTextNativeComponent.ts b/src/EnrichedMarkdownTextNativeComponent.ts index a2119773..ac25d5cc 100644 --- a/src/EnrichedMarkdownTextNativeComponent.ts +++ b/src/EnrichedMarkdownTextNativeComponent.ts @@ -389,10 +389,10 @@ export interface NativeProps extends ViewProps { onContextMenuItemPress?: CodegenTypes.BubblingEventHandler; /** * Sets the text break strategy on Android (API 23+). - * @default 'simple' + * @default 'highQuality' * @platform android */ - textBreakStrategy?: CodegenTypes.WithDefault; + textBreakStrategy?: CodegenTypes.WithDefault; /** * Sets the line break strategy on iOS 14+. * @default 'none' diff --git a/src/native/EnrichedMarkdownText.tsx b/src/native/EnrichedMarkdownText.tsx index b6867bb2..23af29c2 100644 --- a/src/native/EnrichedMarkdownText.tsx +++ b/src/native/EnrichedMarkdownText.tsx @@ -55,7 +55,7 @@ export const EnrichedMarkdownText = ({ selectionMenuConfig, selectionColor, selectionHandleColor, - textBreakStrategy = 'simple', + textBreakStrategy = 'highQuality', lineBreakStrategyIOS, ...rest }: EnrichedMarkdownTextProps) => { diff --git a/src/types/MarkdownTextProps.ts b/src/types/MarkdownTextProps.ts index 0862fa70..17a380f1 100644 --- a/src/types/MarkdownTextProps.ts +++ b/src/types/MarkdownTextProps.ts @@ -221,7 +221,7 @@ export interface EnrichedMarkdownTextProps extends Omit { * * Both the measurement pass and the render pass use this value so that * measured line counts match rendered line counts. - * @default 'simple' + * @default 'highQuality' * @platform android */ textBreakStrategy?: 'simple' | 'highQuality' | 'balanced'; From 5d2a438165efe7ed47d40dfd15b4ffc63a5f9c15 Mon Sep 17 00:00:00 2001 From: Ernest Szlamczyk <127619251+eszlamczyk@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:00:52 +0200 Subject: [PATCH 5/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ios/utils/ParagraphStyleUtils.h | 1 - 1 file changed, 1 deletion(-) diff --git a/ios/utils/ParagraphStyleUtils.h b/ios/utils/ParagraphStyleUtils.h index f19248ac..414de671 100644 --- a/ios/utils/ParagraphStyleUtils.h +++ b/ios/utils/ParagraphStyleUtils.h @@ -17,7 +17,6 @@ void applyLineHeight(NSMutableAttributedString *output, NSRange range, CGFloat l void applyTextAlignment(NSMutableAttributedString *output, NSRange range, NSTextAlignment textAlign); NSTextAlignment textAlignmentFromString(NSString *textAlign); void ENRMSetLineBreakStrategy(NSString *strategy); -void ENRMSetLineBreakStrategy(NSString *strategy); __END_DECLS