Skip to content

Commit

Permalink
Merge branch 'main' into @tomekzaw/fix-markdown-style-update-with-emoji
Browse files Browse the repository at this point in the history
  • Loading branch information
tomekzaw committed May 21, 2024
2 parents fad4a4f + 4cbff0f commit ca19afa
Show file tree
Hide file tree
Showing 20 changed files with 314 additions and 308 deletions.
1 change: 0 additions & 1 deletion RNLiveMarkdown.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,5 @@ Pod::Spec.new do |s|
s.subspec "common" do |ss|
ss.source_files = "cpp/**/*.{cpp,h}"
ss.header_dir = "RNLiveMarkdown"
ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/cpp\"" }
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ protected void setMarkdownStyle(MarkdownStyle markdownStyle) {
mMarkdownUtils.setMarkdownStyle(mMarkdownStyle);
}
if (mReactEditText != null) {
int selectionStart = mReactEditText.getSelectionStart();
int selectionEnd = mReactEditText.getSelectionEnd();
mReactEditText.setText(mReactEditText.getText()); // trigger update
mReactEditText.setSelection(selectionStart, selectionEnd);
}
}
}
9 changes: 9 additions & 0 deletions android/src/main/new_arch/MarkdownCommitHook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ RootShadowNode::Unshared MarkdownCommitHook::shadowTreeWillCommit(
// force measurement of a map buffer
newStateData->cachedAttributedStringId = 0;

// setting -1 as the event counter makes sure that the update will be ignored by the java
// part of the code, which is what we want as we don't change the attributed string here
if (previousEventCount_.contains(nodes.textInput->getTag()) &&
previousEventCount_[nodes.textInput->getTag()] == stateData.mostRecentEventCount) {
newStateData->mostRecentEventCount = -1;
} else {
previousEventCount_[nodes.textInput->getTag()] = stateData.mostRecentEventCount;
}

// clone the text input with the new state
auto newNode = node.clone({
.state =
Expand Down
2 changes: 2 additions & 0 deletions android/src/main/new_arch/MarkdownCommitHook.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ class MarkdownCommitHook : public UIManagerCommitHook {
textLayoutManagers_;
std::unordered_map<facebook::react::Tag, folly::dynamic>
previousDecoratorProps_;
std::unordered_map<facebook::react::Tag, int64_t>
previousEventCount_;
};

} // namespace livemarkdown
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#pragma once
#if defined(RCT_NEW_ARCH_ENABLED) || defined(ANDROID)

#include <react/renderer/core/ShadowNodeFamily.h>

Expand Down Expand Up @@ -32,3 +33,5 @@ class JSI_EXPORT MarkdownTextInputDecoratorState final {

} // namespace react
} // namespace facebook

#endif
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#pragma once
#if defined(RCT_NEW_ARCH_ENABLED) || defined(ANDROID)

#include "MarkdownTextInputDecoratorShadowNode.h"
#include <react/debug/react_native_assert.h>
Expand All @@ -15,3 +16,5 @@ class MarkdownTextInputDecoratorViewComponentDescriptor final

} // namespace react
} // namespace facebook

#endif
File renamed without changes.
File renamed without changes.
45 changes: 38 additions & 7 deletions ios/RCTLiveMarkdownModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,54 @@
@implementation RCTLiveMarkdownModule {
BOOL installed_;
std::shared_ptr<livemarkdown::MarkdownCommitHook> commitHook_;
__weak RCTSurfacePresenter *surfacePresenter_;
}

RCT_EXPORT_MODULE(@"LiveMarkdownModule")

- (NSNumber *)install {
if (!installed_) {
installed_ = YES;

RCTBridge *bridge = self.bridge;
RCTSurfacePresenter *surfacePresenter = bridge.surfacePresenter;
RCTScheduler *scheduler = [surfacePresenter scheduler];
if (!installed_ && surfacePresenter_ != nil) {
RCTScheduler *scheduler = [surfacePresenter_ scheduler];

commitHook_ =
std::make_shared<livemarkdown::MarkdownCommitHook>(scheduler.uiManager);
std::make_shared<livemarkdown::MarkdownCommitHook>(scheduler.uiManager);
installed_ = YES;
}
return @1;
}

- (void)handleJavaScriptDidLoadNotification:(NSNotification *)notification
{
surfacePresenter_ = self.bridge.surfacePresenter;
[self install];
}

- (void)setBridge:(RCTBridge *)bridge
{
[super setBridge:bridge];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleJavaScriptDidLoadNotification:)
name:RCTJavaScriptDidLoadNotification
object:nil];

