@expensify/react-native-live-markdown

repository·main·Indexed 23 days ago

https://github.com/expensify/react-native-live-markdown

A high-performance, native-feeling drop-in replacement for React Native's TextInput component that provides live, synchronous Markdown syntax highlighting. It utilizes worklets to execute parsing logic on the UI thread and requires the React Native New Architecture. The library includes the MarkdownTextInput component, the parseExpensiMark utility, and support for custom parser worklets and styling via the markdownStyle prop.

Tokens
4K
Snippets
12
Records
22
Agent score
80%

What's inside @expensify/react-native-live-markdown

  1. Implement a custom parser worklet

    main

    The parser prop allows you to define custom markdown logic. A parser is a function that takes a plaintext string and returns an array of MarkdownRange objects.

    Critical Requirements:

    • The parser function must be marked as a worklet (using the 'worklet'; directive) because it is executed on the UI thread as the user types.
    • It should return an array of objects matching the MarkdownRange interface.

    Supported MarkdownType values: 'bold' | 'italic' | 'strikethrough' | 'emoji' | 'mention-here' | 'mention-user' | 'mention-report' | 'link' | 'code' | 'pre' | 'blockquote' | 'h1' | 'syntax'

    MarkdownRange Interface:

    interface MarkdownRange {
      type: MarkdownType;
      start: number;
      length: number;
      depth?: number;
    }
    function parser(input: string) {
      'worklet';
    
      const ranges = [];
      const regexp = /\*(.*?)\*/g;
      let match;
      while ((match = regexp.exec(input)) !== null) {
        ranges.push({start: match.index, length: 1, type: 'syntax'});
        ranges.push({start: match.index + 1, length: match[1]!.length, type: 'bold'});
        ranges.push({start: match.index + 1 + match[1]!.length, length: 1, type: 'syntax'});
      }
      return ranges;
    }
  2. Customize Markdown styling with markdownStyle

    main

    You can customize the appearance of formatted markdown elements using the markdownStyle prop. This prop accepts a MarkdownStyle object where keys correspond to specific markdown types (e.g., bold, link, code, h1).

    Supported style keys include:

    • syntax
    • link
    • h1
    • emoji
    • blockquote
    • code
    • pre
    • mentionHere
    • mentionUser

    Best Practice: Store the style object outside of the component body or wrap it in React.useMemo to prevent unnecessary re-renders.

    import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
    
    const markdownStyle: MarkdownStyle = {
      syntax: {
        color: 'gray',
      },
      link: {
        color: 'blue',
      },
      h1: {
        fontSize: 25,
      },
      // ... other styles
    };
    
    <MarkdownTextInput
      value={text}
      onChangeText={setText}
      style={styles.input}
      markdownStyle={markdownStyle}
    />
  3. Install @expensify/react-native-live-markdown

    main

    Install the library and its required peer dependencies using your preferred package manager.

    Important Requirements:

    • react-native-worklets must be version 0.7.0 or newer.
    • expensify-common must be version 2.0.115.
    • html-entities must be version 2.5.3 exactly if using the default ExpensiMark parser.
    • The library requires the React Native New Architecture to be enabled.
    • It does not support Expo Go; you must use an Expo Dev Client.

    After installing, install iOS dependencies with CocoaPods and rebuild the native app.

  4. Run the @expensify/react-native-live-markdown-example project

    main

    To run the example project, you must first ensure your React Native environment is set up according to the official React Native Environment Setup guide.

    1. Start the Metro Bundler

    Open a terminal at the root of the project and run:

    npm start
    # OR
    yarn start

    2. Launch the Application

    Open a new terminal window (keep Metro running in the first one) and run the command for your target platform:

    For Android:

    npm run android
    # OR
    yarn android

    For iOS:

    npm run ios
    # OR
    yarn ios

    3. Reloading Changes

    After modifying App.tsx, reload the application to see updates:

    • Android: Press the <kbd>R</kbd> key twice or open the Developer Menu (<kbd>Ctrl</kbd> + <kbd>M</kbd> on Windows/Linux, <kbd>Cmd ⌘</kbd> + <kbd>M</kbd> on macOS) and select "Reload".
    • iOS: Press <kbd>Cmd ⌘</kbd> + <kbd>R</kbd> in the iOS Simulator.
    npm start
    npm run android
    npm run ios
  5. Use MarkdownTextInput for live markdown formatting

    main

    The MarkdownTextInput component is a drop-in replacement for the standard React Native TextInput. It provides live, synchronous formatting on every keystroke. To use the built-in ExpensiMark parser, import parseExpensiMark and pass it to the parser prop.

    import {MarkdownTextInput, parseExpensiMark} from '@expensify/react-native-live-markdown';
    import React from 'react';
    
    export default function App() {
      const [text, setText] = React.useState('Hello, *world*!');
    
      return (
        <MarkdownTextInput
          value={text}
          onChangeText={setText}
          parser={parseExpensiMark}
        />
      );
    }
  6. MarkdownTextInput API Reference

    main

    The MarkdownTextInput component inherits all props from the standard React Native TextInput. It adds the following specific properties:

    PropTypeDefaultNote
    parser(value: string) => MarkdownRange[]undefinedA function that parses the current value and returns an array of ranges. Must be a worklet.
    markdownStyleMarkdownStyleundefinedAdds custom styling to Markdown text. The provided value is merged with the default style object.
  7. Configure React Native Autolinking for Android

    main

    The react-native.config.js file provides configuration for the library's Android integration, specifically for New Architecture support. It defines the componentDescriptors required for the MarkdownTextInputDecoratorViewComponentDescriptor and specifies the cmakeListsPath for CMake builds in the New Architecture.

    module.exports = {
      dependency: {
        platforms: {
          android: {
            componentDescriptors: [
              "MarkdownTextInputDecoratorViewComponentDescriptor",
            ],
            cmakeListsPath: "../android/src/main/new_arch/CMakeLists.txt"
          },
        },
      },
    }
  8. Configure Metro for the example project

    main

    The example project uses a custom Metro configuration to ensure that the root directory and the local directory are both included in the watchFolders. This allows Metro to resolve dependencies correctly across the monorepo-like structure used in the example. It also configures the transformer to use inlineRequires: true for optimized loading.

    const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
    const path = require('path');
    
    const root = path.resolve(__dirname, '..');
    
    /**
     * Metro configuration
     * https://reactnative.dev/docs/metro
     *
     * @type {import('@react-native/metro-config').MetroConfig}
     */
    const config = {
      watchFolders: [root, __dirname],
    
    transformer: {
        getTransformOptions: async () => ({
          transform: {
            experimentalImportSupport: false,
            inlineRequires: true,
          },
        }),
      },
    };
    
    module.exports = mergeConfig(getDefaultConfig(__dirname), config);
  9. Configure Playwright for WebExample

    main

    The WebExample/playwright.config.ts file defines the testing environment for the web version of the project using Playwright. It specifies the test directory, the local web server command used to host the example, and the browser projects to be tested (Chromium, Firefox, and Webkit).

    import {defineConfig, devices} from '@playwright/test';
    // eslint-disable-next-line import/no-relative-packages
    import * as TEST_CONST from '../example/src/testConstants';
    
    export default defineConfig({
      testDir: './__tests__',
      preserveOutput: 'never',
      outputDir: undefined,
      webServer: {
        command: 'npm run web',
        url: TEST_CONST.LOCAL_URL,
        reuseExistingServer: !process.env.CI,
        stdout: 'pipe',
        stderr: 'pipe',
      },
      projects: [
        {
          name: 'Chromium',
          use: {...devices['Desktop Chrome']},
        },
        {
          name: 'Firefox',
          use: {...devices['Desktop Firefox']},
        },
        {
          name: 'Webkit',
          use: {...devices['Desktop Safari']},
        },
      ],
    });
  10. Configure autolinking for @expensify/react-native-live-markdown

    main

    In projects where the library is being used as a local dependency (e.g., in a monorepo or during development), you may need to configure react-native.config.js to ensure the native code is correctly autolinked. This is done by specifying the root path of the package within the dependencies object.

    const path = require('path');
    const pak = require('../package.json');
    
    module.exports = {
      dependencies: {
        [pak.name]: {
          root: path.join(__dirname, '..'),
        },
      },
    };
  11. Parse string units to numbers with parseStringWithUnitToNumber

    main

    The parseStringWithUnitToNumber utility converts string-based dimension values (e.g., '20px') or numbers into a pure integer. This is useful when processing style values that may contain unit suffixes.

    • If the input is a number, it returns the number as-is.
    • If the input is a string containing 'px', it strips the suffix and parses the integer.
    • If the input is null or an empty string, it returns 0.
  12. Configure MarkdownStyle for live markdown rendering

    main

    The MarkdownStyle type defines the visual properties for various markdown elements rendered by the MarkdownTextInputDecoratorView native component. You can use this type to provide a custom theme for syntax highlighting, emojis, links, headers, blockquotes, code blocks, and mentions.

    Key styling groups include:

    • syntax: Controls the color of syntax-highlighted text.
    • emoji: Sets the fontSize and fontFamily for emojis.
    • link: Sets the color for hyperlinks.
    • h1: Sets the fontSize for H1 headers.
    • blockquote: Controls the border and padding for blockquotes.
    • code and pre: Use CodeBlockStyle to configure fonts, colors, and borders for code segments.
    • mentionHere, mentionUser, and mentionReport: Style specific mention types with colors, background colors, and border radii.
    • inlineImage: Controls dimensions and border radius for images embedded in text.
    • loadingIndicator and loadingIndicatorContainer: Style the appearance of loading states.