Expensify / react-native-live-markdown

Drop-in replacement for React Native's TextInput component with Markdown formatting.
https://www.npmjs.com/package/@expensify/react-native-live-markdown
MIT License
706 stars 49 forks source link

Support custom JS parsers without having separate bundle #317

Open tomekzaw opened 3 months ago

tomekzaw commented 3 months ago

Currently, Live Markdown comes with a JS parser/formatter that wraps ExpensiMark and converts Markdown message into a list of ranges (style, location, length). This code needs to be run on the UI runtime while typing so it is executed on a separate JS runtime. Currently, this code is distributed as a separate bundle which is loaded from this file distributed along with the Live Markdown library: https://github.com/Expensify/react-native-live-markdown/blob/main/parser/react-native-live-markdown-parser.js

However, this makes things more difficult to maintain since after each change in expensify-common we need to bump it in @expensify/react-native-live-markdown and then bump both these libraries in Expensify/App repo. This also leads to inconsistencies when E/App and Live Markdown use different versions of expensify-common.

Finally, since the JS parser/formatter is bundled, there's no way to parametrize its behavior based on app state, however in some cases this will be necessary, e.g. to implement:

The objective of this task is to allow passing JS parser/formatter as a JS function directly to MarkdownTextInput component. Here's the current idea for the API:

function App() {
  const parser = useMarkdownParser((markdown: string) => {
    'worklet';
    // some code here
    return [
      {type: 'syntax', start: 2, length: 1},
      {type: 'bold', start: 3, length: 5},
      {type: 'syntax', start: 8, length: 1},
    ];
  }, []);

  return <MarkdownTextInput
        multiline
        autoCapitalize="none"
        value={value}
        onChangeText={setValue}
        style={styles.input}
        ref={ref}
        markdownStyle={markdownStyle}
        parser={parser}
        placeholder="Type here..."
        onSelectionChange={(e) => setSelection(e.nativeEvent.selection)}
        selection={selection}
      />;
}

The current implementation of JS parser/formatter will be exported as the following hook:

const parser = useExpensiMarkParser();
tomekzaw commented 2 months ago

Problem/Solution: https://swmansion.slack.com/archives/C01GTK53T8Q/p1716235671381059

mallenexpensify commented 2 months ago

@tomekzaw do you have the bandwidth to prioritize this? If not, wanna see about tag-teaming it with another engineer or finding a volunteer to take over? I know you posted the P/S just yesterday, there was a month+ before that without an update, which is why I'm asking.

tomekzaw commented 2 months ago

@mallenexpensify Actually, I have a working proof of concept that uses Reanimated worklets (Reanimated is already a dependency so that's cool) so we might be able to use that quite soon. The only problem for the time being is that ExpensiMark still uses jQuery in some places, but this is something that can be parallelized or even made external, what do you think?

mallenexpensify commented 2 months ago

@tomekzaw , honestly, I have no clue. I'm not familiar with this. Looks like a plan was hatched in this thread and that https://github.com/Expensify/App/issues/42494, linked above, is the next step. thx.

tomekzaw commented 1 month ago

Working on it, looks promising, will share a video recording with a demo early next week.

shubham1206agra commented 2 weeks ago

@tomekzaw Is there any update here?

tomekzaw commented 2 weeks ago

Latest update here

szado commented 2 weeks ago

@tomekzaw can you write a short update here for open source users without access to SM Slack?

tomekzaw commented 2 weeks ago

Sure, will do.

TL;DR in an upcoming version of react-native-live-markdown, MarkdownTextInput component will expose a parser prop that accepts a JS function (must be a worklet, since it's run on the UI thread as you type) and returns an array of ranges (like {start: 2, length: 4, type: 'link'}). This way it will be possible to implement custom formatting logic full in JS with fast-refresh support.

Then, in a following step, instead of having a predefined list of syntax elements (like bold, link or mention-here) customizable only via markdownStyle, it will be possible to use CSS styles (like color, backgroundColor, fontSize, fontWeight, fontStyle etc.), but this is a separate feature.