// only within the first loading `self.bridge.surfacePresenter` exists
// during the reload `self.bridge.surfacePresenter` is null
if (self.bridge.surfacePresenter) {
surfacePresenter_ = self.bridge.surfacePresenter;
}
}

/*
* Taken from RCTNativeAnimatedTurboModule:
* This selector is invoked via BridgelessTurboModuleSetup.
*/
- (void)setSurfacePresenter:(id<RCTSurfacePresenterStub>)surfacePresenter
{
surfacePresenter_ = surfacePresenter;
}


- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params {
return std::make_shared<facebook::react::NativeLiveMarkdownModuleSpecJSI>(
Expand All @@ -40,6 +70,7 @@ - (NSNumber *)install {

- (void)invalidate {
MarkdownShadowFamilyRegistry::reset();
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super invalidate];
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@expensify/react-native-live-markdown",
"version": "0.1.62",
"version": "0.1.74",
"description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.",
"main": "lib/commonjs/index",
"module": "lib/module/index",
Expand Down
28 changes: 8 additions & 20 deletions parser/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ describe('mention-user', () => {
test('with punctation marks', () => {
expect('@[email protected]!').toBeParsedAs([{type: 'mention-user', start: 0, length: 14}]);
});

test('with phone number', () => {
expect('@+1234567890 Hello!').toBeParsedAs([{type: 'mention-user', start: 0, length: 12}]);
});
});

test('plain link', () => {
Expand Down Expand Up @@ -188,13 +192,6 @@ describe('blockquote', () => {
{type: 'syntax', start: 0, length: 1},
]);
});

test('without space', () => {
expect('>Hello world!').toBeParsedAs([
{type: 'blockquote', start: 0, length: 13},
{type: 'syntax', start: 0, length: 1},
]);
});
});

test('multiple blockquotes', () => {
Expand Down Expand Up @@ -242,15 +239,6 @@ test('nested bold and italic', () => {
});

describe('nested h1 in blockquote', () => {
test('without spaces', () => {
expect('># Hello world').toBeParsedAs([
{type: 'blockquote', start: 0, length: 14},
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 2},
{type: 'h1', start: 3, length: 11},
]);
});

test('with single space', () => {
expect('> # Hello world').toBeParsedAs([
{type: 'blockquote', start: 0, length: 15},
Expand All @@ -261,11 +249,11 @@ describe('nested h1 in blockquote', () => {
});

test('with multiple spaces after #', () => {
expect('># Hello world').toBeParsedAs([
{type: 'blockquote', start: 0, length: 17},
expect('> # Hello world').toBeParsedAs([
{type: 'blockquote', start: 0, length: 18},
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 2},
{type: 'h1', start: 3, length: 14},
{type: 'syntax', start: 2, length: 2},
{type: 'h1', start: 4, length: 14},
]);
});
});
Expand Down
2 changes: 1 addition & 1 deletion parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"typescript": "^5.3.3"
},
"dependencies": {
"expensify-common": "Expensify/expensify-common#0b1275e1f0809a5a2365b92d9fa92b11654b164f",
"expensify-common": "Expensify/expensify-common#a018512b1e0fce4d4b5a61c00ca826b8887007ad",
"patch-package": "^8.0.0",
"underscore": "^1.13.6"
}
Expand Down
163 changes: 39 additions & 124 deletions parser/patches/expensify-common+1.0.0.patch

Large diffs are not rendered by default.

40 changes: 20 additions & 20 deletions parser/react-native-live-markdown-parser.js

Large diffs are not rendered by default.

60 changes: 36 additions & 24 deletions src/MarkdownTextInput.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ let focusTimeout: NodeJS.Timeout | null = null;
function normalizeValue(value: string) {
return value.replace(/\n$/, '');
}
// Adds one '\n' at the end of the string if it's missing
function denormalizeValue(value: string) {
return value.endsWith('\n') ? `${value}\n` : value;
}

// If an Input Method Editor is processing key input, the 'keyCode' is 229.
// https://www.w3.org/TR/uievents/#determine-keydown-keyup-keyCode
Expand Down Expand Up @@ -174,7 +178,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
const dimensions = React.useRef<Dimensions | null>(null);

if (!history.current) {
history.current = new InputHistory(100);
history.current = new InputHistory(100, 150, value || '');
}

const flattenedStyle = useMemo(() => StyleSheet.flatten(style), [style]);
Expand Down Expand Up @@ -203,7 +207,8 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
}
const parsedText = ParseUtils.parseText(target, text, cursorPosition, customMarkdownStyles, !multiline);
if (history.current && shouldAddToHistory) {
history.current.debouncedAdd(parsedText.text, parsedText.cursorPosition);
// We need to normalize the value before saving it to the history to prevent situations when additional new lines break the cursor position calculation logic
history.current.throttledAdd(normalizeValue(parsedText.text), parsedText.cursorPosition);
}

return parsedText;
Expand All @@ -214,7 +219,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
const processedMarkdownStyle = useMemo(() => {
const newMarkdownStyle = processMarkdownStyle(markdownStyle);
if (divRef.current) {
parseText(divRef.current, divRef.current.innerText, newMarkdownStyle);
parseText(divRef.current, divRef.current.innerText, newMarkdownStyle, null, false);
}
return newMarkdownStyle;
}, [markdownStyle, parseText]);
Expand All @@ -236,7 +241,8 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
(target: HTMLDivElement) => {
if (!history.current) return '';
const item = history.current.undo();
return parseText(target, item ? item.text : null, processedMarkdownStyle, item ? item.cursorPosition : null, false).text;
const undoValue = item ? denormalizeValue(item.text) : null;
return parseText(target, undoValue, processedMarkdownStyle, item ? item.cursorPosition : null, false).text;
},
[parseText, processedMarkdownStyle],
);
Expand All @@ -245,7 +251,8 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
(target: HTMLDivElement) => {
if (!history.current) return '';
const item = history.current.redo();
return parseText(target, item ? item.text : null, processedMarkdownStyle, item ? item.cursorPosition : null, false).text;
const redoValue = item ? denormalizeValue(item.text) : null;
return parseText(target, redoValue, processedMarkdownStyle, item ? item.cursorPosition : null, false).text;
},
[parseText, processedMarkdownStyle],
);
Expand Down Expand Up @@ -328,9 +335,10 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
if (!divRef.current || !(e.target instanceof HTMLElement)) {
return;
}
const changedText = e.target.innerText;

if (compositionRef.current) {
updateTextColor(divRef.current, e.target.innerText);
updateTextColor(divRef.current, changedText);
compositionRef.current = false;
return;
}
Expand All @@ -344,14 +352,22 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
case 'historyRedo':
text = redo(divRef.current);
break;
case 'insertFromPaste':
// if there is no newline at the end of the copied text, contentEditable adds invisible <br> tag at the end of the text, so we need to normalize it
if (changedText.length > 2 && changedText[changedText.length - 2] !== '\n' && changedText[changedText.length - 1] === '\n') {
text = parseText(divRef.current, normalizeValue(changedText), processedMarkdownStyle).text;
break;
}
text = parseText(divRef.current, changedText, processedMarkdownStyle).text;
break;
default:
text = parseText(divRef.current, e.target.innerText, processedMarkdownStyle).text;
text = parseText(divRef.current, changedText, processedMarkdownStyle).text;
}
if (pasteRef?.current) {
pasteRef.current = false;
updateSelection(e);
}
updateTextColor(divRef.current, e.target.innerText);
updateTextColor(divRef.current, text);

if (onChange) {
const event = e as unknown as NativeSyntheticEvent<any>;
Expand Down Expand Up @@ -437,9 +453,13 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
currentlyFocusedField.current = hostNode;
setEventProps(e);
if (divRef.current) {
const valueLength = value ? value.length : 0;
CursorUtils.setCursorPosition(divRef.current, contentSelection.current ? contentSelection.current.end : valueLength);
updateSelection(event);
if (contentSelection.current) {
CursorUtils.setCursorPosition(divRef.current, contentSelection.current.start, contentSelection.current.end);
} else {
const valueLength = value ? value.length : divRef.current.innerText.length;
CursorUtils.setCursorPosition(divRef.current, valueLength, null);
}
updateSelection(event, contentSelection.current);
}

if (onFocus) {
Expand Down Expand Up @@ -576,20 +596,12 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
if (!divRef.current || !selection || (contentSelection.current && selection.start === contentSelection.current.start && selection.end === contentSelection.current.end)) {
return;
}
CursorUtils.setCursorPosition(divRef.current, selection.start, selection.end);
updateSelection(null, {start: selection.start, end: selection.end || selection.start});
}, [selection, updateSelection]);

useEffect(() => {
if (history.current?.history.length !== 0) {
return;
}
const currentValue = value ?? '';
history.current.add(currentValue, currentValue.length);

handleContentSizeChange();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const newSelection: Selection = {start: selection.start, end: selection.end ?? selection.start};
contentSelection.current = newSelection;
updateRefSelectionVariables(newSelection);
CursorUtils.setCursorPosition(divRef.current, newSelection.start, newSelection.end);
}, [selection, updateRefSelectionVariables]);

return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
Expand Down
Loading

0 comments on commit ca19afa

Please sign in to comment.