diff --git a/WebExample/__tests__/styles.spec.ts b/WebExample/__tests__/styles.spec.ts index d54334a9..0d5674eb 100644 --- a/WebExample/__tests__/styles.spec.ts +++ b/WebExample/__tests__/styles.spec.ts @@ -18,7 +18,7 @@ test.describe('markdown content styling', () => { }); test('h1', async ({page}) => { - await testMarkdownContentStyle({testContent: 'header1', style: 'font-size: 25px; font-weight: bold;', page}); + await testMarkdownContentStyle({testContent: 'header1', style: 'font-size: 25px; line-height: 50px; font-weight: bold;', page}); }); test('inline code', async ({page}) => { diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java index 78b554cd..630bc25a 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java @@ -107,10 +107,10 @@ private void applyRange(@NonNull SpannableStringBuilder ssb, @NonNull MarkdownRa break; case "h1": setSpan(ssb, new MarkdownBoldSpan(), start, end); - CustomLineHeightSpan[] spans = ssb.getSpans(0, ssb.length(), CustomLineHeightSpan.class); - if (spans.length >= 1) { - int lineHeight = spans[0].getLineHeight(); - setSpan(ssb, new MarkdownLineHeightSpan(lineHeight * 1.5f), start, end); + float lineHeight = markdownStyle.getH1LineHeight(); + if (lineHeight != -1) { + // NOTE: actually, we should also include "# " but it also works this way + setSpan(ssb, new MarkdownLineHeightSpan(lineHeight), start, end); } // NOTE: size span must be set after line height span to avoid height jumps setSpan(ssb, new MarkdownFontSizeSpan(markdownStyle.getH1FontSize()), start, end); diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java index ca71e51c..467ba4ab 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java @@ -21,6 +21,8 @@ public class MarkdownStyle { private final float mH1FontSize; + private final float mH1LineHeight; + @NonNull private final String mEmojiFontFamily; @@ -79,6 +81,7 @@ public MarkdownStyle(@NonNull ReadableMap map, @NonNull Context context) { mSyntaxColor = parseColor(map, "syntax", "color", context); mLinkColor = parseColor(map, "link", "color", context); mH1FontSize = parseFloat(map, "h1", "fontSize"); + mH1LineHeight = parseFloat(map, "h1", "lineHeight"); mEmojiFontSize = parseFloat(map, "emoji", "fontSize"); mEmojiFontFamily = parseString(map, "emoji", "fontFamily"); mBlockquoteBorderColor = parseColor(map, "blockquote", "borderColor", context); @@ -142,6 +145,10 @@ public float getH1FontSize() { return mH1FontSize; } + public float getH1LineHeight() { + return mH1LineHeight; + } + public float getEmojiFontSize() { return mEmojiFontSize; } diff --git a/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownLineHeightSpan.java b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownLineHeightSpan.java index 3f4c33fc..936020e7 100644 --- a/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownLineHeightSpan.java +++ b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownLineHeightSpan.java @@ -3,16 +3,19 @@ import android.graphics.Paint; import android.text.style.LineHeightSpan; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.views.text.internal.span.CustomLineHeightSpan; + public class MarkdownLineHeightSpan implements MarkdownSpan, LineHeightSpan { - private final float mLineHeight; + private final CustomLineHeightSpan mCustomLineHeightSpan; public MarkdownLineHeightSpan(float lineHeight) { - mLineHeight = lineHeight; + mCustomLineHeightSpan = new CustomLineHeightSpan(PixelUtil.toPixelFromDIP(lineHeight)); } @Override public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt fm) { - fm.top -= mLineHeight / 4; - fm.ascent -= mLineHeight / 4; + // CustomLineHeightSpan is marked as final, we can't extend it, but we can use it via this adapter + mCustomLineHeightSpan.chooseHeight(text, start, end, spanstartv, lineHeight, fm); } } diff --git a/apple/MarkdownBackedTextInputDelegate.mm b/apple/MarkdownBackedTextInputDelegate.mm index 87335ccb..170d6b02 100644 --- a/apple/MarkdownBackedTextInputDelegate.mm +++ b/apple/MarkdownBackedTextInputDelegate.mm @@ -39,6 +39,8 @@ - (void)textInputDidChangeSelection NSMutableParagraphStyle *mutableParagraphStyle = [typingAttributes[NSParagraphStyleAttributeName] mutableCopy]; mutableParagraphStyle.firstLineHeadIndent = 0; mutableParagraphStyle.headIndent = 0; + mutableParagraphStyle.minimumLineHeight = 0; + mutableParagraphStyle.maximumLineHeight = 0; mutableTypingAttributes[NSParagraphStyleAttributeName] = mutableParagraphStyle; _textView.typingAttributes = mutableTypingAttributes; } diff --git a/apple/MarkdownFormatter.mm b/apple/MarkdownFormatter.mm index b47c4ac7..50d3ba33 100644 --- a/apple/MarkdownFormatter.mm +++ b/apple/MarkdownFormatter.mm @@ -110,6 +110,28 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri paragraphStyle.headIndent = indent; [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; [attributedString addAttribute:RCTLiveMarkdownBlockquoteDepthAttributeName value:@(depth) range:range]; + } else if (type == "h1" && markdownStyle.h1LineHeight != -1) { + __block BOOL found = NO; + [attributedString enumerateAttribute:NSParagraphStyleAttributeName + inRange:range + options:0 + usingBlock:^(NSParagraphStyle *paragraphStyle, NSRange paragraphRange, BOOL *stop) { + if (paragraphStyle && paragraphStyle.headIndent && [paragraphStyle isKindOfClass:[NSMutableParagraphStyle class]]) { + NSMutableParagraphStyle *mutableParagraphStyle = (NSMutableParagraphStyle *)paragraphStyle; + mutableParagraphStyle.minimumLineHeight = markdownStyle.h1LineHeight; + mutableParagraphStyle.maximumLineHeight = markdownStyle.h1LineHeight; + found = YES; + *stop = YES; + } + }]; + if (!found) { + NSParagraphStyle *defaultParagraphStyle = defaultTextAttributes[NSParagraphStyleAttributeName]; + NSMutableParagraphStyle *paragraphStyle = defaultParagraphStyle != nil ? [defaultParagraphStyle mutableCopy] : [NSMutableParagraphStyle new]; + paragraphStyle.minimumLineHeight = markdownStyle.h1LineHeight; + paragraphStyle.maximumLineHeight = markdownStyle.h1LineHeight; + NSRange rangeWithHashAndSpace = NSMakeRange(range.location - 2, range.length + 2); + [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:rangeWithHashAndSpace]; + } } else if (type == "pre") { [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.preColor range:range]; NSRange rangeForBackground = [[attributedString string] characterAtIndex:range.location] == '\n' ? NSMakeRange(range.location + 1, range.length - 1) : range; diff --git a/apple/RCTMarkdownStyle.h b/apple/RCTMarkdownStyle.h index fd88fcaf..80dbe215 100644 --- a/apple/RCTMarkdownStyle.h +++ b/apple/RCTMarkdownStyle.h @@ -9,6 +9,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) UIColor *syntaxColor; @property (nonatomic) UIColor *linkColor; @property (nonatomic) CGFloat h1FontSize; +@property (nonatomic) CGFloat h1LineHeight; @property (nonatomic) CGFloat emojiFontSize; @property (nonatomic) NSString *emojiFontFamily; @property (nonatomic) UIColor *blockquoteBorderColor; diff --git a/apple/RCTMarkdownStyle.mm b/apple/RCTMarkdownStyle.mm index f56f01ae..49d80800 100644 --- a/apple/RCTMarkdownStyle.mm +++ b/apple/RCTMarkdownStyle.mm @@ -12,6 +12,7 @@ - (instancetype)initWithStruct:(const facebook::react::MarkdownTextInputDecorato _linkColor = RCTUIColorFromSharedColor(style.link.color); _h1FontSize = style.h1.fontSize; + _h1LineHeight = style.h1.lineHeight; _emojiFontSize = style.emoji.fontSize; _emojiFontFamily = RCTNSStringFromString(style.emoji.fontFamily); diff --git a/example/Gemfile.lock b/example/Gemfile.lock index dadd346f..990e10e7 100644 --- a/example/Gemfile.lock +++ b/example/Gemfile.lock @@ -5,16 +5,18 @@ GEM base64 nkf rexml - activesupport (7.1.4.2) + activesupport (7.2.2.1) base64 + benchmark (>= 0.3) bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) algoliasearch (1.27.5) @@ -64,21 +66,22 @@ GEM cocoapods-try (1.2.0) colored2 (3.1.2) concurrent-ruby (1.3.3) - connection_pool (2.5.0) + connection_pool (2.5.2) drb (2.2.1) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - ffi (1.17.1) + ffi (1.17.2) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - httpclient (2.8.3) + httpclient (2.9.0) + mutex_m i18n (1.14.7) concurrent-ruby (~> 1.0) - json (2.10.0) + json (2.11.1) logger (1.7.0) - minitest (5.25.4) + minitest (5.25.5) molinillo (0.8.0) mutex_m (0.3.0) nanaimo (0.3.0) @@ -86,8 +89,9 @@ GEM netrc (0.11.0) nkf (0.2.0) public_suffix (4.0.7) - rexml (3.4.0) + rexml (3.4.1) ruby-macho (2.5.1) + securerandom (0.4.1) typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) @@ -114,7 +118,7 @@ DEPENDENCIES xcodeproj (< 1.26.0) RUBY VERSION - ruby 3.3.5p100 + ruby 3.2.2p53 BUNDLED WITH - 2.4.22 + 2.6.3 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 6b5568a5..9a3d2cd6 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2122,7 +2122,7 @@ PODS: - React-perflogger (= 0.80.1) - React-utils (= 0.80.1) - SocketRocket - - RNLiveMarkdown (0.1.296): + - RNLiveMarkdown (0.1.300): - boost - DoubleConversion - fast_float @@ -2544,7 +2544,7 @@ SPEC CHECKSUMS: FBLazyVector: 09f03e4b6f42f955734b64a118f86509cc719427 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - hermes-engine: 4f07404533b808de66cf48ac4200463068d0e95a + hermes-engine: 47c54213afbdb49fcf0aa16eea5a01060d8c41b3 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: efa5010912100e944a7ac9a93a157e1def1988fe RCTRequired: bbc4cf999ddc4a4b076e076c74dd1d39d0254630 @@ -2557,8 +2557,8 @@ SPEC CHECKSUMS: React-debug: deb3a146ef717fa3e8f4c23e0288369fe53199b7 React-defaultsnativemodule: 11e2948787a15d3cf1b66d7f29f13770a177bff7 React-domnativemodule: 2f4b279acdb2963736fb5de2f585811dd90070b5 - React-Fabric: 6f8d1a303c96f1d078c14d74c4005bf457e5b782 - React-FabricComponents: b106410970e9a0c4e592da656c7a7e0947306c23 + React-Fabric: 885f08b2c693523059a1721262e78271bccb69e4 + React-FabricComponents: 13a35051a4bfd84ea46515b07430f30f55f3f313 React-FabricImage: 1abaf230dfce9b58fdf53c4128f3f40c6e64af6a React-featureflags: f7ef58d91079efde3ad223bcca6d197e845d5bcf React-featureflagsnativemodule: ae5abc9849d1696f4f8f11ee3744bf5715e032cf @@ -2609,7 +2609,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: afd905e84ee36e1678016ae04d7370c75ed539be ReactCodegen: 62690fdeb185860673cb78e3d9b049c7e1d40531 ReactCommon: 17fd88849a174bf9ce45461912291aca711410fc - RNLiveMarkdown: e110927c17781579c6b833012482001d181707cd + RNLiveMarkdown: 520b4739cbfc17778a14bd6731f49d412057029a RNReanimated: 237d420b7bb4378ef1dacc7d7a5c674fddb4b5d2 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: daa1e4de4b971b977b23bc842aaa3e135324f1f3 diff --git a/example/src/App.tsx b/example/src/App.tsx index ce6b1db8..853c8eb9 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -15,6 +15,9 @@ export default function App() { const [linkColorState, setLinkColorState] = React.useState(false); const [textFontSizeState, setTextFontSizeState] = React.useState(false); const [emojiFontSizeState, setEmojiFontSizeState] = React.useState(false); + const [textLineHeightState, setTextLineHeightState] = React.useState(false); + const [headingLineHeightState, setHeadingLineHeightState] = + React.useState(false); const [caretHidden, setCaretHidden] = React.useState(false); const [selection, setSelection] = React.useState({start: 0, end: 0}); @@ -22,17 +25,23 @@ export default function App() { return { color: textColorState ? 'gray' : 'black', fontSize: textFontSizeState ? 15 : 20, + lineHeight: textLineHeightState ? 40 : undefined, }; - }, [textColorState, textFontSizeState]); + }, [textColorState, textFontSizeState, textLineHeightState]); - const markdownStyle = { - emoji: { - fontSize: emojiFontSizeState ? 15 : 20, - }, - link: { - color: linkColorState ? 'red' : 'blue', - }, - }; + const markdownStyle = React.useMemo(() => { + return { + emoji: { + fontSize: emojiFontSizeState ? 15 : 20, + }, + link: { + color: linkColorState ? 'red' : 'blue', + }, + h1: { + lineHeight: headingLineHeightState ? 60 : undefined, + }, + }; + }, [emojiFontSizeState, linkColorState, headingLineHeightState]); const ref = React.useRef(null); @@ -118,6 +127,16 @@ export default function App() { title="Toggle emoji font size" onPress={() => setEmojiFontSizeState(prev => !prev)} /> +