From 6cbb4a93f76eebe0811548d351940492a8229a0c Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Sat, 23 Mar 2024 21:18:41 +0100 Subject: [PATCH 001/124] WIP --- RNLiveMarkdown.podspec | 2 +- cpp/MarkdownGlobal.cpp | 29 +++++ cpp/MarkdownGlobal.h | 22 ++++ cpp/RuntimeDecorator.cpp | 42 +++++++ cpp/RuntimeDecorator.h | 13 +++ example/babel.config.js | 1 + example/ios/Podfile.lock | 15 ++- example/package.json | 3 +- example/src/App.tsx | 21 +++- ios/LiveMarkdownModule.h | 16 +++ ios/LiveMarkdownModule.mm | 30 +++++ ios/RCTMarkdownUtils.mm | 30 ++--- package.json | 6 +- src/MarkdownTextInput.tsx | 8 ++ src/NativeLiveMarkdownModule.ts | 8 ++ src/index.tsx | 2 + src/native/getMarkdownRuntime.ts | 13 +++ src/useMarkdownParser.tsx | 31 ++++++ yarn.lock | 184 +++++++++++++++++++++++++++++++ 19 files changed, 453 insertions(+), 23 deletions(-) create mode 100644 cpp/MarkdownGlobal.cpp create mode 100644 cpp/MarkdownGlobal.h create mode 100644 cpp/RuntimeDecorator.cpp create mode 100644 cpp/RuntimeDecorator.h create mode 100644 ios/LiveMarkdownModule.h create mode 100644 ios/LiveMarkdownModule.mm create mode 100644 src/NativeLiveMarkdownModule.ts create mode 100644 src/native/getMarkdownRuntime.ts create mode 100644 src/useMarkdownParser.tsx diff --git a/RNLiveMarkdown.podspec b/RNLiveMarkdown.podspec index 56ed5a1a..cd51e764 100644 --- a/RNLiveMarkdown.podspec +++ b/RNLiveMarkdown.podspec @@ -14,7 +14,7 @@ Pod::Spec.new do |s| s.platforms = { :ios => "11.0" } s.source = { :git => "https://github.com/expensify/react-native-live-markdown.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm}" + s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{h,cpp}" s.resources = "parser/react-native-live-markdown-parser.js" diff --git a/cpp/MarkdownGlobal.cpp b/cpp/MarkdownGlobal.cpp new file mode 100644 index 00000000..e4dfaafd --- /dev/null +++ b/cpp/MarkdownGlobal.cpp @@ -0,0 +1,29 @@ +#include "MarkdownGlobal.h" + +using namespace facebook; + +namespace expensify { +namespace livemarkdown { + +std::shared_ptr globalMarkdownWorkletRuntime; + +void setMarkdownRuntime(const std::shared_ptr &markdownWorkletRuntime) { + globalMarkdownWorkletRuntime = markdownWorkletRuntime; +} + +std::shared_ptr getMarkdownRuntime() { + return globalMarkdownWorkletRuntime; +} + +std::shared_ptr globalMarkdownShareableWorklet; + +void setMarkdownWorklet(const std::shared_ptr &markdownWorklet) { + globalMarkdownShareableWorklet = markdownWorklet; +} + +std::shared_ptr getMarkdownWorklet() { + return globalMarkdownShareableWorklet; +} + +} // namespace livemarkdown +} // namespace expensify diff --git a/cpp/MarkdownGlobal.h b/cpp/MarkdownGlobal.h new file mode 100644 index 00000000..31681572 --- /dev/null +++ b/cpp/MarkdownGlobal.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "WorkletRuntime.h" + +using namespace facebook; +using namespace reanimated; + +namespace expensify { +namespace livemarkdown { + +void setMarkdownRuntime(const std::shared_ptr &markdownWorkletRuntime); + +std::shared_ptr getMarkdownRuntime(); + +void setMarkdownWorklet(const std::shared_ptr &markdownWorklet); + +std::shared_ptr getMarkdownWorklet(); + +} // namespace livemarkdown +} // namespace expensify diff --git a/cpp/RuntimeDecorator.cpp b/cpp/RuntimeDecorator.cpp new file mode 100644 index 00000000..f9d6dfbc --- /dev/null +++ b/cpp/RuntimeDecorator.cpp @@ -0,0 +1,42 @@ +#include "RuntimeDecorator.h" +#include "MarkdownGlobal.h" + +using namespace facebook; + +namespace expensify { +namespace livemarkdown { + +void injectJSIBindings(jsi::Runtime &rt) { + + rt.global().setProperty(rt, "setMarkdownRuntime", jsi::Function::createFromHostFunction( + rt, + jsi::PropNameID::forAscii(rt, "setMarkdownRuntime"), + 1, + [](jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value { + setMarkdownRuntime(reanimated::extractWorkletRuntime(rt, args[0])); + return jsi::Value::undefined(); + })); + + rt.global().setProperty(rt, "registerMarkdownWorklet", jsi::Function::createFromHostFunction( + rt, + jsi::PropNameID::forAscii(rt, "registerMarkdownWorklet"), + 1, + [](jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value { + setMarkdownWorklet(reanimated::extractShareableOrThrow(rt, args[0])); + return jsi::Value(1); + })); + +// rt.global().setProperty(rt, "unregisterMarkdownWorklet", jsi::Function::createFromHostFunction( +// rt, +// jsi::PropNameID::forAscii(rt, "unregisterMarkdownWorklet"), +// 1, +// [](jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args, size_t count) -> jsi::Value { +// auto parserId = static_cast(args[0].asNumber()); +// (void)parserId; +// return jsi::Value::undefined(); +// })); + +} + +} // namespace livemarkdown +} // namespace expensify diff --git a/cpp/RuntimeDecorator.h b/cpp/RuntimeDecorator.h new file mode 100644 index 00000000..c5554af6 --- /dev/null +++ b/cpp/RuntimeDecorator.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +using namespace facebook; + +namespace expensify { +namespace livemarkdown { + +void injectJSIBindings(jsi::Runtime &rt); + +} // namespace livemarkdown +} // namespace expensify diff --git a/example/babel.config.js b/example/babel.config.js index d9addbba..353c7d44 100644 --- a/example/babel.config.js +++ b/example/babel.config.js @@ -13,5 +13,6 @@ module.exports = { }, }, ], + 'react-native-reanimated/plugin', ], }; diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index f3bd6923..5a0867f5 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1111,10 +1111,15 @@ PODS: - React-jsi (= 0.73.4) - React-logger (= 0.73.4) - React-perflogger (= 0.73.4) - - RNLiveMarkdown (0.1.26): + - RNLiveMarkdown (0.1.28): - glog - RCT-Folly (= 2022.05.16.00) - React-Core + - RNReanimated (3.8.1): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core + - ReactCommon/turbomodule/core - SocketRocket (0.6.1) - Yoga (1.14.0) @@ -1192,6 +1197,7 @@ DEPENDENCIES: - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - RNLiveMarkdown (from `../..`) + - RNReanimated (from `../node_modules/react-native-reanimated`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -1308,6 +1314,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" RNLiveMarkdown: :path: "../.." + RNReanimated: + :path: "../node_modules/react-native-reanimated" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -1371,9 +1379,10 @@ SPEC CHECKSUMS: React-runtimescheduler: ed48e5faac6751e66ee1261c4bd01643b436f112 React-utils: 6e5ad394416482ae21831050928ae27348f83487 ReactCommon: 840a955d37b7f3358554d819446bffcf624b2522 - RNLiveMarkdown: 4abc843dc43d32c5dc29da8d0793d3544a62ecbb + RNLiveMarkdown: 4d43745e1b0b5ec526da136f3dfb204ab500f331 + RNReanimated: 3e23d4be380e295e504b7b3b3a357df60e1168a2 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 + Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 PODFILE CHECKSUM: 8cb8ab8858b4911d497d269a353fbfff868afef0 diff --git a/example/package.json b/example/package.json index 5d9b8a2c..e6018545 100644 --- a/example/package.json +++ b/example/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "react": "18.2.0", - "react-native": "0.73.4" + "react-native": "0.73.4", + "react-native-reanimated": "^3.8.1" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/example/src/App.tsx b/example/src/App.tsx index ebde39b4..a107ab47 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -2,10 +2,11 @@ import * as React from 'react'; import {Button, Platform, StyleSheet, Text, View} from 'react-native'; -import {MarkdownTextInput} from '@expensify/react-native-live-markdown'; +import {MarkdownTextInput, useMarkdownParser} from '@expensify/react-native-live-markdown'; import type {TextInput} from 'react-native'; +import {useSharedValue, withRepeat, withTiming} from 'react-native-reanimated'; -const DEFAULT_TEXT = ['Hello, *world*!', 'https://expensify.com', '# Lorem ipsum', '> Hello world', '`foo`', '```\nbar\n```', '@here', '@someone@swmansion.com'].join('\n'); +const DEFAULT_TEXT = ['Hello, *world*!'].join('\n'); function isWeb() { return Platform.OS === 'web'; @@ -67,6 +68,21 @@ export default function App() { // TODO: use MarkdownTextInput ref instead of TextInput ref const ref = React.useRef(null); + const parser = useMarkdownParser((text: string) => { + 'worklet'; + + // eslint-disable-next-line no-console + // console.log(_WORKLET, Math.random()); + + const matches = [...text.matchAll(/[@#][a-z]+/gi)]; + + return matches.map((match) => ({ + start: match.index, + length: match[0].length, + type: match[0].startsWith('@') ? 'mention-here' : 'link', + })); + }, []); + return ( @@ -98,6 +114,7 @@ export default function App() { style={styles.input} ref={ref} markdownStyle={markdownStyle} + parser={parser} placeholder="Type here..." onSelectionChange={(e) => setSelection(e.nativeEvent.selection)} selection={selection} diff --git a/ios/LiveMarkdownModule.h b/ios/LiveMarkdownModule.h new file mode 100644 index 00000000..42fbb1e2 --- /dev/null +++ b/ios/LiveMarkdownModule.h @@ -0,0 +1,16 @@ +#ifdef RCT_NEW_ARCH_ENABLED +#import +#else +#import +#endif // RCT_NEW_ARCH_ENABLED + +#import + +// Without inheriting after RCTEventEmitter we don't get access to bridge +@interface LiveMarkdownModule : RCTEventEmitter +#ifdef RCT_NEW_ARCH_ENABLED + +#else + +#endif // RCT_NEW_ARCH_ENABLED +@end diff --git a/ios/LiveMarkdownModule.mm b/ios/LiveMarkdownModule.mm new file mode 100644 index 00000000..d3a19452 --- /dev/null +++ b/ios/LiveMarkdownModule.mm @@ -0,0 +1,30 @@ +#import "LiveMarkdownModule.h" +#import + +#import +#import + +using namespace facebook; + +@implementation LiveMarkdownModule { + BOOL installed_; +} + +RCT_EXPORT_MODULE() + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) +{ + RCTCxxBridge *cxxBridge = (RCTCxxBridge *)[RCTBridge currentBridge]; + jsi::Runtime &rt = *(jsi::Runtime *)cxxBridge.runtime; + expensify::livemarkdown::injectJSIBindings(rt); + return @(1); +} + +#ifdef RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} +#endif // RCT_NEW_ARCH_ENABLED + +@end diff --git a/ios/RCTMarkdownUtils.mm b/ios/RCTMarkdownUtils.mm index d06c440d..d2fef927 100644 --- a/ios/RCTMarkdownUtils.mm +++ b/ios/RCTMarkdownUtils.mm @@ -1,8 +1,9 @@ #import +#import #import "react_native_assert.h" #import #import -#import +#include @implementation RCTMarkdownUtils { NSString *_prevInputString; @@ -32,20 +33,21 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input return _prevAttributedString; } - static JSContext *ctx = nil; - static JSValue *function = nil; - if (ctx == nil) { - NSString *path = [[NSBundle mainBundle] pathForResource:@"react-native-live-markdown-parser" ofType:@"js"]; - assert(path != nil && "[react-native-live-markdown] Markdown parser bundle not found"); - NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL]; - assert(content != nil && "[react-native-live-markdown] Markdown parser bundle is empty"); - ctx = [[JSContext alloc] init]; - [ctx evaluateScript:content]; - function = ctx[@"parseExpensiMarkToRanges"]; - } + auto markdownRuntime = expensify::livemarkdown::getMarkdownRuntime(); + jsi::Runtime &rt = markdownRuntime->getJSIRuntime(); + + auto markdownWorklet = expensify::livemarkdown::getMarkdownWorklet(); - JSValue *result = [function callWithArguments:@[inputString]]; - NSArray *ranges = [result toArray]; + auto text = jsi::String::createFromUtf8(rt, [inputString UTF8String]); + auto output = markdownRuntime->runGuarded(markdownWorklet, text); + + auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, output).asString(rt).utf8(rt); + NSData *data = [NSData dataWithBytes:json.data() length:json.length()]; + NSError *error = nil; + NSArray *ranges = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + if (error != nil) { + return input; + } NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:inputString attributes:_backedTextInputView.defaultTextAttributes]; [attributedString beginEditing]; diff --git a/package.json b/package.json index 9b0b995d..638c0d32 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "react": "18.2.0", "react-native": "0.73.4", "react-native-builder-bob": "^0.20.0", + "react-native-reanimated": "^3.8.1", "react-native-web": "^0.19.10", "release-it": "^15.0.0", "turbo": "^1.10.7", @@ -101,7 +102,8 @@ }, "peerDependencies": { "react": "*", - "react-native": "*" + "react-native": "*", + "react-native-reanimated": ">=3.6.0" }, "workspaces": [ "example" @@ -156,7 +158,7 @@ }, "codegenConfig": { "name": "RNLiveMarkdownSpec", - "type": "components", + "type": "all", "jsSrcsDir": "src" } } diff --git a/src/MarkdownTextInput.tsx b/src/MarkdownTextInput.tsx index 41a00187..10a2c3f7 100644 --- a/src/MarkdownTextInput.tsx +++ b/src/MarkdownTextInput.tsx @@ -2,9 +2,17 @@ import {StyleSheet, TextInput, processColor} from 'react-native'; import React from 'react'; import type {TextInputProps} from 'react-native'; import MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent'; +import NativeLiveMarkdownModule from './NativeLiveMarkdownModule'; import type * as MarkdownTextInputDecoratorViewNativeComponentTypes from './MarkdownTextInputDecoratorViewNativeComponent'; import * as StyleUtils from './styleUtils'; import type * as StyleUtilsTypes from './styleUtils'; +import getMarkdownRuntime from './native/getMarkdownRuntime'; + +NativeLiveMarkdownModule.install(); + +const markdownRuntime = getMarkdownRuntime(); +// @ts-expect-error TODO +global.setMarkdownRuntime(markdownRuntime); type PartialMarkdownStyle = StyleUtilsTypes.PartialMarkdownStyle; type MarkdownStyle = MarkdownTextInputDecoratorViewNativeComponentTypes.MarkdownStyle; diff --git a/src/NativeLiveMarkdownModule.ts b/src/NativeLiveMarkdownModule.ts new file mode 100644 index 00000000..5a2efbaf --- /dev/null +++ b/src/NativeLiveMarkdownModule.ts @@ -0,0 +1,8 @@ +import type {TurboModule} from 'react-native'; +import {TurboModuleRegistry} from 'react-native'; + +interface Spec extends TurboModule { + install: () => boolean; +} + +export default TurboModuleRegistry.getEnforcing('LiveMarkdownModule'); diff --git a/src/index.tsx b/src/index.tsx index 7e1e7507..eee79759 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,2 +1,4 @@ export {default as MarkdownTextInput} from './MarkdownTextInput'; export type {MarkdownTextInputProps, MarkdownStyle} from './MarkdownTextInput'; +export {default as useMarkdownParser} from './useMarkdownParser'; +export type {Range} from './useMarkdownParser'; diff --git a/src/native/getMarkdownRuntime.ts b/src/native/getMarkdownRuntime.ts new file mode 100644 index 00000000..733df0c0 --- /dev/null +++ b/src/native/getMarkdownRuntime.ts @@ -0,0 +1,13 @@ +import {createWorkletRuntime} from 'react-native-reanimated'; +import type {WorkletRuntime} from 'react-native-reanimated'; + +let markdownRuntime: WorkletRuntime | undefined; + +function getMarkdownRuntime(): WorkletRuntime { + if (markdownRuntime === undefined) { + markdownRuntime = createWorkletRuntime('LiveMarkdownRuntime'); + } + return markdownRuntime; +} + +export default getMarkdownRuntime; diff --git a/src/useMarkdownParser.tsx b/src/useMarkdownParser.tsx new file mode 100644 index 00000000..d8f205ff --- /dev/null +++ b/src/useMarkdownParser.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import {makeShareableCloneRecursive} from 'react-native-reanimated'; + +interface Range { + type: string; + start: number; + length: number; +} + +function useMarkdownParser(worklet: (text: string) => Range[], deps: unknown[]) { + // eslint-disable-next-line no-underscore-dangle + const workletHash = (worklet as unknown as {__workletHash: number}).__workletHash; + + const parserId = React.useMemo(() => { + if (parserId !== undefined) { + // @ts-expect-error TODO + global.unregisterMarkdownWorklet(parserId); + } + + const shareableWorklet = makeShareableCloneRecursive(worklet); + // @ts-expect-error TODO + global.registerMarkdownWorklet(shareableWorklet); + return Math.random(); + }, [workletHash, ...deps]); + + return parserId; +} + +export default useMarkdownParser; + +export type {Range}; diff --git a/yarn.lock b/yarn.lock index 7789afdf..3853a018 100644 --- a/yarn.lock +++ b/yarn.lock @@ -161,6 +161,25 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/helper-create-class-features-plugin@npm:7.24.1" + dependencies: + "@babel/helper-annotate-as-pure": ^7.22.5 + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-function-name": ^7.23.0 + "@babel/helper-member-expression-to-functions": ^7.23.0 + "@babel/helper-optimise-call-expression": ^7.22.5 + "@babel/helper-replace-supers": ^7.24.1 + "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.6 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 310d063eafbd2a777609770c1aa7b24e43f375122fd84031c45edc512686000197da1cf450b48eca266489131bc06dbaa35db2afed8b7213c9bcfa8c89b82c4d + languageName: node + linkType: hard + "@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.22.15, @babel/helper-create-regexp-features-plugin@npm:^7.22.5": version: 7.22.15 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.22.15" @@ -264,6 +283,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.24.0": + version: 7.24.0 + resolution: "@babel/helper-plugin-utils@npm:7.24.0" + checksum: e2baa0eede34d2fa2265947042aa84d444aa48dc51e9feedea55b67fc1bc3ab051387e18b33ca7748285a6061390831ab82f8a2c767d08470b93500ec727e9b9 + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.18.9, @babel/helper-remap-async-to-generator@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-remap-async-to-generator@npm:7.22.20" @@ -290,6 +316,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-replace-supers@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/helper-replace-supers@npm:7.24.1" + dependencies: + "@babel/helper-environment-visitor": ^7.22.20 + "@babel/helper-member-expression-to-functions": ^7.23.0 + "@babel/helper-optimise-call-expression": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: c04182c34a3195c6396de2f2945f86cb60daa94ca7392db09bd8b0d4e7a15b02fbe1947c70f6062c87eadaea6d7135207129efa35cf458ea0987bab8c0f02d5a + languageName: node + linkType: hard + "@babel/helper-simple-access@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-simple-access@npm:7.22.5" @@ -690,6 +729,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-jsx@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-syntax-jsx@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 712f7e7918cb679f106769f57cfab0bc99b311032665c428b98f4c3e2e6d567601d45386a4f246df6a80d741e1f94192b3f008800d66c4f1daae3ad825c243f0 + languageName: node + linkType: hard + "@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4, @babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" @@ -789,6 +839,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-typescript@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-syntax-typescript@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bf4bd70788d5456b5f75572e47a2e31435c7c4e43609bd4dffd2cc0c7a6cf90aabcf6cd389e351854de9a64412a07d30effef5373251fe8f6a4c9db0c0163bda + languageName: node + linkType: hard + "@babel/plugin-syntax-unicode-sets-regex@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-syntax-unicode-sets-regex@npm:7.18.6" @@ -812,6 +873,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-arrow-functions@npm:^7.0.0-0": + version: 7.24.1 + resolution: "@babel/plugin-transform-arrow-functions@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 58f9aa9b0de8382f8cfa3f1f1d40b69d98cd2f52340e2391733d0af745fdddda650ba392e509bc056157c880a2f52834a38ab2c5aa5569af8c61bb6ecbf45f34 + languageName: node + linkType: hard + "@babel/plugin-transform-async-generator-functions@npm:^7.23.7": version: 7.23.7 resolution: "@babel/plugin-transform-async-generator-functions@npm:7.23.7" @@ -1094,6 +1166,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-commonjs@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.24.1" + dependencies: + "@babel/helper-module-transforms": ^7.23.3 + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-simple-access": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 11402b34c49f76aa921b43c2d76f3f129a32544a1dc4f0d1e48b310f9036ab75269a6d8684ed0198b7a0b07bd7898b12f0cacceb26fbb167999fd2a819aa0802 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-systemjs@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.23.3" @@ -1143,6 +1228,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.0.0-0": + version: 7.24.1 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 74025e191ceb7cefc619c15d33753aab81300a03d81b96ae249d9b599bc65878f962d608f452462d3aad5d6e334b7ab2b09a6bdcfe8d101fe77ac7aacca4261e + languageName: node + linkType: hard + "@babel/plugin-transform-nullish-coalescing-operator@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.23.4" @@ -1206,6 +1303,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-optional-chaining@npm:^7.0.0-0": + version: 7.24.1 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-skip-transparent-expression-wrappers": ^7.22.5 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 0eb5f4abdeb1a101c0f67ef25eba4cce0978a74d8722f6222cdb179a28e60d21ab545eda231855f50169cd63d604ec8268cff44ae9370fd3a499a507c56c2bbd + languageName: node + linkType: hard + "@babel/plugin-transform-optional-chaining@npm:^7.23.3, @babel/plugin-transform-optional-chaining@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-optional-chaining@npm:7.23.4" @@ -1388,6 +1498,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-shorthand-properties@npm:^7.0.0-0": + version: 7.24.1 + resolution: "@babel/plugin-transform-shorthand-properties@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 006a2032d1c57dca76579ce6598c679c2f20525afef0a36e9d42affe3c8cf33c1427581ad696b519cc75dfee46c5e8ecdf0c6a29ffb14250caa3e16dd68cb424 + languageName: node + linkType: hard + "@babel/plugin-transform-spread@npm:^7.0.0, @babel/plugin-transform-spread@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-spread@npm:7.23.3" @@ -1411,6 +1532,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-template-literals@npm:^7.0.0-0": + version: 7.24.1 + resolution: "@babel/plugin-transform-template-literals@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 4c9009c72321caf20e3b6328bbe9d7057006c5ae57b794cf247a37ca34d87dfec5e27284169a16df5a6235a083bf0f3ab9e1bfcb005d1c8b75b04aed75652621 + languageName: node + linkType: hard + "@babel/plugin-transform-template-literals@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-template-literals@npm:7.23.3" @@ -1447,6 +1579,20 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-typescript@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/plugin-transform-typescript@npm:7.24.1" + dependencies: + "@babel/helper-annotate-as-pure": ^7.22.5 + "@babel/helper-create-class-features-plugin": ^7.24.1 + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/plugin-syntax-typescript": ^7.24.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 1a37fa55ab176b11c3763da4295651b3db38f0a7f3d47b5cd5ab1e33cbcbbf2b471c4bdb7b24f39392d4660409209621c8d11c521de2deffddc3d876a1b60482 + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-escapes@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-unicode-escapes@npm:7.23.3" @@ -1641,6 +1787,21 @@ __metadata: languageName: node linkType: hard +"@babel/preset-typescript@npm:^7.16.7": + version: 7.24.1 + resolution: "@babel/preset-typescript@npm:7.24.1" + dependencies: + "@babel/helper-plugin-utils": ^7.24.0 + "@babel/helper-validator-option": ^7.23.5 + "@babel/plugin-syntax-jsx": ^7.24.1 + "@babel/plugin-transform-modules-commonjs": ^7.24.1 + "@babel/plugin-transform-typescript": ^7.24.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f3e0ff8c20dd5abc82614df2d7953f1549a98282b60809478f7dfb41c29be63720f2d1d7a51ef1f0d939b65e8666cb7d36e32bc4f8ac2b74c20664efd41e8bdd + languageName: node + linkType: hard + "@babel/register@npm:^7.13.16": version: 7.23.7 resolution: "@babel/register@npm:7.23.7" @@ -1833,6 +1994,7 @@ __metadata: pod-install: ^0.1.0 react: 18.2.0 react-native: 0.73.4 + react-native-reanimated: ^3.8.1 languageName: unknown linkType: soft @@ -1873,6 +2035,7 @@ __metadata: react: 18.2.0 react-native: 0.73.4 react-native-builder-bob: ^0.20.0 + react-native-reanimated: ^3.8.1 react-native-web: ^0.19.10 release-it: ^15.0.0 turbo: ^1.10.7 @@ -1880,6 +2043,7 @@ __metadata: peerDependencies: react: "*" react-native: "*" + react-native-reanimated: ">=3.6.0" languageName: unknown linkType: soft @@ -11784,6 +11948,26 @@ __metadata: languageName: node linkType: hard +"react-native-reanimated@npm:^3.8.1": + version: 3.8.1 + resolution: "react-native-reanimated@npm:3.8.1" + dependencies: + "@babel/plugin-transform-arrow-functions": ^7.0.0-0 + "@babel/plugin-transform-nullish-coalescing-operator": ^7.0.0-0 + "@babel/plugin-transform-optional-chaining": ^7.0.0-0 + "@babel/plugin-transform-shorthand-properties": ^7.0.0-0 + "@babel/plugin-transform-template-literals": ^7.0.0-0 + "@babel/preset-typescript": ^7.16.7 + convert-source-map: ^2.0.0 + invariant: ^2.2.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + react: "*" + react-native: "*" + checksum: 078a1e32ce1ca11488862db90663fd5cacb61aea65dc540c90cd5346d97e63cc6836f25cfc48b7acefd301fda414f05329a92ae5cb28fb83b94c571e66f147e3 + languageName: node + linkType: hard + "react-native-web@npm:^0.19.10": version: 0.19.10 resolution: "react-native-web@npm:0.19.10" From 5d97778b0ab2f102e8cc7ccaec1d53d40d68184b Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Mon, 25 Mar 2024 20:54:21 +0100 Subject: [PATCH 002/124] make it work on android --- android/build.gradle | 11 +---- android/src/main/cpp/CMakeLists.txt | 12 ++++-- android/src/main/cpp/MarkdownUtils.cpp | 31 +++++--------- android/src/main/cpp/MarkdownUtils.h | 6 --- android/src/main/cpp/OnLoad.cpp | 6 +++ .../livemarkdown/LiveMarkdownModule.java | 36 +++++++++++++++++ .../livemarkdown/LiveMarkdownPackage.java | 40 ++++++++++++++++++- .../MarkdownTextInputDecoratorView.java | 1 - .../expensify/livemarkdown/MarkdownUtils.java | 23 ----------- .../src/newarch/LiveMarkdownModuleSpec.java | 9 +++++ .../src/oldarch/LiveMarkdownModuleSpec.java | 12 ++++++ 11 files changed, 121 insertions(+), 66 deletions(-) create mode 100644 android/src/main/java/com/expensify/livemarkdown/LiveMarkdownModule.java create mode 100644 android/src/newarch/LiveMarkdownModuleSpec.java create mode 100644 android/src/oldarch/LiveMarkdownModuleSpec.java diff --git a/android/build.gradle b/android/build.gradle index 86dcfd2e..265cbd42 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -110,6 +110,7 @@ android { "**/libfbjni.so", "**/libjsi.so", "**/libreactnativejni.so", + "**/libreact_nativemodule_core.so", ] } } @@ -123,6 +124,7 @@ repositories { dependencies { implementation "com.facebook.react:react-android" // version substituted by RNGP implementation "com.facebook.react:hermes-android" // version substituted by RNGP + implementation project(":react-native-reanimated") } if (isNewArchitectureEnabled()) { @@ -132,12 +134,3 @@ if (isNewArchitectureEnabled()) { codegenJavaPackageName = "com.expensify.livemarkdown" } } - -task copyJS(type: Copy) { - from '../parser/react-native-live-markdown-parser.js' - into 'src/main/assets' -} - -tasks.preBuild { - dependsOn copyJS -} diff --git a/android/src/main/cpp/CMakeLists.txt b/android/src/main/cpp/CMakeLists.txt index f846aa33..0db2f3e3 100644 --- a/android/src/main/cpp/CMakeLists.txt +++ b/android/src/main/cpp/CMakeLists.txt @@ -6,18 +6,22 @@ set(CMAKE_VERBOSE_MAKEFILE on) add_compile_options(-fvisibility=hidden -fexceptions -frtti) -file(GLOB livemarkdown_SRC CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) +file(GLOB android_SRC CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) +file(GLOB cpp_SRC CONFIGURE_DEPENDS /Users/tomekzaw/Expensify/react-native-live-markdown/cpp/*.cpp) -add_library(${CMAKE_PROJECT_NAME} SHARED ${livemarkdown_SRC}) +add_library(${CMAKE_PROJECT_NAME} SHARED ${android_SRC} ${cpp_SRC}) -target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} /Users/tomekzaw/Expensify/react-native-live-markdown/cpp) find_package(fbjni REQUIRED CONFIG) find_package(ReactAndroid REQUIRED CONFIG) find_package(hermes-engine REQUIRED CONFIG) +find_package(react-native-reanimated REQUIRED CONFIG) target_link_libraries(${CMAKE_PROJECT_NAME} fbjni::fbjni ReactAndroid::jsi ReactAndroid::reactnativejni - hermes-engine::libhermes) + hermes-engine::libhermes + react-native-reanimated::reanimated + ReactAndroid::react_nativemodule_core) diff --git a/android/src/main/cpp/MarkdownUtils.cpp b/android/src/main/cpp/MarkdownUtils.cpp index 63c69a72..9f2d4720 100644 --- a/android/src/main/cpp/MarkdownUtils.cpp +++ b/android/src/main/cpp/MarkdownUtils.cpp @@ -1,4 +1,5 @@ #include "MarkdownUtils.h" +#include "MarkdownGlobal.h" #include #include @@ -7,36 +8,24 @@ using namespace facebook; namespace expensify { namespace livemarkdown { - std::shared_ptr MarkdownUtils::runtime_; - - void MarkdownUtils::nativeInitializeRuntime( - jni::alias_ref jThis, - jni::alias_ref code) { - assert(runtime_ == nullptr && "Markdown runtime is already initialized"); - runtime_ = facebook::hermes::makeHermesRuntime(); - auto codeBuffer = std::make_shared(code->toStdString()); - runtime_->evaluateJavaScript(codeBuffer, "nativeInitializeRuntime"); - } - jni::local_ref MarkdownUtils::nativeParseMarkdown( jni::alias_ref jThis, jni::alias_ref input) { - jsi::Runtime &rt = *runtime_; - auto func = rt.global().getPropertyAsFunction(rt, "parseExpensiMarkToRanges"); - auto arg = input->toStdString(); - jsi::Value result; - try { - result = func.call(rt, arg); - } catch (jsi::JSError e) { - result = jsi::Array(rt, 0); - } + + auto markdownRuntime = expensify::livemarkdown::getMarkdownRuntime(); + jsi::Runtime &rt = markdownRuntime->getJSIRuntime(); + + auto markdownWorklet = expensify::livemarkdown::getMarkdownWorklet(); + + auto text = jsi::String::createFromUtf8(rt, input->toStdString()); + auto result = markdownRuntime->runGuarded(markdownWorklet, text); + auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, result).asString(rt).utf8(rt); return jni::make_jstring(json); } void MarkdownUtils::registerNatives() { registerHybrid({ - makeNativeMethod("nativeInitializeRuntime", MarkdownUtils::nativeInitializeRuntime), makeNativeMethod("nativeParseMarkdown", MarkdownUtils::nativeParseMarkdown)}); } diff --git a/android/src/main/cpp/MarkdownUtils.h b/android/src/main/cpp/MarkdownUtils.h index eb951bae..1c343da1 100644 --- a/android/src/main/cpp/MarkdownUtils.h +++ b/android/src/main/cpp/MarkdownUtils.h @@ -21,10 +21,6 @@ namespace livemarkdown { static constexpr auto kJavaDescriptor = "Lcom/expensify/livemarkdown/MarkdownUtils;"; - static void nativeInitializeRuntime( - jni::alias_ref jThis, - jni::alias_ref code); - static jni::local_ref nativeParseMarkdown( jni::alias_ref jThis, jni::alias_ref input); @@ -32,8 +28,6 @@ namespace livemarkdown { static void registerNatives(); private: - static std::shared_ptr runtime_; - friend HybridBase; }; diff --git a/android/src/main/cpp/OnLoad.cpp b/android/src/main/cpp/OnLoad.cpp index deddae9b..47086340 100644 --- a/android/src/main/cpp/OnLoad.cpp +++ b/android/src/main/cpp/OnLoad.cpp @@ -1,8 +1,14 @@ #include #include "MarkdownUtils.h" +#include "RuntimeDecorator.h" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { return facebook::jni::initialize( vm, [] { expensify::livemarkdown::MarkdownUtils::registerNatives(); }); } + +extern "C" JNIEXPORT void JNICALL Java_com_expensify_livemarkdown_LiveMarkdownModule_injectJSIBindings(JNIEnv *env, jobject thiz, jlong jsiRuntime) { + jsi::Runtime &rt = *reinterpret_cast(jsiRuntime); + expensify::livemarkdown::injectJSIBindings(rt); +} diff --git a/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownModule.java b/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownModule.java new file mode 100644 index 00000000..a887efb0 --- /dev/null +++ b/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownModule.java @@ -0,0 +1,36 @@ +package com.expensify.livemarkdown; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.soloader.SoLoader; + +import java.util.Objects; + +public class LiveMarkdownModule extends com.expensify.livemarkdown.LiveMarkdownModuleSpec { + static { + SoLoader.loadLibrary("livemarkdown"); + } + + public static final String NAME = "LiveMarkdownModule"; + + LiveMarkdownModule(ReactApplicationContext context) { + super(context); + } + + @Override + @NonNull + public String getName() { + return NAME; + } + + @ReactMethod(isBlockingSynchronousMethod = true) + public boolean install() { + long jsiRuntime = Objects.requireNonNull(getReactApplicationContext().getJavaScriptContextHolder()).get(); + injectJSIBindings(jsiRuntime); + return true; + } + + private native void injectJSIBindings(long jsiRuntime); +} diff --git a/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownPackage.java b/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownPackage.java index c1c8fece..a1fcf450 100644 --- a/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownPackage.java +++ b/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownPackage.java @@ -1,15 +1,21 @@ package com.expensify.livemarkdown; -import com.facebook.react.ReactPackage; +import androidx.annotation.Nullable; + import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.react.module.model.ReactModuleInfoProvider; +import com.facebook.react.TurboReactPackage; import com.facebook.react.uimanager.ViewManager; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; -public class LiveMarkdownPackage implements ReactPackage { +public class LiveMarkdownPackage extends TurboReactPackage { @Override public List createViewManagers(ReactApplicationContext reactContext) { List viewManagers = new ArrayList<>(); @@ -21,4 +27,34 @@ public List createViewManagers(ReactApplicationContext reactContext public List createNativeModules(ReactApplicationContext reactContext) { return Collections.emptyList(); } + + @Nullable + @Override + public NativeModule getModule(String name, ReactApplicationContext reactContext) { + if (name.equals(LiveMarkdownModule.NAME)) { + return new LiveMarkdownModule(reactContext); + } else { + return null; + } + } + + @Override + public ReactModuleInfoProvider getReactModuleInfoProvider() { + return () -> { + final Map moduleInfos = new HashMap<>(); + boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; + moduleInfos.put( + LiveMarkdownModule.NAME, + new ReactModuleInfo( + LiveMarkdownModule.NAME, + LiveMarkdownModule.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + true, // hasConstants + false, // isCxxModule + isTurboModule // isTurboModule + )); + return moduleInfos; + }; + } } diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java index 51361af3..05df10de 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java @@ -53,7 +53,6 @@ protected void onAttachedToWindow() { if (previousSibling instanceof ReactEditText) { AssetManager assetManager = getContext().getAssets(); - MarkdownUtils.maybeInitializeRuntime(assetManager); mMarkdownUtils = new MarkdownUtils(assetManager); mMarkdownUtils.setMarkdownStyle(mMarkdownStyle); mReactEditText = (ReactEditText) previousSibling; diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java index e5ce7ef7..b442bf99 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java @@ -27,29 +27,6 @@ public class MarkdownUtils { SoLoader.loadLibrary("livemarkdown"); } - private static boolean IS_RUNTIME_INITIALIZED = false; - - @ThreadConfined(UI) - public static void maybeInitializeRuntime(AssetManager assetManager) { - UiThreadUtil.assertOnUiThread(); - if (IS_RUNTIME_INITIALIZED) { - return; - } - try { - InputStream inputStream = assetManager.open("react-native-live-markdown-parser.js"); - byte[] buffer = new byte[inputStream.available()]; - inputStream.read(buffer); - inputStream.close(); - String code = new String(buffer); - nativeInitializeRuntime(code); - IS_RUNTIME_INITIALIZED = true; - } catch (IOException e) { - throw new RuntimeException("Failed to initialize Markdown runtime"); - } - } - - private static native void nativeInitializeRuntime(String code); - @ThreadConfined(UI) private static String parseMarkdown(String input) { UiThreadUtil.assertOnUiThread(); diff --git a/android/src/newarch/LiveMarkdownModuleSpec.java b/android/src/newarch/LiveMarkdownModuleSpec.java new file mode 100644 index 00000000..41e499ac --- /dev/null +++ b/android/src/newarch/LiveMarkdownModuleSpec.java @@ -0,0 +1,9 @@ +package com.expensify.livemarkdown; + +import com.facebook.react.bridge.ReactApplicationContext; + +abstract class LiveMarkdownModuleSpec extends NativeLiveMarkdownModuleSpec { + LiveMarkdownModuleSpec(ReactApplicationContext context) { + super(context); + } +} diff --git a/android/src/oldarch/LiveMarkdownModuleSpec.java b/android/src/oldarch/LiveMarkdownModuleSpec.java new file mode 100644 index 00000000..1a0b682b --- /dev/null +++ b/android/src/oldarch/LiveMarkdownModuleSpec.java @@ -0,0 +1,12 @@ +package com.expensify.livemarkdown; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; + +abstract class LiveMarkdownModuleSpec extends ReactContextBaseJavaModule { + LiveMarkdownModuleSpec(ReactApplicationContext context) { + super(context); + } + + public abstract boolean install(); +} From 45bbd66e1357371c0bda139cfe21e378eea3bc4d Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Mon, 25 Mar 2024 21:09:10 +0100 Subject: [PATCH 003/124] remove parser from resources --- RNLiveMarkdown.podspec | 2 -- example/ios/Podfile.lock | 2 +- package.json | 6 ++++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/RNLiveMarkdown.podspec b/RNLiveMarkdown.podspec index cd51e764..aaa38d40 100644 --- a/RNLiveMarkdown.podspec +++ b/RNLiveMarkdown.podspec @@ -16,8 +16,6 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{h,cpp}" - s.resources = "parser/react-native-live-markdown-parser.js" - # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. if respond_to?(:install_modules_dependencies, true) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 00459b46..6246f9de 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1379,7 +1379,7 @@ SPEC CHECKSUMS: React-runtimescheduler: ed48e5faac6751e66ee1261c4bd01643b436f112 React-utils: 6e5ad394416482ae21831050928ae27348f83487 ReactCommon: 840a955d37b7f3358554d819446bffcf624b2522 - RNLiveMarkdown: 7874e32d4e1013ecb7df8743d3bcf8c8ba0f76a1 + RNLiveMarkdown: 5aa5235076bf1beaeaed4f0770ea1d707f840b18 RNReanimated: 3e23d4be380e295e504b7b3b3a357df60e1168a2 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 diff --git a/package.json b/package.json index feb63565..40ceabde 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "files": [ "src", "lib", - "parser/react-native-live-markdown-parser.js", "android", "ios", "cpp", @@ -159,6 +158,9 @@ "codegenConfig": { "name": "RNLiveMarkdownSpec", "type": "all", - "jsSrcsDir": "src" + "jsSrcsDir": "src", + "android": { + "javaPackageName": "com.expensify.livemarkdown" + } } } From 12e2798f8b12e6d25c56c4be722d47479121d722 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Tue, 26 Mar 2024 09:42:38 +0100 Subject: [PATCH 004/124] make it work on android fabric --- android/build.gradle | 1 + package.json | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 265cbd42..7fb0b823 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -111,6 +111,7 @@ android { "**/libjsi.so", "**/libreactnativejni.so", "**/libreact_nativemodule_core.so", + "**/libreanimated.so", ] } } diff --git a/package.json b/package.json index 40ceabde..4dae72c2 100644 --- a/package.json +++ b/package.json @@ -158,9 +158,6 @@ "codegenConfig": { "name": "RNLiveMarkdownSpec", "type": "all", - "jsSrcsDir": "src", - "android": { - "javaPackageName": "com.expensify.livemarkdown" - } + "jsSrcsDir": "src" } } From 258d9b2d54578fb0f0a199b662540ef78b9a1c6c Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Wed, 27 Mar 2024 12:05:32 +0100 Subject: [PATCH 005/124] plug expensimark --- .../expensify-common-https-ae2a96d818.patch | 215 ++++++++++++++++++ example/package.json | 1 + example/src/App.tsx | 8 +- package.json | 3 +- yarn.lock | 168 +++++++++++++- 5 files changed, 388 insertions(+), 7 deletions(-) create mode 100644 .yarn/patches/expensify-common-https-ae2a96d818.patch diff --git a/.yarn/patches/expensify-common-https-ae2a96d818.patch b/.yarn/patches/expensify-common-https-ae2a96d818.patch new file mode 100644 index 00000000..202390e4 --- /dev/null +++ b/.yarn/patches/expensify-common-https-ae2a96d818.patch @@ -0,0 +1,215 @@ +diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js +index 02d408c88f1823204041a34c386a23720abab104..6cfbff608194e19241e1ae9a86cc1164f19b7189 100644 +--- a/lib/ExpensiMark.js ++++ b/lib/ExpensiMark.js +@@ -1,4 +1,3 @@ +-import _ from 'underscore'; + import Str from './str'; + import {MARKDOWN_URL_REGEX, LOOSE_URL_REGEX, URL_REGEX} from './Url'; + import {CONST} from './CONST'; +@@ -9,8 +8,11 @@ const MARKDOWN_IMAGE_REGEX = new RegExp(`\\!${MARKDOWN_LINK}`, 'gi'); + + const SLACK_SPAN_NEW_LINE_TAG = ''; + +-export default class ExpensiMark { +- constructor() { ++export function makeExpensiMark() { ++'worklet'; ++ ++const ExpensiMark = { ++ initialize() { + /** + * The list of regex replacements to do on a comment. Check the link regex is first so links are processed + * before other delimiters +@@ -516,7 +518,7 @@ export default class ExpensiMark { + * @type {Number} + */ + this.currentQuoteDepth = 0; +- } ++ }, + + /** + * Replaces markdown with html elements +@@ -530,10 +532,31 @@ export default class ExpensiMark { + * @returns {String} + */ + replace(text, {filterRules = [], shouldEscapeText = true, shouldKeepRawInput = false} = {}) { ++ function escape(text) { ++ const matchHtmlRegExp = /["'&<>]/g; ++ ++ return text.replace(matchHtmlRegExp, function(match) { ++ switch (match) { ++ case '&': ++ return '&'; ++ case '<': ++ return '<'; ++ case '>': ++ return '>'; ++ case '"': ++ return '"'; ++ case "'": ++ return '''; ++ default: ++ return match; ++ } ++ }); ++ } ++ + // This ensures that any html the user puts into the comment field shows as raw html +- let replacedText = shouldEscapeText ? _.escape(text) : text; ++ let replacedText = shouldEscapeText ? escape(text) : text; + const enabledRules = shouldKeepRawInput ? this.shouldKeepWhitespaceRules : this.rules; +- const rules = _.isEmpty(filterRules) ? enabledRules : _.filter(this.rules, rule => _.contains(filterRules, rule.name)); ++ const rules = filterRules.length === 0 ? enabledRules : this.rules.filter(rule => filterRules.includes(rule.name)); + + try { + rules.forEach((rule) => { +@@ -557,12 +580,33 @@ export default class ExpensiMark { + // eslint-disable-next-line no-console + console.warn('Error replacing text with html in ExpensiMark.replace', {error: e}); + ++ function escape(text) { ++ const matchHtmlRegExp = /["'&<>]/g; ++ ++ return text.replace(matchHtmlRegExp, function(match) { ++ switch (match) { ++ case '&': ++ return '&'; ++ case '<': ++ return '<'; ++ case '>': ++ return '>'; ++ case '"': ++ return '"'; ++ case "'": ++ return '''; ++ default: ++ return match; ++ } ++ }); ++ } ++ + // We want to return text without applying rules if exception occurs during replacing +- return shouldEscapeText ? _.escape(text) : text; ++ return shouldEscapeText ? escape(text) : text; + } + + return replacedText; +- } ++ }, + + /** + * Checks matched URLs for validity and replace valid links with html elements +@@ -669,7 +713,7 @@ export default class ExpensiMark { + } + + return replacedText; +- } ++ }, + + /** + * Checks matched Emails for validity and replace valid links with html elements +@@ -708,7 +752,7 @@ export default class ExpensiMark { + replacedText = replacedText.concat(textToCheck.substr(startIndex)); + } + return replacedText; +- } ++ }, + + /** + * replace block element with '\n' if : +@@ -748,7 +792,7 @@ export default class ExpensiMark { + }); + + return joinedText; +- } ++ }, + + /** + * Replaces HTML with markdown +@@ -775,7 +819,7 @@ export default class ExpensiMark { + generatedMarkdown = generatedMarkdown.replace(rule.regex, rule.replacement); + }); + return Str.htmlDecode(this.replaceBlockElementWithNewLine(generatedMarkdown)); +- } ++ }, + + /** + * Convert HTML to text +@@ -794,7 +838,7 @@ export default class ExpensiMark { + // We use 'htmlDecode' instead of 'unescape' to replace entities like ' ' + replacedText = Str.htmlDecode(replacedText); + return replacedText; +- } ++ }, + + /** + * Modify text for Quotes replacing chevrons with html elements +@@ -857,7 +901,7 @@ export default class ExpensiMark { + replacedText = textToCheck; + } + return replacedText; +- } ++ }, + + /** + * Format the content of blockquote if the text matches the regex or else just return the original text +@@ -878,7 +922,7 @@ export default class ExpensiMark { + return replacement(textToFormat); + } + return textToCheck; +- } ++ }, + + /** + * Check if the input text includes only the open or the close tag of an element. +@@ -917,7 +961,7 @@ export default class ExpensiMark { + + // If there are any tags left in the stack, they're unclosed + return tagStack.length !== 0; +- } ++ }, + + /** + * @param {String} comment +@@ -932,14 +976,14 @@ export default class ExpensiMark { + const matches = [...htmlString.matchAll(regex)]; + + // Element 1 from match is the regex group if it exists which contains the link URLs +- const links = _.map(matches, match => Str.sanitizeURL(match[1])); ++ const links = matches.map(match => Str.sanitizeURL(match[1])); + return links; + } catch (e) { + // eslint-disable-next-line no-console + console.warn('Error parsing url in ExpensiMark.extractLinksInMarkdownComment', {error: e}); + return undefined; + } +- } ++ }, + + /** + * Compares two markdown comments and returns a list of the links removed in a new comment. +@@ -951,8 +995,8 @@ export default class ExpensiMark { + getRemovedMarkdownLinks(oldComment, newComment) { + const linksInOld = this.extractLinksInMarkdownComment(oldComment); + const linksInNew = this.extractLinksInMarkdownComment(newComment); +- return linksInOld === undefined || linksInNew === undefined ? [] : _.difference(linksInOld, linksInNew); +- } ++ return linksInOld === undefined || linksInNew === undefined ? [] : linksInOld.filter(link => !linksInNew.includes(link)); ++ }, + + /** + * Replace MD characters with their HTML entity equivalent +@@ -975,5 +1019,11 @@ export default class ExpensiMark { + }; + + return text.replace(pattern, char => entities[char] || char); +- } ++ }, ++} ++ ++ExpensiMark.initialize(); ++ ++return ExpensiMark; ++ + } diff --git a/example/package.json b/example/package.json index e6018545..f76b3136 100644 --- a/example/package.json +++ b/example/package.json @@ -10,6 +10,7 @@ "build:ios": "cd ios && xcodebuild -workspace LiveMarkdownExample.xcworkspace -scheme LiveMarkdownExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" }, "dependencies": { + "expensify-common": "Expensify/expensify-common#4e020cfa13ffabde14313c92b341285aeb919f29", "react": "18.2.0", "react-native": "0.73.4", "react-native-reanimated": "^3.8.1" diff --git a/example/src/App.tsx b/example/src/App.tsx index a107ab47..c01a8351 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -4,7 +4,8 @@ import {Button, Platform, StyleSheet, Text, View} from 'react-native'; import {MarkdownTextInput, useMarkdownParser} from '@expensify/react-native-live-markdown'; import type {TextInput} from 'react-native'; -import {useSharedValue, withRepeat, withTiming} from 'react-native-reanimated'; + +import {makeExpensiMark} from 'expensify-common/lib/ExpensiMark'; const DEFAULT_TEXT = ['Hello, *world*!'].join('\n'); @@ -71,8 +72,9 @@ export default function App() { const parser = useMarkdownParser((text: string) => { 'worklet'; - // eslint-disable-next-line no-console - // console.log(_WORKLET, Math.random()); + const parser = makeExpensiMark(); + const html = parser.replace(text, {shouldKeepRawInput: true}); + console.log(html); const matches = [...text.matchAll(/[@#][a-z]+/gi)]; diff --git a/package.json b/package.json index 4dae72c2..0cc35f5e 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,8 @@ "typescript": "^5.3.3" }, "resolutions": { - "@types/react": "17.0.21" + "@types/react": "17.0.21", + "expensify-common@Expensify/expensify-common#4e020cfa13ffabde14313c92b341285aeb919f29": "patch:expensify-common@https%3A//github.com/Expensify/expensify-common.git%23commit=4e020cfa13ffabde14313c92b341285aeb919f29#./.yarn/patches/expensify-common-https-ae2a96d818.patch" }, "peerDependencies": { "react": "*", diff --git a/yarn.lock b/yarn.lock index 3853a018..d9efb454 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1991,6 +1991,7 @@ __metadata: "@react-native/babel-preset": 0.73.21 "@react-native/metro-config": 0.73.5 babel-plugin-module-resolver: ^5.0.0 + expensify-common: "Expensify/expensify-common#4e020cfa13ffabde14313c92b341285aeb919f29" pod-install: ^0.1.0 react: 18.2.0 react-native: 0.73.4 @@ -4838,6 +4839,13 @@ __metadata: languageName: node linkType: hard +"classnames@npm:2.5.0": + version: 2.5.0 + resolution: "classnames@npm:2.5.0" + checksum: 7805e0ed49790dd11da5da4d8509dbad2d9ed8ec817bfa01d3b69d40dc752ee4df757b9e845371a42547989863db76c463619c446aa24cdbd253d65ef927455e + languageName: node + linkType: hard + "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -4900,6 +4908,17 @@ __metadata: languageName: node linkType: hard +"clipboard@npm:2.0.11": + version: 2.0.11 + resolution: "clipboard@npm:2.0.11" + dependencies: + good-listener: ^1.2.2 + select: ^1.1.2 + tiny-emitter: ^2.0.0 + checksum: 413055a6038e43898e0e895216b58ed54fbf386f091cb00188875ef35b186cefbd258acdf4cb4b0ac87cbc00de936f41b45dde9fe1fd1a57f7babb28363b8748 + languageName: node + linkType: hard + "cliui@npm:^6.0.0": version: 6.0.0 resolution: "cliui@npm:6.0.0" @@ -5793,6 +5812,13 @@ __metadata: languageName: node linkType: hard +"delegate@npm:^3.1.2": + version: 3.2.0 + resolution: "delegate@npm:3.2.0" + checksum: d943058fe05897228b158cbd1bab05164df28c8f54127873231d6b03b0a5acc1b3ee1f98ac70ccc9b79cd84aa47118a7de111fee2923753491583905069da27d + languageName: node + linkType: hard + "denodeify@npm:^1.2.1": version: 1.2.1 resolution: "denodeify@npm:1.2.1" @@ -6980,6 +7006,27 @@ __metadata: languageName: node linkType: hard +"expensify-common@Expensify/expensify-common#4e020cfa13ffabde14313c92b341285aeb919f29": + version: 1.0.0 + resolution: "expensify-common@https://github.com/Expensify/expensify-common.git#commit=4e020cfa13ffabde14313c92b341285aeb919f29" + dependencies: + classnames: 2.5.0 + clipboard: 2.0.11 + html-entities: ^2.4.0 + jquery: 3.6.0 + localforage: ^1.10.0 + lodash: 4.17.21 + prop-types: 15.8.1 + react: 16.12.0 + react-dom: 16.12.0 + semver: ^7.6.0 + simply-deferred: "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5" + ua-parser-js: ^1.0.37 + underscore: 1.13.6 + checksum: 65b8d905005076a99dd98b50b8c424c1708133ee7b855b1071812a542164fc28096bb3bec51ed85623dfa30156fce23d552823386ff2f24a5c5a2fd21ebd9be1 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -7740,6 +7787,15 @@ __metadata: languageName: node linkType: hard +"good-listener@npm:^1.2.2": + version: 1.2.2 + resolution: "good-listener@npm:1.2.2" + dependencies: + delegate: ^3.1.2 + checksum: f39fb82c4e41524f56104cfd2d7aef1a88e72f3f75139115fbdf98cc7d844e0c1b39218b2e83438c6188727bf904ed78c7f0f2feff67b32833bc3af7f0202b33 + languageName: node + linkType: hard + "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -7956,6 +8012,13 @@ __metadata: languageName: node linkType: hard +"html-entities@npm:^2.4.0": + version: 2.5.2 + resolution: "html-entities@npm:2.5.2" + checksum: b23f4a07d33d49ade1994069af4e13d31650e3fb62621e92ae10ecdf01d1a98065c78fd20fdc92b4c7881612210b37c275f2c9fba9777650ab0d6f2ceb3b99b6 + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -8112,6 +8175,13 @@ __metadata: languageName: node linkType: hard +"immediate@npm:~3.0.5": + version: 3.0.6 + resolution: "immediate@npm:3.0.6" + checksum: f9b3486477555997657f70318cc8d3416159f208bec4cca3ff3442fd266bc23f50f0c9bd8547e1371a6b5e82b821ec9a7044a4f7b944798b25aa3cc6d5e63e62 + languageName: node + linkType: hard + "import-fresh@npm:^2.0.0": version: 2.0.0 resolution: "import-fresh@npm:2.0.0" @@ -9533,6 +9603,13 @@ __metadata: languageName: node linkType: hard +"jquery@npm:3.6.0": + version: 3.6.0 + resolution: "jquery@npm:3.6.0" + checksum: 8fd5fef4aa48fd374ec716dd1c1df1af407814a228e15c1260ca140de3a697c2a77c30c54ff1d238b6a3ab4ddc445ddeef9adce6c6d28e4869d85eb9d3951c0e + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -9863,6 +9940,15 @@ __metadata: languageName: node linkType: hard +"lie@npm:3.1.1": + version: 3.1.1 + resolution: "lie@npm:3.1.1" + dependencies: + immediate: ~3.0.5 + checksum: 6da9f2121d2dbd15f1eca44c0c7e211e66a99c7b326ec8312645f3648935bc3a658cf0e9fa7b5f10144d9e2641500b4f55bd32754607c3de945b5f443e50ddd1 + languageName: node + linkType: hard + "lighthouse-logger@npm:^1.0.0": version: 1.4.2 resolution: "lighthouse-logger@npm:1.4.2" @@ -9892,6 +9978,15 @@ __metadata: languageName: node linkType: hard +"localforage@npm:^1.10.0": + version: 1.10.0 + resolution: "localforage@npm:1.10.0" + dependencies: + lie: 3.1.1 + checksum: f2978b434dafff9bcb0d9498de57d97eba165402419939c944412e179cab1854782830b5ec196212560b22712d1dd03918939f59cf1d4fc1d756fca7950086cf + languageName: node + linkType: hard + "locate-path@npm:^2.0.0": version: 2.0.0 resolution: "locate-path@npm:2.0.0" @@ -11731,7 +11826,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:15.8.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -11894,6 +11989,20 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:16.12.0": + version: 16.12.0 + resolution: "react-dom@npm:16.12.0" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + prop-types: ^15.6.2 + scheduler: ^0.18.0 + peerDependencies: + react: ^16.0.0 + checksum: e3bc9bf9ea4e28abfe83ec9200ebc45c86795ded9ad486c955d28c6df4ebde94359c841b8096209d13b017c0a3a751b6be34fd123105faad73467bfbbe8c84fb + languageName: node + linkType: hard + "react-is@npm:^16.12.0 || ^17.0.0 || ^18.0.0, react-is@npm:^18.0.0": version: 18.2.0 resolution: "react-is@npm:18.2.0" @@ -12056,6 +12165,17 @@ __metadata: languageName: node linkType: hard +"react@npm:16.12.0": + version: 16.12.0 + resolution: "react@npm:16.12.0" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + prop-types: ^15.6.2 + checksum: 6c5b5740c7521a41142d012d937db7520658a8317ea6ea9ae5d40ec1ef650596088e048ffebe673f9ddb640f876050d54720905b92250ce43cad0fd1103e7f4c + languageName: node + linkType: hard + "react@npm:18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0" @@ -12692,6 +12812,23 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.18.0": + version: 0.18.0 + resolution: "scheduler@npm:0.18.0" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + checksum: b6e0b9e0086b200b114f1ef7f9fcac966ad5d84cd3b1867174be0549d3ff713d1fd22d5574cb024b7d3eadf3307f2afc1cb5bfbf28b0d3bc34b200183800fcae + languageName: node + linkType: hard + +"select@npm:^1.1.2": + version: 1.1.2 + resolution: "select@npm:1.1.2" + checksum: 4346151e94f226ea6131e44e68e6d837f3fdee64831b756dd657cc0b02f4cb5107f867cb34a1d1216ab7737d0bf0645d44546afb030bbd8d64e891f5e4c4814e + languageName: node + linkType: hard + "semver-diff@npm:^4.0.0": version: 4.0.0 resolution: "semver-diff@npm:4.0.0" @@ -12752,6 +12889,17 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.6.0": + version: 7.6.0 + resolution: "semver@npm:7.6.0" + dependencies: + lru-cache: ^6.0.0 + bin: + semver: bin/semver.js + checksum: 7427f05b70786c696640edc29fdd4bc33b2acf3bbe1740b955029044f80575fc664e1a512e4113c3af21e767154a94b4aa214bf6cd6e42a1f6dba5914e0b208c + languageName: node + linkType: hard + "send@npm:0.18.0": version: 0.18.0 resolution: "send@npm:0.18.0" @@ -12923,6 +13071,13 @@ __metadata: languageName: node linkType: hard +"simply-deferred@git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5": + version: 3.0.0 + resolution: "simply-deferred@https://github.com/Expensify/simply-deferred.git#commit=77a08a95754660c7bd6e0b6979fdf84e8e831bf5" + checksum: 6da4065e2450667db73358f7e7212bd93e0ac0dc1c9876a20422acb2280bcbec50f59dabcb83db111630f987fe57ec6a8302f945e7509e33ba717d38c89122fd + languageName: node + linkType: hard + "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" @@ -13530,6 +13685,13 @@ __metadata: languageName: node linkType: hard +"tiny-emitter@npm:^2.0.0": + version: 2.1.0 + resolution: "tiny-emitter@npm:2.1.0" + checksum: fbcfb5145751a0e3b109507a828eb6d6d4501352ab7bb33eccef46e22e9d9ad3953158870a6966a59e57ab7c3f9cfac7cab8521db4de6a5e757012f4677df2dd + languageName: node + linkType: hard + "titleize@npm:^3.0.0": version: 3.0.0 resolution: "titleize@npm:3.0.0" @@ -13899,7 +14061,7 @@ __metadata: languageName: node linkType: hard -"ua-parser-js@npm:^1.0.35": +"ua-parser-js@npm:^1.0.35, ua-parser-js@npm:^1.0.37": version: 1.0.37 resolution: "ua-parser-js@npm:1.0.37" checksum: 4d481c720d523366d7762dc8a46a1b58967d979aacf786f9ceceb1cd767de069f64a4bdffb63956294f1c0696eb465ddb950f28ba90571709e33521b4bd75e07 @@ -13934,7 +14096,7 @@ __metadata: languageName: node linkType: hard -"underscore@npm:^1.13.1": +"underscore@npm:1.13.6, underscore@npm:^1.13.1": version: 1.13.6 resolution: "underscore@npm:1.13.6" checksum: d5cedd14a9d0d91dd38c1ce6169e4455bb931f0aaf354108e47bd46d3f2da7464d49b2171a5cf786d61963204a42d01ea1332a903b7342ad428deaafaf70ec36 From 5a749ff4d9065d442472cd5d2e0dedbb9dbb7efe Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Wed, 27 Mar 2024 12:38:42 +0100 Subject: [PATCH 006/124] add useExpensiMarkParser --- example/src/App.tsx | 21 +-- example/src/useExpensiMarkParser.ts | 253 ++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+), 18 deletions(-) create mode 100644 example/src/useExpensiMarkParser.ts diff --git a/example/src/App.tsx b/example/src/App.tsx index c01a8351..d8676c64 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -2,10 +2,9 @@ import * as React from 'react'; import {Button, Platform, StyleSheet, Text, View} from 'react-native'; -import {MarkdownTextInput, useMarkdownParser} from '@expensify/react-native-live-markdown'; +import {MarkdownTextInput} from '@expensify/react-native-live-markdown'; import type {TextInput} from 'react-native'; - -import {makeExpensiMark} from 'expensify-common/lib/ExpensiMark'; +import useExpensiMarkParser from './useExpensiMarkParser'; const DEFAULT_TEXT = ['Hello, *world*!'].join('\n'); @@ -69,21 +68,7 @@ export default function App() { // TODO: use MarkdownTextInput ref instead of TextInput ref const ref = React.useRef(null); - const parser = useMarkdownParser((text: string) => { - 'worklet'; - - const parser = makeExpensiMark(); - const html = parser.replace(text, {shouldKeepRawInput: true}); - console.log(html); - - const matches = [...text.matchAll(/[@#][a-z]+/gi)]; - - return matches.map((match) => ({ - start: match.index, - length: match[0].length, - type: match[0].startsWith('@') ? 'mention-here' : 'link', - })); - }, []); + const parser = useExpensiMarkParser(); return ( diff --git a/example/src/useExpensiMarkParser.ts b/example/src/useExpensiMarkParser.ts new file mode 100644 index 00000000..0331099d --- /dev/null +++ b/example/src/useExpensiMarkParser.ts @@ -0,0 +1,253 @@ +import {makeExpensiMark} from 'expensify-common/lib/ExpensiMark'; +import {useMarkdownParser} from '@expensify/react-native-live-markdown'; + +type MarkdownType = 'bold' | 'italic' | 'strikethrough' | 'emoji' | 'mention-here' | 'mention-user' | 'link' | 'code' | 'pre' | 'blockquote' | 'h1' | 'syntax'; +type Range = { + type: MarkdownType; + start: number; + length: number; + depth?: number; +}; +type Token = ['TEXT' | 'HTML', string]; +type StackItem = {tag: string; children: Array}; + +function unescapeText(text: string): string { + 'worklet'; + + return Object.entries({'&': '&', '<': '<', '>': '>', '"': '"', ''': "'"}).reduce((acc, [key, value]) => acc.replace(new RegExp(key, 'g'), value), text); +} + +function parseMarkdownToHTML(markdown: string): string { + 'worklet'; + + const parser = makeExpensiMark(); + const html = parser.replace(markdown, { + shouldKeepRawInput: true, + }); + return html as string; +} + +function parseHTMLToTokens(html: string): Token[] { + 'worklet'; + + const tokens: Token[] = []; + let left = 0; + // eslint-disable-next-line no-constant-condition + while (true) { + const open = html.indexOf('<', left); + if (open === -1) { + if (left < html.length) { + tokens.push(['TEXT', html.substring(left)]); + } + break; + } + if (open !== left) { + tokens.push(['TEXT', html.substring(left, open)]); + } + const close = html.indexOf('>', open); + if (close === -1) { + throw new Error('Invalid HTML: no matching ">"'); + } + tokens.push(['HTML', html.substring(open, close + 1)]); + left = close + 1; + } + return tokens; +} + +function parseTokensToTree(tokens: Token[]): StackItem { + 'worklet'; + + const stack: StackItem[] = [{tag: '<>', children: []}]; + tokens.forEach(([type, payload]) => { + if (type === 'TEXT') { + const text = unescapeText(payload); + const top = stack[stack.length - 1]; + top!.children.push(text); + } else if (type === 'HTML') { + if (payload.startsWith('') { + processChildren(node); + } else if (node.tag === '') { + appendSyntax('*'); + addChildrenWithStyle(node, 'bold'); + appendSyntax('*'); + } else if (node.tag === '') { + appendSyntax('_'); + addChildrenWithStyle(node, 'italic'); + appendSyntax('_'); + } else if (node.tag === '') { + appendSyntax('~'); + addChildrenWithStyle(node, 'strikethrough'); + appendSyntax('~'); + } else if (node.tag === '') { + addChildrenWithStyle(node, 'emoji'); + } else if (node.tag === '') { + appendSyntax('`'); + addChildrenWithStyle(node, 'code'); + appendSyntax('`'); + } else if (node.tag === '') { + addChildrenWithStyle(node, 'mention-here'); + } else if (node.tag === '') { + addChildrenWithStyle(node, 'mention-user'); + } else if (node.tag === '
') { + appendSyntax('>'); + addChildrenWithStyle(node, 'blockquote'); + // compensate for "> " at the beginning + if (ranges.length > 0) { + const curr = ranges[ranges.length - 1]; + curr!.start -= 1; + curr!.length += 1; + } + } else if (node.tag === '

') { + appendSyntax('# '); + addChildrenWithStyle(node, 'h1'); + } else if (node.tag.startsWith(' a.start - b.start || b.length - a.length || getTagPriority(b.type) - getTagPriority(a.type) || 0); +} + +function groupRanges(ranges: Range[]) { + 'worklet'; + + const lastVisibleRangeIndex: {[key in MarkdownType]?: number} = {}; + + return ranges.reduce((acc, range) => { + const start = range.start; + const end = range.start + range.length; + + const rangeWithSameStyleIndex = lastVisibleRangeIndex[range.type]; + const sameStyleRange = rangeWithSameStyleIndex !== undefined ? acc[rangeWithSameStyleIndex] : undefined; + + if (sameStyleRange && sameStyleRange.start <= start && sameStyleRange.start + sameStyleRange.length >= end && range.length > 1) { + // increment depth of overlapping range + sameStyleRange.depth = (sameStyleRange.depth || 1) + 1; + } else { + lastVisibleRangeIndex[range.type] = acc.length; + acc.push(range); + } + + return acc; + }, [] as Range[]); +} + +function parseExpensiMarkToRanges(markdown: string): Range[] { + 'worklet'; + + const html = parseMarkdownToHTML(markdown); + const tokens = parseHTMLToTokens(html); + const tree = parseTokensToTree(tokens); + const [text, ranges] = parseTreeToTextAndRanges(tree); + if (text !== markdown) { + // text mismatch, don't return any ranges + return []; + } + const sortedRanges = sortRanges(ranges); + const groupedRanges = groupRanges(sortedRanges); + return groupedRanges; +} + +function useExpensiMarkParser() { + const parser = useMarkdownParser((markdown: string) => { + 'worklet'; + + return parseExpensiMarkToRanges(markdown); + }, []); + + return parser; +} + +export default useExpensiMarkParser; From 03de712296accb9cca219b4035cd2cc031b1976f Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Wed, 27 Mar 2024 12:41:27 +0100 Subject: [PATCH 007/124] rename escape to escapeText and stringify error --- .../expensify-common-https-ae2a96d818.patch | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.yarn/patches/expensify-common-https-ae2a96d818.patch b/.yarn/patches/expensify-common-https-ae2a96d818.patch index 202390e4..68a1bc0c 100644 --- a/.yarn/patches/expensify-common-https-ae2a96d818.patch +++ b/.yarn/patches/expensify-common-https-ae2a96d818.patch @@ -1,5 +1,5 @@ diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js -index 02d408c88f1823204041a34c386a23720abab104..6cfbff608194e19241e1ae9a86cc1164f19b7189 100644 +index 02d408c88f1823204041a34c386a23720abab104..b3377f1b30d4428ac052f21e45fa33878dcc765e 100644 --- a/lib/ExpensiMark.js +++ b/lib/ExpensiMark.js @@ -1,4 +1,3 @@ @@ -34,7 +34,7 @@ index 02d408c88f1823204041a34c386a23720abab104..6cfbff608194e19241e1ae9a86cc1164 * @returns {String} */ replace(text, {filterRules = [], shouldEscapeText = true, shouldKeepRawInput = false} = {}) { -+ function escape(text) { ++ function escapeText(text) { + const matchHtmlRegExp = /["'&<>]/g; + + return text.replace(matchHtmlRegExp, function(match) { @@ -57,18 +57,21 @@ index 02d408c88f1823204041a34c386a23720abab104..6cfbff608194e19241e1ae9a86cc1164 + // This ensures that any html the user puts into the comment field shows as raw html - let replacedText = shouldEscapeText ? _.escape(text) : text; -+ let replacedText = shouldEscapeText ? escape(text) : text; ++ let replacedText = shouldEscapeText ? escapeText(text) : text; const enabledRules = shouldKeepRawInput ? this.shouldKeepWhitespaceRules : this.rules; - const rules = _.isEmpty(filterRules) ? enabledRules : _.filter(this.rules, rule => _.contains(filterRules, rule.name)); + const rules = filterRules.length === 0 ? enabledRules : this.rules.filter(rule => filterRules.includes(rule.name)); try { rules.forEach((rule) => { -@@ -557,12 +580,33 @@ export default class ExpensiMark { +@@ -555,14 +578,35 @@ export default class ExpensiMark { + }); + } catch (e) { // eslint-disable-next-line no-console - console.warn('Error replacing text with html in ExpensiMark.replace', {error: e}); - -+ function escape(text) { +- console.warn('Error replacing text with html in ExpensiMark.replace', {error: e}); ++ console.warn('Error replacing text with html in ExpensiMark.replace', {error: String(e)}); ++ ++ function escapeText(text) { + const matchHtmlRegExp = /["'&<>]/g; + + return text.replace(matchHtmlRegExp, function(match) { @@ -88,10 +91,10 @@ index 02d408c88f1823204041a34c386a23720abab104..6cfbff608194e19241e1ae9a86cc1164 + } + }); + } -+ + // We want to return text without applying rules if exception occurs during replacing - return shouldEscapeText ? _.escape(text) : text; -+ return shouldEscapeText ? escape(text) : text; ++ return shouldEscapeText ? escapeText(text) : text; } return replacedText; From 0cdc5c303158698ea2d81c6fe9ce7cc16584246a Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Wed, 27 Mar 2024 12:52:37 +0100 Subject: [PATCH 008/124] workletize str.js --- .../expensify-common-https-ae2a96d818.patch | 876 ++++++++++++++---- 1 file changed, 686 insertions(+), 190 deletions(-) diff --git a/.yarn/patches/expensify-common-https-ae2a96d818.patch b/.yarn/patches/expensify-common-https-ae2a96d818.patch index 68a1bc0c..f140ae27 100644 --- a/.yarn/patches/expensify-common-https-ae2a96d818.patch +++ b/.yarn/patches/expensify-common-https-ae2a96d818.patch @@ -1,218 +1,714 @@ -diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js -index 02d408c88f1823204041a34c386a23720abab104..b3377f1b30d4428ac052f21e45fa33878dcc765e 100644 ---- a/lib/ExpensiMark.js -+++ b/lib/ExpensiMark.js -@@ -1,4 +1,3 @@ +diff --git a/lib/str.js b/lib/str.js +index 813e51eb5988a7982bab2519805b29d932bef80a..b7323a608d49f50637f221f025db3341f60da5f7 100644 +--- a/lib/str.js ++++ b/lib/str.js +@@ -1,11 +1,15 @@ + /* eslint-disable no-control-regex */ -import _ from 'underscore'; - import Str from './str'; - import {MARKDOWN_URL_REGEX, LOOSE_URL_REGEX, URL_REGEX} from './Url'; + import {encode, decode} from 'html-entities'; import {CONST} from './CONST'; -@@ -9,8 +8,11 @@ const MARKDOWN_IMAGE_REGEX = new RegExp(`\\!${MARKDOWN_LINK}`, 'gi'); + import {URL_REGEX} from './Url'; - const SLACK_SPAN_NEW_LINE_TAG = ''; + const REMOVE_SMS_DOMAIN_PATTERN = new RegExp(`@${CONST.SMS.DOMAIN}`, 'gi'); --export default class ExpensiMark { -- constructor() { -+export function makeExpensiMark() { -+'worklet'; ++// TODO: remove jQuery and underscore ++const jQuery = undefined; ++const $ = {}; ++const _ = {}; + -+const ExpensiMark = { -+ initialize() { - /** - * The list of regex replacements to do on a comment. Check the link regex is first so links are processed - * before other delimiters -@@ -516,7 +518,7 @@ export default class ExpensiMark { - * @type {Number} - */ - this.currentQuoteDepth = 0; -- } -+ }, + const Str = { /** - * Replaces markdown with html elements -@@ -530,10 +532,31 @@ export default class ExpensiMark { - * @returns {String} +@@ -16,6 +20,8 @@ const Str = { + * @return {Boolean} */ - replace(text, {filterRules = [], shouldEscapeText = true, shouldKeepRawInput = false} = {}) { -+ function escapeText(text) { -+ const matchHtmlRegExp = /["'&<>]/g; -+ -+ return text.replace(matchHtmlRegExp, function(match) { -+ switch (match) { -+ case '&': -+ return '&'; -+ case '<': -+ return '<'; -+ case '>': -+ return '>'; -+ case '"': -+ return '"'; -+ case "'": -+ return '''; -+ default: -+ return match; -+ } -+ }); -+ } -+ - // This ensures that any html the user puts into the comment field shows as raw html -- let replacedText = shouldEscapeText ? _.escape(text) : text; -+ let replacedText = shouldEscapeText ? escapeText(text) : text; - const enabledRules = shouldKeepRawInput ? this.shouldKeepWhitespaceRules : this.rules; -- const rules = _.isEmpty(filterRules) ? enabledRules : _.filter(this.rules, rule => _.contains(filterRules, rule.name)); -+ const rules = filterRules.length === 0 ? enabledRules : this.rules.filter(rule => filterRules.includes(rule.name)); - - try { - rules.forEach((rule) => { -@@ -555,14 +578,35 @@ export default class ExpensiMark { - }); - } catch (e) { - // eslint-disable-next-line no-console -- console.warn('Error replacing text with html in ExpensiMark.replace', {error: e}); -+ console.warn('Error replacing text with html in ExpensiMark.replace', {error: String(e)}); + endsWith(str, suffix) { ++ 'worklet'; + -+ function escapeText(text) { -+ const matchHtmlRegExp = /["'&<>]/g; -+ -+ return text.replace(matchHtmlRegExp, function(match) { -+ switch (match) { -+ case '&': -+ return '&'; -+ case '<': -+ return '<'; -+ case '>': -+ return '>'; -+ case '"': -+ return '"'; -+ case "'": -+ return '''; -+ default: -+ return match; -+ } -+ }); -+ } - - // We want to return text without applying rules if exception occurs during replacing -- return shouldEscapeText ? _.escape(text) : text; -+ return shouldEscapeText ? escapeText(text) : text; + if (!str || !suffix) { + return false; + } +@@ -32,6 +38,8 @@ const Str = { + * @return {Number} The cent value of the @p amountStr. + */ + fromUSDToNumber(amountStr, allowFraction) { ++ 'worklet'; ++ + let amount = String(amountStr).replace(/[^\d.\-()]+/g, ''); + if (amount.match(/\(.*\)/)) { + const modifiedAmount = amount.replace(/[()]/g, ''); +@@ -56,6 +64,8 @@ const Str = { + * @returns {string} + */ + truncateInMiddle(fullStr, maxLength) { ++ 'worklet'; ++ + if (fullStr.length <= maxLength) { + return fullStr; } +@@ -75,6 +85,8 @@ const Str = { + * @returns {string} + */ + nl2br(str) { ++ 'worklet'; ++ + return str.replace(/\n/g, '
'); + }, - return replacedText; -- } -+ }, +@@ -85,6 +97,8 @@ const Str = { + * @return {String} The decoded string. + */ + htmlDecode(s) { ++ 'worklet'; ++ + // Use jQuery if it exists or else use html-entities + if (typeof jQuery !== 'undefined') { + return jQuery('