Skip to content

Commit 3893ba8

Browse files
committed
Merge branch 'main' into CI-improve
2 parents f8e658d + 40a17e0 commit 3893ba8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1951
-168
lines changed

RNLiveMarkdown.podspec

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,17 @@ Pod::Spec.new do |s|
1919
s.resources = "parser/react-native-live-markdown-parser.js"
2020

2121
install_modules_dependencies(s)
22+
23+
if ENV['USE_FRAMEWORKS'] && ENV['RCT_NEW_ARCH_ENABLED']
24+
add_dependency(s, "React-Fabric", :additional_framework_paths => [
25+
"react/renderer/textlayoutmanager/platform/ios",
26+
"react/renderer/components/textinput/iostextinput",
27+
])
28+
end
29+
30+
s.subspec "common" do |ss|
31+
ss.source_files = "cpp/**/*.{cpp,h}"
32+
ss.header_dir = "RNLiveMarkdown"
33+
ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/cpp\"" }
34+
end
2235
end

android/build.gradle

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ android {
5353
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
5454
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
5555
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
56+
57+
consumerProguardFiles "proguard-rules.pro"
58+
5659
externalNativeBuild {
5760
cmake {
5861
arguments "-DANDROID_STL=c++_shared", "-DANDROID_TOOLCHAIN=clang"
@@ -112,6 +115,15 @@ android {
112115
"**/libreactnativejni.so",
113116
]
114117
}
118+
119+
packagingOptions {
120+
// For some reason gradle only complains about the duplicated version of librrc_root and libreact_render libraries
121+
// while there are more libraries copied in intermediates folder of the lib build directory, we exclude
122+
// only the ones that make the build fail (ideally we should only include libreanimated but we
123+
// are only allowed to specify exlude patterns)
124+
exclude "**/libreact_render*.so"
125+
exclude "**/librrc_root.so"
126+
}
115127
}
116128

117129
repositories {

android/proguard-rules.pro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-keep class com.expensify.livemarkdown.** { *; }
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.expensify.livemarkdown;
2+
3+
import androidx.annotation.NonNull;
4+
5+
import com.facebook.react.bridge.ReactApplicationContext;
6+
import com.facebook.react.bridge.ReadableMap;
7+
import com.facebook.react.fabric.FabricUIManager;
8+
import com.facebook.react.fabric.mounting.MountingManager;
9+
import com.facebook.react.uimanager.ViewManagerRegistry;
10+
import com.facebook.react.uimanager.events.BatchEventDispatchedListener;
11+
12+
import java.lang.reflect.Field;
13+
14+
public class CustomFabricUIManager {
15+
16+
public static FabricUIManager create(FabricUIManager source, ReadableMap markdownProps) {
17+
Class<? extends FabricUIManager> uiManagerClass = source.getClass();
18+
19+
try {
20+
Field mountingManagerField = uiManagerClass.getDeclaredField("mMountingManager");
21+
mountingManagerField.setAccessible(true);
22+
23+
ReactApplicationContext reactContext = readPrivateField(source, "mReactApplicationContext");
24+
ViewManagerRegistry viewManagerRegistry = readPrivateField(source, "mViewManagerRegistry");
25+
BatchEventDispatchedListener batchEventDispatchedListener = readPrivateField(source, "mBatchEventDispatchedListener");
26+
MountingManager.MountItemExecutor mountItemExecutor = readPrivateField(source, "mMountItemExecutor");
27+
28+
FabricUIManager customFabricUIManager = new FabricUIManager(reactContext, viewManagerRegistry, batchEventDispatchedListener);
29+
30+
mountingManagerField.set(customFabricUIManager, new CustomMountingManager(viewManagerRegistry, mountItemExecutor, reactContext, markdownProps));
31+
32+
return customFabricUIManager;
33+
} catch (NoSuchFieldException | IllegalAccessException e) {
34+
throw new RuntimeException("[LiveMarkdown] Cannot read data from FabricUIManager");
35+
}
36+
}
37+
38+
private static <T> T readPrivateField(Object obj, String name) throws NoSuchFieldException, IllegalAccessException {
39+
Class<?> clazz = obj.getClass();
40+
41+
Field field = clazz.getDeclaredField(name);
42+
field.setAccessible(true);
43+
T value = (T) field.get(obj);
44+
45+
return value;
46+
}
47+
}
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
package com.expensify.livemarkdown;
2+
3+
import static com.facebook.react.views.text.TextAttributeProps.UNSET;
4+
5+
import android.content.Context;
6+
import android.content.res.AssetManager;
7+
import android.text.BoringLayout;
8+
import android.text.Layout;
9+
import android.text.Spannable;
10+
import android.text.SpannableStringBuilder;
11+
import android.text.TextPaint;
12+
13+
import androidx.annotation.NonNull;
14+
import androidx.annotation.Nullable;
15+
16+
import com.facebook.common.logging.FLog;
17+
import com.facebook.react.bridge.ReactContext;
18+
import com.facebook.react.bridge.ReadableMap;
19+
import com.facebook.react.common.mapbuffer.MapBuffer;
20+
import com.facebook.react.fabric.mounting.MountingManager;
21+
import com.facebook.react.uimanager.PixelUtil;
22+
import com.facebook.react.uimanager.ViewManagerRegistry;
23+
import com.facebook.react.views.text.TextAttributeProps;
24+
import com.facebook.react.views.text.TextInlineViewPlaceholderSpan;
25+
import com.facebook.react.views.text.TextLayoutManagerMapBuffer;
26+
import com.facebook.yoga.YogaMeasureMode;
27+
import com.facebook.yoga.YogaMeasureOutput;
28+
29+
import java.lang.reflect.InvocationTargetException;
30+
import java.lang.reflect.Method;
31+
32+
public class CustomMountingManager extends MountingManager {
33+
private static final boolean DEFAULT_INCLUDE_FONT_PADDING = true;
34+
private static final TextPaint sTextPaintInstance = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);
35+
36+
private MarkdownUtils markdownUtils;
37+
38+
public CustomMountingManager(
39+
@NonNull ViewManagerRegistry viewManagerRegistry,
40+
@NonNull MountItemExecutor mountItemExecutor,
41+
@NonNull Context context,
42+
@NonNull ReadableMap decoratorProps) {
43+
super(viewManagerRegistry, mountItemExecutor);
44+
45+
AssetManager assetManager = context.getAssets();
46+
MarkdownUtils.maybeInitializeRuntime(assetManager);
47+
48+
this.markdownUtils = new MarkdownUtils(assetManager);
49+
this.markdownUtils.setMarkdownStyle(new MarkdownStyle(decoratorProps, context));
50+
}
51+
52+
@Override
53+
public long measureMapBuffer(
54+
@NonNull ReactContext context,
55+
@NonNull String componentName,
56+
@NonNull MapBuffer attributedString,
57+
@NonNull MapBuffer paragraphAttributes,
58+
@Nullable MapBuffer state,
59+
float width,
60+
@NonNull YogaMeasureMode widthYogaMeasureMode,
61+
float height,
62+
@NonNull YogaMeasureMode heightYogaMeasureMode,
63+
@Nullable float[] attachmentsPositions) {
64+
65+
Spannable text =
66+
TextLayoutManagerMapBuffer.getOrCreateSpannableForText(context, attributedString, null);
67+
68+
if (text == null) {
69+
return 0;
70+
}
71+
72+
int textBreakStrategy =
73+
TextAttributeProps.getTextBreakStrategy(
74+
paragraphAttributes.getString(TextLayoutManagerMapBuffer.PA_KEY_TEXT_BREAK_STRATEGY));
75+
boolean includeFontPadding =
76+
paragraphAttributes.contains(TextLayoutManagerMapBuffer.PA_KEY_INCLUDE_FONT_PADDING)
77+
? paragraphAttributes.getBoolean(TextLayoutManagerMapBuffer.PA_KEY_INCLUDE_FONT_PADDING)
78+
: DEFAULT_INCLUDE_FONT_PADDING;
79+
int hyphenationFrequency =
80+
TextAttributeProps.getHyphenationFrequency(
81+
paragraphAttributes.getString(TextLayoutManagerMapBuffer.PA_KEY_HYPHENATION_FREQUENCY));
82+
83+
// StaticLayout returns wrong metrics for the last line if it's empty, add something to the
84+
// last line so it's measured correctly
85+
if (text.toString().endsWith("\n")) {
86+
SpannableStringBuilder sb = new SpannableStringBuilder(text);
87+
sb.append("I");
88+
89+
text = sb;
90+
}
91+
92+
markdownUtils.applyMarkdownFormatting((SpannableStringBuilder)text);
93+
94+
BoringLayout.Metrics boring = BoringLayout.isBoring(text, sTextPaintInstance);
95+
96+
Class<TextLayoutManagerMapBuffer> mapBufferClass = TextLayoutManagerMapBuffer.class;
97+
try {
98+
Method createLayoutMethod = mapBufferClass.getDeclaredMethod("createLayout", Spannable.class, BoringLayout.Metrics.class, float.class, YogaMeasureMode.class, boolean.class, int.class, int.class);
99+
createLayoutMethod.setAccessible(true);
100+
101+
Layout layout = (Layout)createLayoutMethod.invoke(
102+
null,
103+
text,
104+
boring,
105+
width,
106+
widthYogaMeasureMode,
107+
includeFontPadding,
108+
textBreakStrategy,
109+
hyphenationFrequency);
110+
111+
int maximumNumberOfLines =
112+
paragraphAttributes.contains(TextLayoutManagerMapBuffer.PA_KEY_MAX_NUMBER_OF_LINES)
113+
? paragraphAttributes.getInt(TextLayoutManagerMapBuffer.PA_KEY_MAX_NUMBER_OF_LINES)
114+
: UNSET;
115+
116+
int calculatedLineCount =
117+
maximumNumberOfLines == UNSET || maximumNumberOfLines == 0
118+
? layout.getLineCount()
119+
: Math.min(maximumNumberOfLines, layout.getLineCount());
120+
121+
// Instead of using `layout.getWidth()` (which may yield a significantly larger width for
122+
// text that is wrapping), compute width using the longest line.
123+
float calculatedWidth = 0;
124+
if (widthYogaMeasureMode == YogaMeasureMode.EXACTLY) {
125+
calculatedWidth = width;
126+
} else {
127+
for (int lineIndex = 0; lineIndex < calculatedLineCount; lineIndex++) {
128+
boolean endsWithNewLine =
129+
text.length() > 0 && text.charAt(layout.getLineEnd(lineIndex) - 1) == '\n';
130+
float lineWidth =
131+
endsWithNewLine ? layout.getLineMax(lineIndex) : layout.getLineWidth(lineIndex);
132+
if (lineWidth > calculatedWidth) {
133+
calculatedWidth = lineWidth;
134+
}
135+
}
136+
if (widthYogaMeasureMode == YogaMeasureMode.AT_MOST && calculatedWidth > width) {
137+
calculatedWidth = width;
138+
}
139+
}
140+
141+
// Android 11+ introduces changes in text width calculation which leads to cases
142+
// where the container is measured smaller than text. Math.ceil prevents it
143+
// See T136756103 for investigation
144+
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.Q) {
145+
calculatedWidth = (float) Math.ceil(calculatedWidth);
146+
}
147+
148+
float calculatedHeight = height;
149+
if (heightYogaMeasureMode != YogaMeasureMode.EXACTLY) {
150+
calculatedHeight = layout.getLineBottom(calculatedLineCount - 1);
151+
if (heightYogaMeasureMode == YogaMeasureMode.AT_MOST && calculatedHeight > height) {
152+
calculatedHeight = height;
153+
}
154+
}
155+
156+
// Calculate the positions of the attachments (views) that will be rendered inside the
157+
// Spanned Text. The following logic is only executed when a text contains views inside.
158+
// This follows a similar logic than used in pre-fabric (see ReactTextView.onLayout method).
159+
int attachmentIndex = 0;
160+
int lastAttachmentFoundInSpan;
161+
for (int i = 0; i < text.length(); i = lastAttachmentFoundInSpan) {
162+
lastAttachmentFoundInSpan =
163+
text.nextSpanTransition(i, text.length(), TextInlineViewPlaceholderSpan.class);
164+
TextInlineViewPlaceholderSpan[] placeholders =
165+
text.getSpans(i, lastAttachmentFoundInSpan, TextInlineViewPlaceholderSpan.class);
166+
for (TextInlineViewPlaceholderSpan placeholder : placeholders) {
167+
int start = text.getSpanStart(placeholder);
168+
int line = layout.getLineForOffset(start);
169+
boolean isLineTruncated = layout.getEllipsisCount(line) > 0;
170+
// This truncation check works well on recent versions of Android (tested on 5.1.1 and
171+
// 6.0.1) but not on Android 4.4.4. The reason is that getEllipsisCount is buggy on
172+
// Android 4.4.4. Specifically, it incorrectly returns 0 if an inline view is the
173+
// first thing to be truncated.
174+
if (!(isLineTruncated && start >= layout.getLineStart(line) + layout.getEllipsisStart(line))
175+
|| start >= layout.getLineEnd(line)) {
176+
float placeholderWidth = placeholder.getWidth();
177+
float placeholderHeight = placeholder.getHeight();
178+
// Calculate if the direction of the placeholder character is Right-To-Left.
179+
boolean isRtlChar = layout.isRtlCharAt(start);
180+
boolean isRtlParagraph = layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT;
181+
float placeholderLeftPosition;
182+
// There's a bug on Samsung devices where calling getPrimaryHorizontal on
183+
// the last offset in the layout will result in an endless loop. Work around
184+
// this bug by avoiding getPrimaryHorizontal in that case.
185+
if (start == text.length() - 1) {
186+
boolean endsWithNewLine =
187+
text.length() > 0 && text.charAt(layout.getLineEnd(line) - 1) == '\n';
188+
float lineWidth = endsWithNewLine ? layout.getLineMax(line) : layout.getLineWidth(line);
189+
placeholderLeftPosition =
190+
isRtlParagraph
191+
// Equivalent to `layout.getLineLeft(line)` but `getLineLeft` returns
192+
// incorrect
193+
// values when the paragraph is RTL and `setSingleLine(true)`.
194+
? calculatedWidth - lineWidth
195+
: layout.getLineRight(line) - placeholderWidth;
196+
} else {
197+
// The direction of the paragraph may not be exactly the direction the string is
198+
// heading
199+
// in at the
200+
// position of the placeholder. So, if the direction of the character is the same
201+
// as the
202+
// paragraph
203+
// use primary, secondary otherwise.
204+
boolean characterAndParagraphDirectionMatch = isRtlParagraph == isRtlChar;
205+
placeholderLeftPosition =
206+
characterAndParagraphDirectionMatch
207+
? layout.getPrimaryHorizontal(start)
208+
: layout.getSecondaryHorizontal(start);
209+
if (isRtlParagraph) {
210+
// Adjust `placeholderLeftPosition` to work around an Android bug.
211+
// The bug is when the paragraph is RTL and `setSingleLine(true)`, some layout
212+
// methods such as `getPrimaryHorizontal`, `getSecondaryHorizontal`, and
213+
// `getLineRight` return incorrect values. Their return values seem to be off
214+
// by the same number of pixels so subtracting these values cancels out the
215+
// error.
216+
//
217+
// The result is equivalent to bugless versions of
218+
// `getPrimaryHorizontal`/`getSecondaryHorizontal`.
219+
placeholderLeftPosition =
220+
calculatedWidth - (layout.getLineRight(line) - placeholderLeftPosition);
221+
}
222+
if (isRtlChar) {
223+
placeholderLeftPosition -= placeholderWidth;
224+
}
225+
}
226+
// Vertically align the inline view to the baseline of the line of text.
227+
float placeholderTopPosition = layout.getLineBaseline(line) - placeholderHeight;
228+
int attachmentPosition = attachmentIndex * 2;
229+
230+
// The attachment array returns the positions of each of the attachments as
231+
attachmentsPositions[attachmentPosition] =
232+
PixelUtil.toDIPFromPixel(placeholderTopPosition);
233+
attachmentsPositions[attachmentPosition + 1] =
234+
PixelUtil.toDIPFromPixel(placeholderLeftPosition);
235+
attachmentIndex++;
236+
}
237+
}
238+
}
239+
240+
float widthInSP = PixelUtil.toDIPFromPixel(calculatedWidth);
241+
float heightInSP = PixelUtil.toDIPFromPixel(calculatedHeight);
242+
243+
return YogaMeasureOutput.make(widthInSP, heightInSP);
244+
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
245+
throw new RuntimeException(e);
246+
}
247+
}
248+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.expensify.livemarkdown;
2+
3+
import com.facebook.react.bridge.ReactApplicationContext;
4+
import com.facebook.react.bridge.UIManager;
5+
import com.facebook.react.fabric.FabricUIManager;
6+
import com.facebook.react.uimanager.UIManagerHelper;
7+
import com.facebook.react.uimanager.common.UIManagerType;
8+
9+
public class LiveMarkdownModule extends NativeLiveMarkdownModuleSpec {
10+
private NativeProxy mNativeProxy;
11+
public LiveMarkdownModule(ReactApplicationContext reactContext) {
12+
super(reactContext);
13+
14+
this.mNativeProxy = new NativeProxy();
15+
}
16+
17+
@Override
18+
public boolean install() {
19+
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
20+
FabricUIManager uiManager =
21+
(FabricUIManager) UIManagerHelper.getUIManager(getReactApplicationContext(), UIManagerType.FABRIC);
22+
mNativeProxy.createCommitHook(uiManager);
23+
}
24+
25+
return true;
26+
}
27+
}

0 commit comments

Comments
 (0)